@leverageaiapps/theseus-server 1.0.0
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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/capture.d.ts +3 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +134 -0
- package/dist/capture.js.map +1 -0
- package/dist/cloudflare-tunnel.d.ts +9 -0
- package/dist/cloudflare-tunnel.d.ts.map +1 -0
- package/dist/cloudflare-tunnel.js +218 -0
- package/dist/cloudflare-tunnel.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +84 -0
- package/dist/config.js.map +1 -0
- package/dist/context-extractor.d.ts +17 -0
- package/dist/context-extractor.d.ts.map +1 -0
- package/dist/context-extractor.js +118 -0
- package/dist/context-extractor.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/pty.d.ts +20 -0
- package/dist/pty.d.ts.map +1 -0
- package/dist/pty.js +148 -0
- package/dist/pty.js.map +1 -0
- package/dist/relay.d.ts +5 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +131 -0
- package/dist/relay.js.map +1 -0
- package/dist/session.d.ts +5 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +257 -0
- package/dist/session.js.map +1 -0
- package/dist/voice-recognition-modelscope.d.ts +50 -0
- package/dist/voice-recognition-modelscope.d.ts.map +1 -0
- package/dist/voice-recognition-modelscope.js +171 -0
- package/dist/voice-recognition-modelscope.js.map +1 -0
- package/dist/web-server.d.ts +6 -0
- package/dist/web-server.d.ts.map +1 -0
- package/dist/web-server.js +1971 -0
- package/dist/web-server.js.map +1 -0
- package/package.json +66 -0
- package/public/index.html +639 -0
- package/public/js/terminal-asr.js +508 -0
- package/public/js/terminal.js +514 -0
- package/public/js/voice-input.js +422 -0
- package/scripts/postinstall.js +66 -0
- package/scripts/verify-install.js +124 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
window.term = new Terminal({
|
|
2
|
+
cursorBlink: true,
|
|
3
|
+
fontSize: 13,
|
|
4
|
+
theme: { background: '#0a0a0a', foreground: '#ededed' },
|
|
5
|
+
scrollback: 10000,
|
|
6
|
+
allowTransparency: false,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const fitAddon = new FitAddon.FitAddon();
|
|
10
|
+
term.loadAddon(fitAddon);
|
|
11
|
+
term.open(document.getElementById('terminal-container'));
|
|
12
|
+
fitAddon.fit();
|
|
13
|
+
|
|
14
|
+
const statusDot = document.getElementById('status-dot');
|
|
15
|
+
const input = document.getElementById('input');
|
|
16
|
+
const scrollBtn = document.getElementById('scroll-to-bottom');
|
|
17
|
+
const specialKeysBtn = document.getElementById('special-keys-btn');
|
|
18
|
+
const specialKeysPopup = document.getElementById('special-keys-popup');
|
|
19
|
+
|
|
20
|
+
let ws = null;
|
|
21
|
+
let pendingInputs = [];
|
|
22
|
+
let reconnectAttempts = 0;
|
|
23
|
+
const MAX_RECONNECT_ATTEMPTS = 3;
|
|
24
|
+
let reconnectTimeoutId = null;
|
|
25
|
+
let isReconnecting = false;
|
|
26
|
+
let isUserScrolling = false;
|
|
27
|
+
|
|
28
|
+
// Touch scrolling state
|
|
29
|
+
const terminalContainer = document.getElementById('terminal-container');
|
|
30
|
+
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
31
|
+
|
|
32
|
+
// Initialize touch scrolling for mobile devices
|
|
33
|
+
if (isTouchDevice) {
|
|
34
|
+
initTouchScrolling(terminalContainer, () => { isUserScrolling = true; });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Touch scrolling implementation for smooth mobile scrolling
|
|
38
|
+
function initTouchScrolling(container, onScrollStart) {
|
|
39
|
+
const touchState = {
|
|
40
|
+
startY: 0, lastY: 0, lastTime: 0,
|
|
41
|
+
velocity: 0, identifier: null,
|
|
42
|
+
touching: false, velocityHistory: [],
|
|
43
|
+
accumulator: 0, inertiaId: null
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Create touch overlay
|
|
47
|
+
const overlay = createTouchOverlay(container);
|
|
48
|
+
|
|
49
|
+
// Attach event handlers
|
|
50
|
+
overlay.addEventListener('touchstart', handleTouchStart, { passive: false });
|
|
51
|
+
overlay.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
52
|
+
overlay.addEventListener('touchend', handleTouchEnd, { passive: false });
|
|
53
|
+
overlay.addEventListener('touchcancel', handleTouchCancel, { passive: false });
|
|
54
|
+
|
|
55
|
+
// Prevent conflicts with input area
|
|
56
|
+
const inputArea = document.getElementById('input-area');
|
|
57
|
+
if (inputArea) {
|
|
58
|
+
inputArea.addEventListener('touchstart', e => e.stopPropagation(), { passive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createTouchOverlay(parent) {
|
|
62
|
+
const div = document.createElement('div');
|
|
63
|
+
Object.assign(div.style, {
|
|
64
|
+
position: 'absolute', top: '0', left: '0', right: '0', bottom: '0',
|
|
65
|
+
zIndex: '1', touchAction: 'none', webkitTouchCallout: 'none',
|
|
66
|
+
webkitUserSelect: 'none', userSelect: 'none', pointerEvents: 'auto'
|
|
67
|
+
});
|
|
68
|
+
parent.appendChild(div);
|
|
69
|
+
return div;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function performScroll(deltaY) {
|
|
73
|
+
const viewport = container.querySelector('.xterm-viewport');
|
|
74
|
+
if (!viewport) return;
|
|
75
|
+
viewport.scrollTop += deltaY;
|
|
76
|
+
viewport.dispatchEvent(new WheelEvent('wheel', {
|
|
77
|
+
deltaY, deltaMode: 0, bubbles: true, cancelable: true
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleTouchStart(e) {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
cancelInertia();
|
|
84
|
+
touchState.accumulator = 0;
|
|
85
|
+
|
|
86
|
+
if (e.touches.length > 0) {
|
|
87
|
+
const touch = e.touches[0];
|
|
88
|
+
Object.assign(touchState, {
|
|
89
|
+
identifier: touch.identifier,
|
|
90
|
+
startY: touch.clientY,
|
|
91
|
+
lastY: touch.clientY,
|
|
92
|
+
lastTime: performance.now(),
|
|
93
|
+
velocity: 0,
|
|
94
|
+
velocityHistory: [],
|
|
95
|
+
touching: true
|
|
96
|
+
});
|
|
97
|
+
onScrollStart();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function handleTouchMove(e) {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
if (!touchState.touching || e.touches.length === 0) return;
|
|
104
|
+
|
|
105
|
+
const touch = findTrackedTouch(e.touches) || e.touches[0];
|
|
106
|
+
const currentY = touch.clientY;
|
|
107
|
+
const deltaY = touchState.lastY - currentY;
|
|
108
|
+
const currentTime = performance.now();
|
|
109
|
+
const timeDelta = Math.max(1, currentTime - touchState.lastTime);
|
|
110
|
+
|
|
111
|
+
// Update velocity
|
|
112
|
+
updateVelocity(deltaY / timeDelta);
|
|
113
|
+
|
|
114
|
+
touchState.lastY = currentY;
|
|
115
|
+
touchState.lastTime = currentTime;
|
|
116
|
+
touchState.accumulator += deltaY;
|
|
117
|
+
|
|
118
|
+
// Apply scroll when threshold reached
|
|
119
|
+
if (Math.abs(touchState.accumulator) >= 0.5) {
|
|
120
|
+
performScroll(touchState.accumulator * 1.8);
|
|
121
|
+
touchState.accumulator = touchState.accumulator % 0.5;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleTouchEnd(e) {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
if (!isTouchEnded(e.touches)) return;
|
|
128
|
+
|
|
129
|
+
touchState.touching = false;
|
|
130
|
+
touchState.identifier = null;
|
|
131
|
+
|
|
132
|
+
// Apply remaining scroll
|
|
133
|
+
if (Math.abs(touchState.accumulator) > 0) {
|
|
134
|
+
performScroll(touchState.accumulator * 1.8);
|
|
135
|
+
touchState.accumulator = 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Start inertia if needed
|
|
139
|
+
if (Math.abs(touchState.velocity) > 0.01) {
|
|
140
|
+
startInertia();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleTouchCancel(e) {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
resetTouchState();
|
|
147
|
+
cancelInertia();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function findTrackedTouch(touches) {
|
|
151
|
+
for (let i = 0; i < touches.length; i++) {
|
|
152
|
+
if (touches[i].identifier === touchState.identifier) {
|
|
153
|
+
return touches[i];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isTouchEnded(touches) {
|
|
160
|
+
return !findTrackedTouch(touches);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function updateVelocity(instant) {
|
|
164
|
+
touchState.velocityHistory.push(instant);
|
|
165
|
+
if (touchState.velocityHistory.length > 5) {
|
|
166
|
+
touchState.velocityHistory.shift();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Calculate weighted average
|
|
170
|
+
let weightedSum = 0, totalWeight = 0;
|
|
171
|
+
touchState.velocityHistory.forEach((v, i) => {
|
|
172
|
+
const weight = i + 1;
|
|
173
|
+
weightedSum += v * weight;
|
|
174
|
+
totalWeight += weight;
|
|
175
|
+
});
|
|
176
|
+
touchState.velocity = totalWeight ? weightedSum / totalWeight : 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function startInertia() {
|
|
180
|
+
const friction = 0.95;
|
|
181
|
+
const minVelocity = 0.01;
|
|
182
|
+
|
|
183
|
+
function animate() {
|
|
184
|
+
if (Math.abs(touchState.velocity) < minVelocity || touchState.touching) {
|
|
185
|
+
touchState.inertiaId = null;
|
|
186
|
+
touchState.velocity = 0;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
performScroll(touchState.velocity * 25);
|
|
191
|
+
touchState.velocity *= friction;
|
|
192
|
+
touchState.inertiaId = requestAnimationFrame(animate);
|
|
193
|
+
}
|
|
194
|
+
animate();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function cancelInertia() {
|
|
198
|
+
if (touchState.inertiaId) {
|
|
199
|
+
cancelAnimationFrame(touchState.inertiaId);
|
|
200
|
+
touchState.inertiaId = null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resetTouchState() {
|
|
205
|
+
Object.assign(touchState, {
|
|
206
|
+
touching: false, identifier: null,
|
|
207
|
+
velocity: 0, velocityHistory: [],
|
|
208
|
+
accumulator: 0
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function setInputEnabled(enabled) {
|
|
214
|
+
input.disabled = !enabled;
|
|
215
|
+
input.style.opacity = enabled ? '1' : '0.5';
|
|
216
|
+
input.style.cursor = enabled ? 'text' : 'not-allowed';
|
|
217
|
+
if (!enabled) {
|
|
218
|
+
input.placeholder = 'Reconnecting...';
|
|
219
|
+
} else {
|
|
220
|
+
input.placeholder = 'Type command or use voice input...';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function updateStatus(state) {
|
|
225
|
+
statusDot.className = '';
|
|
226
|
+
if (state === 'disconnected') {
|
|
227
|
+
statusDot.classList.add('disconnected');
|
|
228
|
+
isReconnecting = true;
|
|
229
|
+
} else if (state === 'connecting') {
|
|
230
|
+
statusDot.classList.add('connecting');
|
|
231
|
+
isReconnecting = true;
|
|
232
|
+
} else if (state === 'connected') {
|
|
233
|
+
isReconnecting = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function connect() {
|
|
238
|
+
// WebSocket will automatically include cookies with the request
|
|
239
|
+
const wsUrl = location.protocol.replace('http', 'ws') + '//' + location.host + '/ws';
|
|
240
|
+
|
|
241
|
+
// For debugging - log if auth cookie exists
|
|
242
|
+
const hasCookie = document.cookie.includes('auth=');
|
|
243
|
+
console.log('Connecting WebSocket, auth cookie present:', hasCookie);
|
|
244
|
+
|
|
245
|
+
ws = new WebSocket(wsUrl);
|
|
246
|
+
|
|
247
|
+
// Expose WebSocket globally for voice input
|
|
248
|
+
window.terminalWs = ws;
|
|
249
|
+
|
|
250
|
+
ws.onopen = () => {
|
|
251
|
+
console.log('WebSocket connected');
|
|
252
|
+
updateStatus('connected');
|
|
253
|
+
reconnectAttempts = 0;
|
|
254
|
+
fitAddon.fit();
|
|
255
|
+
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
ws.onclose = () => {
|
|
259
|
+
console.log('WebSocket closed');
|
|
260
|
+
updateStatus('disconnected');
|
|
261
|
+
ws = null;
|
|
262
|
+
window.terminalWs = null;
|
|
263
|
+
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
264
|
+
reconnectAttempts++;
|
|
265
|
+
reconnectTimeoutId = setTimeout(() => {
|
|
266
|
+
connect();
|
|
267
|
+
}, 500);
|
|
268
|
+
} else {
|
|
269
|
+
setInputEnabled(false);
|
|
270
|
+
input.placeholder = 'Connection failed. Refresh page.';
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
ws.onerror = (err) => {
|
|
275
|
+
console.log('WebSocket error');
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
ws.onmessage = (e) => {
|
|
279
|
+
const msg = JSON.parse(e.data);
|
|
280
|
+
if (msg.type === 'output') {
|
|
281
|
+
term.write(msg.data);
|
|
282
|
+
checkScrollPosition();
|
|
283
|
+
}
|
|
284
|
+
if (msg.type === 'history') {
|
|
285
|
+
term.clear();
|
|
286
|
+
msg.data.forEach(d => term.write(d));
|
|
287
|
+
setInputEnabled(true);
|
|
288
|
+
term.scrollToBottom();
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
const viewport = document.querySelector('.xterm-viewport');
|
|
291
|
+
if (viewport) {
|
|
292
|
+
viewport.scrollTop = viewport.scrollHeight;
|
|
293
|
+
}
|
|
294
|
+
isUserScrolling = false;
|
|
295
|
+
}, 100);
|
|
296
|
+
}
|
|
297
|
+
// Handle ASR messages
|
|
298
|
+
if (msg.type === 'asr_response') {
|
|
299
|
+
if (window.handleASRResponse) {
|
|
300
|
+
window.handleASRResponse(msg.data);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Handle Claude response messages
|
|
304
|
+
if (msg.type === 'claude_response') {
|
|
305
|
+
if (window.voiceInput) {
|
|
306
|
+
if (msg.data.error) {
|
|
307
|
+
console.error('[Claude] Server error:', msg.data.error);
|
|
308
|
+
// Fallback to original transcript if available
|
|
309
|
+
if (msg.data.fallback) {
|
|
310
|
+
// Restore existing content and append fallback
|
|
311
|
+
const existingContent = window.voiceInput.existingInputContent || '';
|
|
312
|
+
const needSpace = existingContent && !existingContent.endsWith(' ');
|
|
313
|
+
input.value = existingContent + (needSpace ? ' ' : '') + msg.data.fallback;
|
|
314
|
+
input.style.height = 'auto';
|
|
315
|
+
input.style.height = input.scrollHeight + 'px';
|
|
316
|
+
}
|
|
317
|
+
} else if (msg.data.text) {
|
|
318
|
+
// First chunk: restore existing content
|
|
319
|
+
if (input.value === '' && window.voiceInput.existingInputContent) {
|
|
320
|
+
const existingContent = window.voiceInput.existingInputContent;
|
|
321
|
+
const needSpace = existingContent && !existingContent.endsWith(' ');
|
|
322
|
+
input.value = existingContent + (needSpace ? ' ' : '');
|
|
323
|
+
// Clear the flag so we don't add it again
|
|
324
|
+
window.voiceInput.existingInputContent = '';
|
|
325
|
+
}
|
|
326
|
+
// Stream text to input
|
|
327
|
+
input.value += msg.data.text;
|
|
328
|
+
input.style.height = 'auto';
|
|
329
|
+
input.style.height = input.scrollHeight + 'px';
|
|
330
|
+
} else if (msg.data.done) {
|
|
331
|
+
console.log('[Claude] Processing complete');
|
|
332
|
+
// Clear the existing content flag
|
|
333
|
+
if (window.voiceInput) {
|
|
334
|
+
window.voiceInput.existingInputContent = '';
|
|
335
|
+
|
|
336
|
+
// Check if auto-submit is pending (triggered by "go go go" command)
|
|
337
|
+
if (window.voiceInput.autoSubmitPending) {
|
|
338
|
+
console.log('[Claude] Auto-submit triggered by "go go go" command');
|
|
339
|
+
window.voiceInput.autoSubmitPending = false;
|
|
340
|
+
|
|
341
|
+
// Auto-submit the command after a short delay to ensure input is updated
|
|
342
|
+
setTimeout(() => {
|
|
343
|
+
const cmd = input.value.trim();
|
|
344
|
+
if (cmd && ws && ws.readyState === 1) {
|
|
345
|
+
input.value = '';
|
|
346
|
+
input.style.height = 'auto';
|
|
347
|
+
// Send text first, then Enter key
|
|
348
|
+
ws.send(JSON.stringify({ type: 'input', data: cmd }));
|
|
349
|
+
setTimeout(() => {
|
|
350
|
+
ws.send(JSON.stringify({ type: 'input', data: String.fromCharCode(13) }));
|
|
351
|
+
}, 50);
|
|
352
|
+
}
|
|
353
|
+
}, 100);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Input handling - must send text and Enter key separately for Claude Code to work
|
|
363
|
+
input.addEventListener('keydown', (e) => {
|
|
364
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
365
|
+
e.preventDefault();
|
|
366
|
+
if (ws && ws.readyState === 1) {
|
|
367
|
+
const cmd = input.value;
|
|
368
|
+
input.value = '';
|
|
369
|
+
input.style.height = 'auto';
|
|
370
|
+
if (cmd) {
|
|
371
|
+
// Send text first, then Enter key separately after delay
|
|
372
|
+
ws.send(JSON.stringify({ type: 'input', data: cmd }));
|
|
373
|
+
setTimeout(() => {
|
|
374
|
+
ws.send(JSON.stringify({ type: 'input', data: String.fromCharCode(13) }));
|
|
375
|
+
}, 50);
|
|
376
|
+
} else {
|
|
377
|
+
// Just send Enter if empty
|
|
378
|
+
ws.send(JSON.stringify({ type: 'input', data: String.fromCharCode(13) }));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Auto-resize textarea
|
|
385
|
+
input.addEventListener('input', () => {
|
|
386
|
+
input.style.height = 'auto';
|
|
387
|
+
input.style.height = input.scrollHeight + 'px';
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Special keys handling
|
|
391
|
+
specialKeysBtn.addEventListener('click', (e) => {
|
|
392
|
+
e.stopPropagation();
|
|
393
|
+
specialKeysPopup.classList.toggle('show');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
document.addEventListener('click', (e) => {
|
|
397
|
+
if (!specialKeysPopup.contains(e.target) && e.target !== specialKeysBtn) {
|
|
398
|
+
specialKeysPopup.classList.remove('show');
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
document.querySelectorAll('.special-key').forEach(btn => {
|
|
403
|
+
btn.addEventListener('click', () => {
|
|
404
|
+
const key = btn.getAttribute('data-key');
|
|
405
|
+
handleSpecialKey(key);
|
|
406
|
+
specialKeysPopup.classList.remove('show');
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
function handleSpecialKey(key) {
|
|
411
|
+
if (!ws || ws.readyState !== 1) return;
|
|
412
|
+
|
|
413
|
+
const keyMap = {
|
|
414
|
+
'Escape': '\x1b',
|
|
415
|
+
'Tab': '\t',
|
|
416
|
+
'Up': '\x1b[A',
|
|
417
|
+
'Down': '\x1b[B',
|
|
418
|
+
'Left': '\x1b[D',
|
|
419
|
+
'Right': '\x1b[C',
|
|
420
|
+
'Ctrl+C': '\x03',
|
|
421
|
+
'Ctrl+D': '\x04',
|
|
422
|
+
'Ctrl+Z': '\x1a',
|
|
423
|
+
'Ctrl+L': '\x0c',
|
|
424
|
+
'Home': '\x1b[H',
|
|
425
|
+
'End': '\x1b[F',
|
|
426
|
+
'PageUp': '\x1b[5~',
|
|
427
|
+
'PageDown': '\x1b[6~',
|
|
428
|
+
'F1': '\x1bOP',
|
|
429
|
+
'F2': '\x1bOQ',
|
|
430
|
+
'F3': '\x1bOR',
|
|
431
|
+
'F4': '\x1bOS'
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
if (keyMap[key]) {
|
|
435
|
+
ws.send(JSON.stringify({ type: 'input', data: keyMap[key] }));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Scroll handling
|
|
440
|
+
function checkScrollPosition() {
|
|
441
|
+
const viewport = document.querySelector('.xterm-viewport');
|
|
442
|
+
if (!viewport) return;
|
|
443
|
+
|
|
444
|
+
const isNearBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 100;
|
|
445
|
+
|
|
446
|
+
if (isNearBottom) {
|
|
447
|
+
scrollBtn.classList.remove('visible');
|
|
448
|
+
isUserScrolling = false;
|
|
449
|
+
} else {
|
|
450
|
+
scrollBtn.classList.add('visible');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
scrollBtn.addEventListener('click', () => {
|
|
455
|
+
const viewport = document.querySelector('.xterm-viewport');
|
|
456
|
+
if (viewport) {
|
|
457
|
+
viewport.scrollTop = viewport.scrollHeight;
|
|
458
|
+
}
|
|
459
|
+
term.scrollToBottom();
|
|
460
|
+
isUserScrolling = false;
|
|
461
|
+
scrollBtn.classList.remove('visible');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Monitor scroll
|
|
465
|
+
const viewport = document.querySelector('.xterm-viewport');
|
|
466
|
+
if (viewport) {
|
|
467
|
+
viewport.addEventListener('scroll', () => {
|
|
468
|
+
checkScrollPosition();
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Window resize
|
|
473
|
+
let resizeTimeout;
|
|
474
|
+
window.addEventListener('resize', () => {
|
|
475
|
+
clearTimeout(resizeTimeout);
|
|
476
|
+
resizeTimeout = setTimeout(() => {
|
|
477
|
+
fitAddon.fit();
|
|
478
|
+
if (ws && ws.readyState === 1) {
|
|
479
|
+
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
|
480
|
+
}
|
|
481
|
+
}, 100);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Debug console controls
|
|
485
|
+
const contextLengthInput = document.getElementById('context-length');
|
|
486
|
+
|
|
487
|
+
// Update context length when changed
|
|
488
|
+
if (contextLengthInput) {
|
|
489
|
+
contextLengthInput.addEventListener('change', () => {
|
|
490
|
+
const length = parseInt(contextLengthInput.value);
|
|
491
|
+
if (window.terminalASR) {
|
|
492
|
+
window.terminalASR.setMaxContextLength(length);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Get terminal context function (kept for other uses)
|
|
498
|
+
function getTerminalContext() {
|
|
499
|
+
if (window.term) {
|
|
500
|
+
const buffer = window.term.buffer.active;
|
|
501
|
+
const lines = [];
|
|
502
|
+
for (let i = Math.max(0, buffer.length - 50); i < buffer.length; i++) {
|
|
503
|
+
const line = buffer.getLine(i);
|
|
504
|
+
if (line) {
|
|
505
|
+
lines.push(line.translateToString(true));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return lines;
|
|
509
|
+
}
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Initial connection
|
|
514
|
+
connect();
|