tabminal 3.0.7 → 3.0.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabminal",
3
- "version": "3.0.7",
3
+ "version": "3.0.8",
4
4
  "description": "Tab(ter)minal, a Cloud-Native terminal and ACP agent workspace for desktop, tablet, and phone.",
5
5
  "type": "module",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -108,7 +108,7 @@ const CLOSE_ICON_SVG = '<svg viewBox="0 0 24 24" width="16" height="16" stroke="
108
108
  const AGENT_ICON_SVG = '<svg viewBox="0 0 24 24" width="17" height="17" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7" width="10" height="10" rx="2"></rect><path d="M9 7V5"></path><path d="M15 7V5"></path><path d="M12 17v2"></path><path d="M5 12H3"></path><path d="M21 12h-2"></path><path d="M9 11h.01"></path><path d="M15 11h.01"></path><path d="M9.5 14c.7.67 1.53 1 2.5 1s1.8-.33 2.5-1"></path></svg>';
109
109
  const TERMINAL_TAB_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m8 10 3 2-3 2"></path><path d="M13 15h4"></path></svg>';
110
110
  const MANAGED_TERMINAL_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="M7 12h.01"></path><path d="M12 9v6"></path><path d="M9 12h6"></path><path d="M18 8v2"></path><path d="M19 9h-2"></path></svg>';
111
- const BELL_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M10 5a2 2 0 1 1 4 0"></path><path d="M5 16a7 7 0 1 0 14 0"></path><path d="M4 16h16"></path><path d="M10 20a2 2 0 0 0 4 0"></path></svg>';
111
+ const BELL_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="2.1" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4.5a4.5 4.5 0 0 0-4.5 4.5v2.4c0 1.2-.41 2.37-1.17 3.3L5 16.5h14l-1.33-1.8a5.66 5.66 0 0 1-1.17-3.3V9A4.5 4.5 0 0 0 12 4.5"></path><path d="M10.25 19a1.75 1.75 0 0 0 3.5 0"></path></svg>';
112
112
  const SPINNER_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"><path d="M12 3a9 9 0 1 0 9 9"></path></svg>';
113
113
  const ATTACH_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.9" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05 12.25 20.24a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 1 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.82-2.82l8.49-8.49"></path></svg>';
114
114
  const CHEVRON_DOWN_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg>';
@@ -4464,6 +4464,10 @@ class Session {
4464
4464
  editorFlex: '2 1 0%'
4465
4465
  };
4466
4466
  this.previewRelayoutScheduled = false;
4467
+ this.lastTerminalControlClaimAt = 0;
4468
+ this.boundTerminalClaimRoot = null;
4469
+ this.boundTerminalClaimTextarea = null;
4470
+ this.boundTerminalClaimHandler = null;
4467
4471
  this.wrapperElement = null;
4468
4472
  this._createTerminals();
4469
4473
 
