tabminal 3.0.7 → 3.0.9
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/package.json +1 -1
- package/public/app.js +662 -44
- package/public/styles.css +1 -1
- package/src/acp-manager.mjs +422 -80
- package/src/server.mjs +87 -0
- package/src/terminal-session.mjs +102 -2
package/src/server.mjs
CHANGED
|
@@ -343,6 +343,52 @@ router.get('/api/agents', async (ctx) => {
|
|
|
343
343
|
ctx.body = await acpManager.listState();
|
|
344
344
|
});
|
|
345
345
|
|
|
346
|
+
router.get('/api/agents/sessions', async (ctx) => {
|
|
347
|
+
const { agentId = '', cwd = '', cursor = '' } = ctx.query || {};
|
|
348
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
349
|
+
ctx.status = 400;
|
|
350
|
+
ctx.body = { error: 'agentId is required' };
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (!cwd || typeof cwd !== 'string') {
|
|
354
|
+
ctx.status = 400;
|
|
355
|
+
ctx.body = { error: 'cwd is required' };
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
let nextCursor = typeof cursor === 'string' ? cursor : '';
|
|
361
|
+
const sessions = [];
|
|
362
|
+
const paginate = !!nextCursor;
|
|
363
|
+
for (let page = 0; page < 5; page += 1) {
|
|
364
|
+
const result = await acpManager.listSessions({
|
|
365
|
+
agentId,
|
|
366
|
+
cwd,
|
|
367
|
+
cursor: nextCursor
|
|
368
|
+
});
|
|
369
|
+
sessions.push(...(Array.isArray(result?.sessions)
|
|
370
|
+
? result.sessions
|
|
371
|
+
: []));
|
|
372
|
+
nextCursor = typeof result?.nextCursor === 'string'
|
|
373
|
+
? result.nextCursor
|
|
374
|
+
: '';
|
|
375
|
+
if (paginate || !nextCursor || sessions.length >= 50) {
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
ctx.body = {
|
|
380
|
+
sessions: sessions.slice(0, 50),
|
|
381
|
+
nextCursor
|
|
382
|
+
};
|
|
383
|
+
} catch (error) {
|
|
384
|
+
const message = error?.message || 'Failed to list agent sessions';
|
|
385
|
+
ctx.status = /does not support session history/i.test(message)
|
|
386
|
+
? 501
|
|
387
|
+
: 500;
|
|
388
|
+
ctx.body = { error: message };
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
346
392
|
router.get('/api/agents/config', async (ctx) => {
|
|
347
393
|
ctx.body = {
|
|
348
394
|
configs: await acpManager.listAgentConfigs()
|
|
@@ -414,6 +460,47 @@ router.post('/api/agents/tabs', async (ctx) => {
|
|
|
414
460
|
}
|
|
415
461
|
});
|
|
416
462
|
|
|
463
|
+
router.post('/api/agents/tabs/resume', async (ctx) => {
|
|
464
|
+
const { agentId, cwd, terminalSessionId, sessionId, title } =
|
|
465
|
+
ctx.request.body || {};
|
|
466
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
467
|
+
ctx.status = 400;
|
|
468
|
+
ctx.body = { error: 'agentId is required' };
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (!cwd || typeof cwd !== 'string') {
|
|
472
|
+
ctx.status = 400;
|
|
473
|
+
ctx.body = { error: 'cwd is required' };
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
477
|
+
ctx.status = 400;
|
|
478
|
+
ctx.body = { error: 'sessionId is required' };
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
ctx.status = 201;
|
|
484
|
+
ctx.body = await acpManager.resumeTab({
|
|
485
|
+
agentId,
|
|
486
|
+
cwd,
|
|
487
|
+
sessionId,
|
|
488
|
+
title: typeof title === 'string' ? title : '',
|
|
489
|
+
terminalSessionId: typeof terminalSessionId === 'string'
|
|
490
|
+
? terminalSessionId
|
|
491
|
+
: ''
|
|
492
|
+
});
|
|
493
|
+
} catch (error) {
|
|
494
|
+
const message = error?.message || 'Failed to resume agent tab';
|
|
495
|
+
ctx.status = /already open/i.test(message)
|
|
496
|
+
? 409
|
|
497
|
+
: /does not support session restore/i.test(message)
|
|
498
|
+
? 501
|
|
499
|
+
: 500;
|
|
500
|
+
ctx.body = { error: message };
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
417
504
|
router.post('/api/agents/tabs/:tabId/prompt', async (ctx) => {
|
|
418
505
|
const { tabId } = ctx.params;
|
|
419
506
|
let text = '';
|
package/src/terminal-session.mjs
CHANGED
|
@@ -22,6 +22,7 @@ const SOS_PM_APC_SEQUENCE_REGEX = /\u001b[\^_][\s\S]*?\u001b\\/g;
|
|
|
22
22
|
const TWO_CHAR_ESCAPE_REGEX = /\u001b[@-Z\\-_]/g;
|
|
23
23
|
const CONTROL_CHAR_REGEX = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
|
|
24
24
|
const TITLE_POLL_INTERVAL_MS = 3000;
|
|
25
|
+
const QUERY_RESPONSE_CSI_REGEX = /^\u001b\[[0-9;?]*[Rn]/;
|
|
25
26
|
|
|
26
27
|
const IGNORED_COMMANDS = [
|
|
27
28
|
'export PROMPT_COMMAND',
|
|
@@ -97,6 +98,75 @@ function estimateSnapshotScrollback(cols, rows, historyLimit) {
|
|
|
97
98
|
return Math.max(safeRows, Math.min(50000, estimatedRows));
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
function consumeTerminalQueryResponse(input, start = 0) {
|
|
102
|
+
if (typeof input !== 'string' || start >= input.length) {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const slice = input.slice(start);
|
|
107
|
+
const csiMatch = QUERY_RESPONSE_CSI_REGEX.exec(slice);
|
|
108
|
+
if (csiMatch) {
|
|
109
|
+
return csiMatch[0].length;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!slice.startsWith('\u001b]')) {
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const oscMatch = /^\u001b](4|10|11);/.exec(slice);
|
|
117
|
+
if (!oscMatch) {
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const belIndex = slice.indexOf('\u0007');
|
|
122
|
+
const stIndex = slice.indexOf('\u001b\\');
|
|
123
|
+
let endIndex = -1;
|
|
124
|
+
|
|
125
|
+
if (belIndex >= 0) {
|
|
126
|
+
endIndex = belIndex + 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (stIndex >= 0) {
|
|
130
|
+
const stEnd = stIndex + 2;
|
|
131
|
+
endIndex = endIndex < 0 ? stEnd : Math.min(endIndex, stEnd);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return endIndex > 0 ? endIndex : 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isTerminalQueryResponseInput(input) {
|
|
138
|
+
if (typeof input !== 'string' || !input.startsWith('\u001b')) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let index = 0;
|
|
143
|
+
while (index < input.length) {
|
|
144
|
+
const consumed = consumeTerminalQueryResponse(input, index);
|
|
145
|
+
if (!consumed) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
index += consumed;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return index > 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function selectFallbackQueryResponder(clients, pendingClients) {
|
|
155
|
+
for (const client of clients) {
|
|
156
|
+
if (client?.readyState === WS_STATE_OPEN) {
|
|
157
|
+
return client;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const client of pendingClients.keys()) {
|
|
162
|
+
if (client?.readyState === WS_STATE_OPEN) {
|
|
163
|
+
return client;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
100
170
|
export class TerminalSession {
|
|
101
171
|
constructor(pty, options = {}) {
|
|
102
172
|
this.pty = pty;
|
|
@@ -129,6 +199,7 @@ export class TerminalSession {
|
|
|
129
199
|
this.history = '';
|
|
130
200
|
this.clients = new Set();
|
|
131
201
|
this.pendingClients = new Map();
|
|
202
|
+
this.queryResponseOwner = null;
|
|
132
203
|
this.closed = false;
|
|
133
204
|
this.exitStatus = null;
|
|
134
205
|
this.exitWaiters = [];
|
|
@@ -386,9 +457,18 @@ export class TerminalSession {
|
|
|
386
457
|
attach(ws) {
|
|
387
458
|
if (!ws) throw new Error('WebSocket instance required');
|
|
388
459
|
this.pendingClients.set(ws, []);
|
|
460
|
+
if (!this.queryResponseOwner) {
|
|
461
|
+
this.queryResponseOwner = ws;
|
|
462
|
+
}
|
|
389
463
|
ws.once('close', () => {
|
|
390
464
|
this.clients.delete(ws);
|
|
391
465
|
this.pendingClients.delete(ws);
|
|
466
|
+
if (this.queryResponseOwner === ws) {
|
|
467
|
+
this.queryResponseOwner = selectFallbackQueryResponder(
|
|
468
|
+
this.clients,
|
|
469
|
+
this.pendingClients
|
|
470
|
+
);
|
|
471
|
+
}
|
|
392
472
|
});
|
|
393
473
|
ws.on('message', (raw) => this._routeIncoming(raw, ws));
|
|
394
474
|
ws.on('error', () => ws.close());
|
|
@@ -473,17 +553,37 @@ export class TerminalSession {
|
|
|
473
553
|
} catch { return; }
|
|
474
554
|
|
|
475
555
|
switch (payload.type) {
|
|
476
|
-
case 'input': this._handleInput(payload.data); break;
|
|
556
|
+
case 'input': this._handleInput(payload.data, ws); break;
|
|
477
557
|
case 'resize': this._handleResize(payload.cols, payload.rows); break;
|
|
558
|
+
case 'claim_terminal_control':
|
|
559
|
+
this._claimTerminalControl(ws);
|
|
560
|
+
break;
|
|
478
561
|
case 'ping': this._send(ws, { type: 'pong' }); break;
|
|
479
562
|
}
|
|
480
563
|
}
|
|
481
564
|
|
|
482
|
-
_handleInput(data) {
|
|
565
|
+
_handleInput(data, ws) {
|
|
483
566
|
if (this.closed || typeof data !== 'string') return;
|
|
567
|
+
if (
|
|
568
|
+
isTerminalQueryResponseInput(data)
|
|
569
|
+
&& this.queryResponseOwner
|
|
570
|
+
&& ws
|
|
571
|
+
&& ws !== this.queryResponseOwner
|
|
572
|
+
) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
484
575
|
this.write(data);
|
|
485
576
|
}
|
|
486
577
|
|
|
578
|
+
_claimTerminalControl(ws) {
|
|
579
|
+
if (!ws) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (this.clients.has(ws) || this.pendingClients.has(ws)) {
|
|
583
|
+
this.queryResponseOwner = ws;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
487
587
|
_isAiEnabled() {
|
|
488
588
|
return Boolean(
|
|
489
589
|
(config.openrouterKey && String(config.openrouterKey).trim()) ||
|