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/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 = '';
@@ -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()) ||