@@ -4529,6 +4533,8 @@ class Session {
4529
4533
  const wasActive = state.activeSessionKey === this.key;
4530
4534
  const previewWrapper = this.wrapperElement;
4531
4535
 
4536
+ this.unbindTerminalControlClaim();
4537
+
4532
4538
  try {
4533
4539
  this.previewTerm?.dispose();
4534
4540
  } catch (e) {
@@ -4556,6 +4562,7 @@ class Session {
4556
4562
  if (wasActive && terminalEl) {
4557
4563
  terminalEl.innerHTML = '';
4558
4564
  this.mainTerm.open(terminalEl);
4565
+ this.bindTerminalControlClaim();
4559
4566
  if (this.fitMainTerminalIfVisible()) {
4560
4567
  this.mainTerm.focus();
4561
4568
  }
@@ -5067,6 +5074,79 @@ class Session {
5067
5074
  }
5068
5075
  }
5069
5076
 
5077
+ claimTerminalControl(force = false) {
5078
+ if (state.activeSessionKey !== this.key) {
5079
+ return;
5080
+ }
5081
+ if (this.socket?.readyState !== WebSocket.OPEN) {
5082
+ return;
5083
+ }
5084
+
5085
+ const now = Date.now();
5086
+ if (!force && now - this.lastTerminalControlClaimAt < 250) {
5087
+ return;
5088
+ }
5089
+
5090
+ this.lastTerminalControlClaimAt = now;
5091
+ this.send({ type: 'claim_terminal_control' });
5092
+ }
5093
+
5094
+ bindTerminalControlClaim() {
5095
+ this.unbindTerminalControlClaim();
5096
+
5097
+ const root = this.mainTerm?.element;
5098
+ if (!root) {
5099
+ return;
5100
+ }
5101
+
5102
+ const textarea = this.mainTerm.textarea
5103
+ || root.querySelector('textarea');
5104
+ const handler = () => this.claimTerminalControl();
5105
+
5106
+ root.addEventListener('mousedown', handler, true);
5107
+ root.addEventListener('touchstart', handler, true);
5108
+ if (textarea) {
5109
+ textarea.addEventListener('keydown', handler, true);
5110
+ textarea.addEventListener('paste', handler, true);
5111
+ }
5112
+
5113
+ this.boundTerminalClaimRoot = root;
5114
+ this.boundTerminalClaimTextarea = textarea;
5115
+ this.boundTerminalClaimHandler = handler;
5116
+ }
5117
+
5118
+ unbindTerminalControlClaim() {
5119
+ const handler = this.boundTerminalClaimHandler;
5120
+ if (!handler) {
5121
+ return;
5122
+ }
5123
+
5124
+ this.boundTerminalClaimRoot?.removeEventListener(
5125
+ 'mousedown',
5126
+ handler,
5127
+ true
5128
+ );
5129
+ this.boundTerminalClaimRoot?.removeEventListener(
5130
+ 'touchstart',
5131
+ handler,
5132
+ true
5133
+ );
5134
+ this.boundTerminalClaimTextarea?.removeEventListener(
5135
+ 'keydown',
5136
+ handler,
5137
+ true
5138
+ );
5139
+ this.boundTerminalClaimTextarea?.removeEventListener(
5140
+ 'paste',
5141
+ handler,
5142
+ true
5143
+ );
5144
+
5145
+ this.boundTerminalClaimRoot = null;
5146
+ this.boundTerminalClaimTextarea = null;
5147
+ this.boundTerminalClaimHandler = null;
5148
+ }
5149
+
5070
5150
  reportResize() {
5071
5151
  if (!this.isMainTerminalVisible()) {
5072
5152
  return;
@@ -5084,6 +5164,7 @@ class Session {
5084
5164
  this.shouldReconnect = false;
5085
5165
  clearTimeout(this.retryTimer);
5086
5166
  this.socket?.close();
5167
+ this.unbindTerminalControlClaim();
5087
5168
 
5088
5169
  try {
5089
5170
  if (this.previewTerm) this.previewTerm.dispose();
@@ -10773,6 +10854,11 @@ async function switchToSession(sessionKey, options = {}) {
10773
10854
  return;
10774
10855
  }
10775
10856
 
10857
+ const previousSession = state.activeSessionKey
10858
+ ? state.sessions.get(state.activeSessionKey)
10859
+ : null;
10860
+ previousSession?.unbindTerminalControlClaim();
10861
+
10776
10862
  state.activeSessionKey = sessionKey;
10777
10863
  renderTabs();
10778
10864
  if (scrollTabIntoView) {
@@ -10789,6 +10875,7 @@ async function switchToSession(sessionKey, options = {}) {
10789
10875
 
10790
10876
  // Mount new session
10791
10877
  session.mainTerm.open(terminalEl);
10878
+ session.bindTerminalControlClaim();
10792
10879
  session.fitMainTerminalIfVisible();
10793
10880
  if (session.isMainTerminalVisible()) {
10794
10881
  session.mainTerm.focus();
package/public/styles.css CHANGED
@@ -1561,7 +1561,7 @@ kbd {
1561
1561
  .tab-status-icon.is-attention,
1562
1562
  .agent-editor-tab-icon.is-attention,
1563
1563
  .toggle-agent-btn.is-attention {
1564
- color: var(--warning-color);
1564
+ color: #cb4b16;
1565
1565
  }
1566
1566
 
1567
1567
  .tab-status-icon.is-running,
@@ -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()) ||