tabminal 2.0.13 → 2.0.15
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/README.md +238 -105
- package/package.json +6 -4
- 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 +300 -12
- package/src/terminal-manager.mjs +184 -73
- package/src/terminal-session.mjs +233 -15
package/src/persistence.mjs
CHANGED
|
@@ -7,6 +7,8 @@ const BASE_DIR = path.join(HOME_DIR, '.tabminal');
|
|
|
7
7
|
const SESSIONS_DIR = path.join(BASE_DIR, 'sessions');
|
|
8
8
|
const MEMORY_FILE = path.join(BASE_DIR, 'memory.json');
|
|
9
9
|
const CLUSTER_FILE = path.join(BASE_DIR, 'cluster.json');
|
|
10
|
+
const AGENT_TABS_FILE = path.join(BASE_DIR, 'agent-tabs.json');
|
|
11
|
+
const AGENT_CONFIG_FILE = path.join(BASE_DIR, 'agent-config.json');
|
|
10
12
|
const getSessionSnapshotPath = (id) => path.join(SESSIONS_DIR, `${id}.snapshot`);
|
|
11
13
|
|
|
12
14
|
// Ensure directories exist
|
|
@@ -193,6 +195,157 @@ export const saveCluster = async (servers) => {
|
|
|
193
195
|
}
|
|
194
196
|
};
|
|
195
197
|
|
|
198
|
+
// --- ACP Agent Tab Persistence ---
|
|
199
|
+
|
|
200
|
+
function normalizeAgentTabs(tabs) {
|
|
201
|
+
if (!Array.isArray(tabs)) return [];
|
|
202
|
+
const normalized = [];
|
|
203
|
+
for (const entry of tabs) {
|
|
204
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
205
|
+
const id = typeof entry.id === 'string' ? entry.id.trim() : '';
|
|
206
|
+
const agentId = typeof entry.agentId === 'string'
|
|
207
|
+
? entry.agentId.trim()
|
|
208
|
+
: '';
|
|
209
|
+
const cwd = typeof entry.cwd === 'string' ? entry.cwd.trim() : '';
|
|
210
|
+
const acpSessionId = typeof entry.acpSessionId === 'string'
|
|
211
|
+
? entry.acpSessionId.trim()
|
|
212
|
+
: '';
|
|
213
|
+
const terminalSessionId = typeof entry.terminalSessionId === 'string'
|
|
214
|
+
? entry.terminalSessionId.trim()
|
|
215
|
+
: '';
|
|
216
|
+
const createdAt = typeof entry.createdAt === 'string'
|
|
217
|
+
? entry.createdAt.trim()
|
|
218
|
+
: '';
|
|
219
|
+
const title = typeof entry.title === 'string'
|
|
220
|
+
? entry.title
|
|
221
|
+
: '';
|
|
222
|
+
const currentModeId = typeof entry.currentModeId === 'string'
|
|
223
|
+
? entry.currentModeId
|
|
224
|
+
: '';
|
|
225
|
+
const availableModes = Array.isArray(entry.availableModes)
|
|
226
|
+
? entry.availableModes
|
|
227
|
+
: [];
|
|
228
|
+
const availableCommands = Array.isArray(entry.availableCommands)
|
|
229
|
+
? entry.availableCommands
|
|
230
|
+
: [];
|
|
231
|
+
const configOptions = Array.isArray(entry.configOptions)
|
|
232
|
+
? entry.configOptions
|
|
233
|
+
: [];
|
|
234
|
+
const messages = Array.isArray(entry.messages)
|
|
235
|
+
? entry.messages
|
|
236
|
+
: [];
|
|
237
|
+
const toolCalls = Array.isArray(entry.toolCalls)
|
|
238
|
+
? entry.toolCalls
|
|
239
|
+
: [];
|
|
240
|
+
const permissions = Array.isArray(entry.permissions)
|
|
241
|
+
? entry.permissions
|
|
242
|
+
: [];
|
|
243
|
+
const plan = Array.isArray(entry.plan)
|
|
244
|
+
? entry.plan
|
|
245
|
+
: [];
|
|
246
|
+
const usage = entry.usage && typeof entry.usage === 'object'
|
|
247
|
+
? entry.usage
|
|
248
|
+
: null;
|
|
249
|
+
const terminals = Array.isArray(entry.terminals)
|
|
250
|
+
? entry.terminals
|
|
251
|
+
: [];
|
|
252
|
+
if (!id || !agentId || !cwd || !acpSessionId) continue;
|
|
253
|
+
normalized.push({
|
|
254
|
+
id,
|
|
255
|
+
agentId,
|
|
256
|
+
cwd,
|
|
257
|
+
acpSessionId,
|
|
258
|
+
terminalSessionId,
|
|
259
|
+
createdAt,
|
|
260
|
+
title,
|
|
261
|
+
currentModeId,
|
|
262
|
+
availableModes,
|
|
263
|
+
availableCommands,
|
|
264
|
+
configOptions,
|
|
265
|
+
messages,
|
|
266
|
+
toolCalls,
|
|
267
|
+
permissions,
|
|
268
|
+
plan,
|
|
269
|
+
usage,
|
|
270
|
+
terminals
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return normalized;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export const loadAgentTabs = async () => {
|
|
277
|
+
await init();
|
|
278
|
+
try {
|
|
279
|
+
const content = await fs.readFile(AGENT_TABS_FILE, 'utf-8');
|
|
280
|
+
const parsed = JSON.parse(content);
|
|
281
|
+
if (Array.isArray(parsed)) {
|
|
282
|
+
return normalizeAgentTabs(parsed);
|
|
283
|
+
}
|
|
284
|
+
return normalizeAgentTabs(parsed?.tabs);
|
|
285
|
+
} catch {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export const saveAgentTabs = async (tabs) => {
|
|
291
|
+
await init();
|
|
292
|
+
const normalized = normalizeAgentTabs(tabs);
|
|
293
|
+
const payload = { tabs: normalized };
|
|
294
|
+
try {
|
|
295
|
+
await fs.writeFile(AGENT_TABS_FILE, JSON.stringify(payload, null, 2));
|
|
296
|
+
} catch (e) {
|
|
297
|
+
console.error('[Persistence] Failed to save agent tabs:', e);
|
|
298
|
+
throw e;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// --- ACP Agent Config Persistence ---
|
|
303
|
+
|
|
304
|
+
function normalizeAgentEnv(env) {
|
|
305
|
+
if (!env || typeof env !== 'object') return {};
|
|
306
|
+
const normalized = {};
|
|
307
|
+
for (const [key, value] of Object.entries(env)) {
|
|
308
|
+
if (typeof key !== 'string' || !key.trim()) continue;
|
|
309
|
+
normalized[key.trim()] = typeof value === 'string' ? value : '';
|
|
310
|
+
}
|
|
311
|
+
return normalized;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function normalizeAgentConfigs(configs) {
|
|
315
|
+
if (!configs || typeof configs !== 'object') return {};
|
|
316
|
+
const normalized = {};
|
|
317
|
+
for (const [agentId, entry] of Object.entries(configs)) {
|
|
318
|
+
if (typeof agentId !== 'string' || !agentId.trim()) continue;
|
|
319
|
+
normalized[agentId.trim()] = {
|
|
320
|
+
env: normalizeAgentEnv(entry?.env)
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return normalized;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export const loadAgentConfigs = async () => {
|
|
327
|
+
await init();
|
|
328
|
+
try {
|
|
329
|
+
const content = await fs.readFile(AGENT_CONFIG_FILE, 'utf-8');
|
|
330
|
+
const parsed = JSON.parse(content);
|
|
331
|
+
return normalizeAgentConfigs(parsed?.agents || parsed);
|
|
332
|
+
} catch {
|
|
333
|
+
return {};
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export const saveAgentConfigs = async (configs) => {
|
|
338
|
+
await init();
|
|
339
|
+
const normalized = normalizeAgentConfigs(configs);
|
|
340
|
+
const payload = { agents: normalized };
|
|
341
|
+
try {
|
|
342
|
+
await fs.writeFile(AGENT_CONFIG_FILE, JSON.stringify(payload, null, 2));
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.error('[Persistence] Failed to save agent configs:', e);
|
|
345
|
+
throw e;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
196
349
|
// --- Raw Log Persistence ---
|
|
197
350
|
|
|
198
351
|
export const appendSessionLog = async (id, chunk) => {
|
package/src/server.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import process from 'node:process';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -10,9 +11,11 @@ import Koa from 'koa';
|
|
|
10
11
|
import serve from 'koa-static';
|
|
11
12
|
import Router from '@koa/router';
|
|
12
13
|
import bodyParser from 'koa-bodyparser';
|
|
14
|
+
import { formidable } from 'formidable';
|
|
13
15
|
import { WebSocketServer } from 'ws';
|
|
14
16
|
|
|
15
17
|
import { TerminalManager } from './terminal-manager.mjs';
|
|
18
|
+
import { AcpManager } from './acp-manager.mjs';
|
|
16
19
|
import { SystemMonitor } from './system-monitor.mjs';
|
|
17
20
|
import { config } from './config.mjs';
|
|
18
21
|
import { authMiddleware, verifyClient } from './auth.mjs';
|
|
@@ -27,12 +30,61 @@ const publicDir = path.join(__dirname, '..', 'public');
|
|
|
27
30
|
const app = new Koa();
|
|
28
31
|
const router = new Router();
|
|
29
32
|
const SERVER_BOOT_ID = `${Date.now()}`;
|
|
33
|
+
const AGENT_ATTACHMENT_FIELD = 'attachments';
|
|
34
|
+
const MAX_AGENT_ATTACHMENTS = 8;
|
|
35
|
+
const MAX_AGENT_ATTACHMENT_SIZE = 10 * 1024 * 1024;
|
|
36
|
+
const MAX_AGENT_ATTACHMENTS_TOTAL_SIZE = 25 * 1024 * 1024;
|
|
37
|
+
|
|
30
38
|
function debugLog(...args) {
|
|
31
39
|
if (config.debug) {
|
|
32
40
|
console.log(...args);
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
function parseMultipartForm(req, options = {}) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const form = formidable({
|
|
47
|
+
multiples: true,
|
|
48
|
+
allowEmptyFiles: false,
|
|
49
|
+
maxFiles: MAX_AGENT_ATTACHMENTS,
|
|
50
|
+
maxFileSize: MAX_AGENT_ATTACHMENT_SIZE,
|
|
51
|
+
maxTotalFileSize: MAX_AGENT_ATTACHMENTS_TOTAL_SIZE,
|
|
52
|
+
...options
|
|
53
|
+
});
|
|
54
|
+
form.parse(req, (error, fields, files) => {
|
|
55
|
+
if (error) {
|
|
56
|
+
reject(error);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
resolve({ fields, files });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function firstFormFieldValue(value) {
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
return typeof value[0] === 'string' ? value[0] : '';
|
|
67
|
+
}
|
|
68
|
+
return typeof value === 'string' ? value : '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizePromptAttachments(files) {
|
|
72
|
+
const rawList = Array.isArray(files)
|
|
73
|
+
? files
|
|
74
|
+
: (files ? [files] : []);
|
|
75
|
+
return rawList
|
|
76
|
+
.filter((file) => file && typeof file === 'object')
|
|
77
|
+
.map((file) => ({
|
|
78
|
+
id: crypto.randomUUID(),
|
|
79
|
+
name: String(file.originalFilename || 'attachment').trim()
|
|
80
|
+
|| 'attachment',
|
|
81
|
+
mimeType: String(file.mimetype || '').trim(),
|
|
82
|
+
size: Number.isFinite(file.size) ? file.size : 0,
|
|
83
|
+
tempPath: String(file.filepath || '').trim()
|
|
84
|
+
}))
|
|
85
|
+
.filter((file) => file.tempPath);
|
|
86
|
+
}
|
|
87
|
+
|
|
36
88
|
app.use(async (ctx, next) => {
|
|
37
89
|
const origin = ctx.get('Origin');
|
|
38
90
|
if (origin) {
|
|
@@ -130,15 +182,22 @@ app.use(authMiddleware);
|
|
|
130
182
|
|
|
131
183
|
const systemMonitor = new SystemMonitor();
|
|
132
184
|
const terminalManager = new TerminalManager();
|
|
185
|
+
const acpManager = new AcpManager({ terminalManager });
|
|
133
186
|
|
|
134
187
|
// Restore sessions
|
|
135
188
|
(async () => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
189
|
+
acpManager.restoring = true;
|
|
190
|
+
try {
|
|
191
|
+
const restoredSessions = await persistence.loadSessions();
|
|
192
|
+
if (restoredSessions.length > 0) {
|
|
193
|
+
console.log(`[Server] Restoring ${restoredSessions.length} sessions...`);
|
|
194
|
+
for (const data of restoredSessions) {
|
|
195
|
+
terminalManager.createSession(data);
|
|
196
|
+
}
|
|
141
197
|
}
|
|
198
|
+
await acpManager.restoreTabs(new Set(terminalManager.sessions.keys()));
|
|
199
|
+
} finally {
|
|
200
|
+
acpManager.restoring = false;
|
|
142
201
|
}
|
|
143
202
|
})();
|
|
144
203
|
|
|
@@ -199,9 +258,16 @@ router.post('/api/sessions', (ctx) => {
|
|
|
199
258
|
};
|
|
200
259
|
});
|
|
201
260
|
|
|
202
|
-
router.delete('/api/sessions/:id', (ctx) => {
|
|
261
|
+
router.delete('/api/sessions/:id', async (ctx) => {
|
|
203
262
|
const { id } = ctx.params;
|
|
204
|
-
terminalManager.
|
|
263
|
+
const session = terminalManager.getSession(id);
|
|
264
|
+
if (session?.managed?.kind === 'agent-terminal') {
|
|
265
|
+
await acpManager.releaseManagedTerminalSession(id, { destroy: true });
|
|
266
|
+
ctx.status = 204;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
await acpManager.closeTabsForTerminalSession(id);
|
|
270
|
+
await terminalManager.removeSession(id);
|
|
205
271
|
ctx.status = 204;
|
|
206
272
|
});
|
|
207
273
|
|
|
@@ -269,6 +335,197 @@ router.put('/api/cluster', async (ctx) => {
|
|
|
269
335
|
}
|
|
270
336
|
});
|
|
271
337
|
|
|
338
|
+
router.get('/api/agents', async (ctx) => {
|
|
339
|
+
ctx.body = await acpManager.listState();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
router.get('/api/agents/config', async (ctx) => {
|
|
343
|
+
ctx.body = {
|
|
344
|
+
configs: await acpManager.listAgentConfigs()
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
router.put('/api/agents/config/:agentId', async (ctx) => {
|
|
349
|
+
const { agentId } = ctx.params;
|
|
350
|
+
const { env, clearEnvKeys } = ctx.request.body || {};
|
|
351
|
+
try {
|
|
352
|
+
const configState = await acpManager.updateAgentConfig(agentId, {
|
|
353
|
+
env: typeof env === 'object' && env ? env : {},
|
|
354
|
+
clearEnvKeys: Array.isArray(clearEnvKeys) ? clearEnvKeys : []
|
|
355
|
+
});
|
|
356
|
+
ctx.body = {
|
|
357
|
+
config: configState,
|
|
358
|
+
definitions: await acpManager.listDefinitions()
|
|
359
|
+
};
|
|
360
|
+
} catch (error) {
|
|
361
|
+
ctx.status = 400;
|
|
362
|
+
ctx.body = {
|
|
363
|
+
error: error?.message || 'Failed to save agent config'
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
router.delete('/api/agents/config/:agentId', async (ctx) => {
|
|
369
|
+
const { agentId } = ctx.params;
|
|
370
|
+
try {
|
|
371
|
+
const configState = await acpManager.clearAgentConfig(agentId);
|
|
372
|
+
ctx.body = {
|
|
373
|
+
config: configState,
|
|
374
|
+
definitions: await acpManager.listDefinitions()
|
|
375
|
+
};
|
|
376
|
+
} catch (error) {
|
|
377
|
+
ctx.status = 400;
|
|
378
|
+
ctx.body = {
|
|
379
|
+
error: error?.message || 'Failed to clear agent config'
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
router.post('/api/agents/tabs', async (ctx) => {
|
|
385
|
+
const { agentId, cwd, terminalSessionId, modeId } = ctx.request.body || {};
|
|
386
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
387
|
+
ctx.status = 400;
|
|
388
|
+
ctx.body = { error: 'agentId is required' };
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (!cwd || typeof cwd !== 'string') {
|
|
392
|
+
ctx.status = 400;
|
|
393
|
+
ctx.body = { error: 'cwd is required' };
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
ctx.status = 201;
|
|
399
|
+
ctx.body = await acpManager.createTab({
|
|
400
|
+
agentId,
|
|
401
|
+
cwd,
|
|
402
|
+
terminalSessionId: typeof terminalSessionId === 'string'
|
|
403
|
+
? terminalSessionId
|
|
404
|
+
: '',
|
|
405
|
+
modeId: typeof modeId === 'string' ? modeId : ''
|
|
406
|
+
});
|
|
407
|
+
} catch (error) {
|
|
408
|
+
ctx.status = 500;
|
|
409
|
+
ctx.body = { error: error?.message || 'Failed to create agent tab' };
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
router.post('/api/agents/tabs/:tabId/prompt', async (ctx) => {
|
|
414
|
+
const { tabId } = ctx.params;
|
|
415
|
+
let text = '';
|
|
416
|
+
let attachments = [];
|
|
417
|
+
|
|
418
|
+
if (ctx.is('multipart')) {
|
|
419
|
+
try {
|
|
420
|
+
const { fields, files } = await parseMultipartForm(ctx.req);
|
|
421
|
+
text = firstFormFieldValue(fields?.text);
|
|
422
|
+
attachments = normalizePromptAttachments(
|
|
423
|
+
files?.[AGENT_ATTACHMENT_FIELD]
|
|
424
|
+
);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
ctx.status = 400;
|
|
427
|
+
ctx.body = {
|
|
428
|
+
error: error?.message || 'Failed to parse prompt attachments'
|
|
429
|
+
};
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
const body = ctx.request.body || {};
|
|
434
|
+
text = typeof body.text === 'string' ? body.text : '';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!text.trim() && attachments.length === 0) {
|
|
438
|
+
ctx.status = 400;
|
|
439
|
+
ctx.body = { error: 'text or attachments are required' };
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
await acpManager.sendPrompt(tabId, text, attachments);
|
|
444
|
+
ctx.status = 202;
|
|
445
|
+
ctx.body = { ok: true };
|
|
446
|
+
} catch (error) {
|
|
447
|
+
ctx.status = 500;
|
|
448
|
+
ctx.body = { error: error?.message || 'Failed to send prompt' };
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
router.post('/api/agents/tabs/:tabId/cancel', async (ctx) => {
|
|
453
|
+
const { tabId } = ctx.params;
|
|
454
|
+
try {
|
|
455
|
+
await acpManager.cancel(tabId);
|
|
456
|
+
ctx.status = 202;
|
|
457
|
+
ctx.body = { ok: true };
|
|
458
|
+
} catch (error) {
|
|
459
|
+
ctx.status = 500;
|
|
460
|
+
ctx.body = { error: error?.message || 'Failed to cancel prompt' };
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
router.post(
|
|
465
|
+
'/api/agents/tabs/:tabId/permissions/:permissionId',
|
|
466
|
+
async (ctx) => {
|
|
467
|
+
const { tabId, permissionId } = ctx.params;
|
|
468
|
+
const { optionId } = ctx.request.body || {};
|
|
469
|
+
try {
|
|
470
|
+
await acpManager.resolvePermission(
|
|
471
|
+
tabId,
|
|
472
|
+
permissionId,
|
|
473
|
+
typeof optionId === 'string' ? optionId : ''
|
|
474
|
+
);
|
|
475
|
+
ctx.status = 200;
|
|
476
|
+
ctx.body = { ok: true };
|
|
477
|
+
} catch (error) {
|
|
478
|
+
ctx.status = 500;
|
|
479
|
+
ctx.body = {
|
|
480
|
+
error: error?.message || 'Failed to resolve permission'
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
router.post('/api/agents/tabs/:tabId/mode', async (ctx) => {
|
|
487
|
+
const { tabId } = ctx.params;
|
|
488
|
+
const { modeId } = ctx.request.body || {};
|
|
489
|
+
if (!modeId || typeof modeId !== 'string') {
|
|
490
|
+
ctx.status = 400;
|
|
491
|
+
ctx.body = { error: 'modeId is required' };
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
ctx.body = await acpManager.setMode(tabId, modeId);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
ctx.status = 500;
|
|
498
|
+
ctx.body = { error: error?.message || 'Failed to switch mode' };
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
router.post('/api/agents/tabs/:tabId/config', async (ctx) => {
|
|
503
|
+
const { tabId } = ctx.params;
|
|
504
|
+
const { configId, valueId } = ctx.request.body || {};
|
|
505
|
+
if (!configId || typeof configId !== 'string') {
|
|
506
|
+
ctx.status = 400;
|
|
507
|
+
ctx.body = { error: 'configId is required' };
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (!valueId || typeof valueId !== 'string') {
|
|
511
|
+
ctx.status = 400;
|
|
512
|
+
ctx.body = { error: 'valueId is required' };
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
ctx.body = await acpManager.setConfigOption(tabId, configId, valueId);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
ctx.status = 500;
|
|
519
|
+
ctx.body = { error: error?.message || 'Failed to update agent setting' };
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
router.delete('/api/agents/tabs/:tabId', async (ctx) => {
|
|
524
|
+
const { tabId } = ctx.params;
|
|
525
|
+
await acpManager.closeTab(tabId);
|
|
526
|
+
ctx.status = 204;
|
|
527
|
+
});
|
|
528
|
+
|
|
272
529
|
// Middleware
|
|
273
530
|
app.use(router.routes());
|
|
274
531
|
app.use(router.allowedMethods());
|
|
@@ -288,7 +545,21 @@ httpServer.on('upgrade', (request, socket, head) => {
|
|
|
288
545
|
const url = new URL(request.url, `http://${request.headers.host}`);
|
|
289
546
|
const pathname = url.pathname;
|
|
290
547
|
|
|
291
|
-
if (pathname.startsWith('/ws/')) {
|
|
548
|
+
if (pathname.startsWith('/ws/agents/')) {
|
|
549
|
+
const match = pathname.match(/^\/ws\/agents\/([a-zA-Z0-9-]+)$/);
|
|
550
|
+
if (!match) {
|
|
551
|
+
socket.destroy();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const tabId = match[1];
|
|
556
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
557
|
+
wss.emit('connection', ws, {
|
|
558
|
+
kind: 'agent',
|
|
559
|
+
tabId
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
} else if (pathname.startsWith('/ws/')) {
|
|
292
563
|
const match = pathname.match(/^\/ws\/([a-zA-Z0-9-]+)$/);
|
|
293
564
|
if (!match) {
|
|
294
565
|
socket.destroy();
|
|
@@ -305,20 +576,36 @@ httpServer.on('upgrade', (request, socket, head) => {
|
|
|
305
576
|
return;
|
|
306
577
|
}
|
|
307
578
|
const ua = request.headers['user-agent'] || 'Unknown';
|
|
308
|
-
wss.emit('connection', ws,
|
|
579
|
+
wss.emit('connection', ws, {
|
|
580
|
+
kind: 'terminal',
|
|
581
|
+
session,
|
|
582
|
+
ua
|
|
583
|
+
});
|
|
309
584
|
});
|
|
310
585
|
} else {
|
|
311
586
|
socket.destroy();
|
|
312
587
|
}
|
|
313
588
|
});
|
|
314
589
|
|
|
315
|
-
wss.on('connection', (socket,
|
|
590
|
+
wss.on('connection', (socket, target) => {
|
|
316
591
|
socket.isAlive = true;
|
|
317
592
|
socket.on('pong', () => {
|
|
318
593
|
socket.isAlive = true;
|
|
319
594
|
});
|
|
320
|
-
|
|
321
|
-
|
|
595
|
+
if (target.kind === 'terminal') {
|
|
596
|
+
debugLog(
|
|
597
|
+
`[Server] WebSocket connected to session `
|
|
598
|
+
+ `${target.session.id} [${target.ua}]`
|
|
599
|
+
);
|
|
600
|
+
target.session.attach(socket);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (target.kind === 'agent') {
|
|
604
|
+
debugLog(
|
|
605
|
+
`[Server] WebSocket connected to agent tab ${target.tabId}`
|
|
606
|
+
);
|
|
607
|
+
acpManager.attachSocket(target.tabId, socket);
|
|
608
|
+
}
|
|
322
609
|
});
|
|
323
610
|
|
|
324
611
|
const heartbeatInterval = setInterval(() => {
|
|
@@ -383,6 +670,7 @@ function shutdown(signal) {
|
|
|
383
670
|
}
|
|
384
671
|
wss.close();
|
|
385
672
|
terminalManager.dispose();
|
|
673
|
+
void acpManager.dispose();
|
|
386
674
|
|
|
387
675
|
const forceExitTimer = setTimeout(() => {
|
|
388
676
|
console.warn('Forced shutdown after timeout.');
|