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 +1 -1
- package/public/app.js +88 -1
- package/public/styles.css +1 -1
- package/src/terminal-session.mjs +102 -2
package/package.json
CHANGED
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
|
|
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
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()) ||
|