@leverageaiapps/locus 1.0.8 → 1.1.1

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.
@@ -1,517 +0,0 @@
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
- // Build WebSocket URL relative to current location
240
- // Remove any trailing slash from pathname
241
- const basePath = location.pathname.replace(/\/$/, '');
242
- const wsUrl = location.protocol.replace('http', 'ws') + '//' + location.host + basePath + '/ws';
243
-
244
- // For debugging - log if auth cookie exists
245
- const hasCookie = document.cookie.includes('auth=');
246
- console.log('Connecting WebSocket, auth cookie present:', hasCookie);
247
-
248
- ws = new WebSocket(wsUrl);
249
-
250
- // Expose WebSocket globally for voice input
251
- window.terminalWs = ws;
252
-
253
- ws.onopen = () => {
254
- console.log('WebSocket connected');
255
- updateStatus('connected');
256
- reconnectAttempts = 0;
257
- fitAddon.fit();
258
- ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
259
- };
260
-
261
- ws.onclose = () => {
262
- console.log('WebSocket closed');
263
- updateStatus('disconnected');
264
- ws = null;
265
- window.terminalWs = null;
266
- if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
267
- reconnectAttempts++;
268
- reconnectTimeoutId = setTimeout(() => {
269
- connect();
270
- }, 500);
271
- } else {
272
- setInputEnabled(false);
273
- input.placeholder = 'Connection failed. Refresh page.';
274
- }
275
- };
276
-
277
- ws.onerror = (err) => {
278
- console.log('WebSocket error');
279
- };
280
-
281
- ws.onmessage = (e) => {
282
- const msg = JSON.parse(e.data);
283
- if (msg.type === 'output') {
284
- term.write(msg.data);
285
- checkScrollPosition();
286
- }
287
- if (msg.type === 'history') {
288
- term.clear();
289
- msg.data.forEach(d => term.write(d));
290
- setInputEnabled(true);
291
- term.scrollToBottom();
292
- setTimeout(() => {
293
- const viewport = document.querySelector('.xterm-viewport');
294
- if (viewport) {
295
- viewport.scrollTop = viewport.scrollHeight;
296
- }
297
- isUserScrolling = false;
298
- }, 100);
299
- }
300
- // Handle ASR messages
301
- if (msg.type === 'asr_response') {
302
- if (window.handleASRResponse) {
303
- window.handleASRResponse(msg.data);
304
- }
305
- }
306
- // Handle Claude response messages
307
- if (msg.type === 'claude_response') {
308
- if (window.voiceInput) {
309
- if (msg.data.error) {
310
- console.error('[Claude] Server error:', msg.data.error);
311
- // Fallback to original transcript if available
312
- if (msg.data.fallback) {
313
- // Restore existing content and append fallback
314
- const existingContent = window.voiceInput.existingInputContent || '';
315
- const needSpace = existingContent && !existingContent.endsWith(' ');
316
- input.value = existingContent + (needSpace ? ' ' : '') + msg.data.fallback;
317
- input.style.height = 'auto';
318
- input.style.height = input.scrollHeight + 'px';
319
- }
320
- } else if (msg.data.text) {
321
- // First chunk: restore existing content
322
- if (input.value === '' && window.voiceInput.existingInputContent) {
323
- const existingContent = window.voiceInput.existingInputContent;
324
- const needSpace = existingContent && !existingContent.endsWith(' ');
325
- input.value = existingContent + (needSpace ? ' ' : '');
326
- // Clear the flag so we don't add it again
327
- window.voiceInput.existingInputContent = '';
328
- }
329
- // Stream text to input
330
- input.value += msg.data.text;
331
- input.style.height = 'auto';
332
- input.style.height = input.scrollHeight + 'px';
333
- } else if (msg.data.done) {
334
- console.log('[Claude] Processing complete');
335
- // Clear the existing content flag
336
- if (window.voiceInput) {
337
- window.voiceInput.existingInputContent = '';
338
-
339
- // Check if auto-submit is pending (triggered by "go go go" command)
340
- if (window.voiceInput.autoSubmitPending) {
341
- console.log('[Claude] Auto-submit triggered by "go go go" command');
342
- window.voiceInput.autoSubmitPending = false;
343
-
344
- // Auto-submit the command after a short delay to ensure input is updated
345
- setTimeout(() => {
346
- const cmd = input.value.trim();
347
- if (cmd && ws && ws.readyState === 1) {
348
- input.value = '';
349
- input.style.height = 'auto';
350
- // Send text first, then Enter key
351
- ws.send(JSON.stringify({ type: 'input', data: cmd }));
352
- setTimeout(() => {
353
- ws.send(JSON.stringify({ type: 'input', data: String.fromCharCode(13) }));
354
- }, 50);
355
- }
356
- }, 100);
357
- }
358
- }
359
- }
360
- }
361
- }
362
- };
363
- }
364
-
365
- // Input handling - must send text and Enter key separately for Claude Code to work
366
- input.addEventListener('keydown', (e) => {
367
- if (e.key === 'Enter' && !e.shiftKey) {
368
- e.preventDefault();
369
- if (ws && ws.readyState === 1) {
370
- const cmd = input.value;
371
- input.value = '';
372
- input.style.height = 'auto';
373
- if (cmd) {
374
- // Send text first, then Enter key separately after delay
375
- ws.send(JSON.stringify({ type: 'input', data: cmd }));
376
- setTimeout(() => {
377
- ws.send(JSON.stringify({ type: 'input', data: String.fromCharCode(13) }));
378
- }, 50);
379
- } else {
380
- // Just send Enter if empty
381
- ws.send(JSON.stringify({ type: 'input', data: String.fromCharCode(13) }));
382
- }
383
- }
384
- }
385
- });
386
-
387
- // Auto-resize textarea
388
- input.addEventListener('input', () => {
389
- input.style.height = 'auto';
390
- input.style.height = input.scrollHeight + 'px';
391
- });
392
-
393
- // Special keys handling
394
- specialKeysBtn.addEventListener('click', (e) => {
395
- e.stopPropagation();
396
- specialKeysPopup.classList.toggle('show');
397
- });
398
-
399
- document.addEventListener('click', (e) => {
400
- if (!specialKeysPopup.contains(e.target) && e.target !== specialKeysBtn) {
401
- specialKeysPopup.classList.remove('show');
402
- }
403
- });
404
-
405
- document.querySelectorAll('.special-key').forEach(btn => {
406
- btn.addEventListener('click', () => {
407
- const key = btn.getAttribute('data-key');
408
- handleSpecialKey(key);
409
- specialKeysPopup.classList.remove('show');
410
- });
411
- });
412
-
413
- function handleSpecialKey(key) {
414
- if (!ws || ws.readyState !== 1) return;
415
-
416
- const keyMap = {
417
- 'Escape': '\x1b',
418
- 'Tab': '\t',
419
- 'Up': '\x1b[A',
420
- 'Down': '\x1b[B',
421
- 'Left': '\x1b[D',
422
- 'Right': '\x1b[C',
423
- 'Ctrl+C': '\x03',
424
- 'Ctrl+D': '\x04',
425
- 'Ctrl+Z': '\x1a',
426
- 'Ctrl+L': '\x0c',
427
- 'Home': '\x1b[H',
428
- 'End': '\x1b[F',
429
- 'PageUp': '\x1b[5~',
430
- 'PageDown': '\x1b[6~',
431
- 'F1': '\x1bOP',
432
- 'F2': '\x1bOQ',
433
- 'F3': '\x1bOR',
434
- 'F4': '\x1bOS'
435
- };
436
-
437
- if (keyMap[key]) {
438
- ws.send(JSON.stringify({ type: 'input', data: keyMap[key] }));
439
- }
440
- }
441
-
442
- // Scroll handling
443
- function checkScrollPosition() {
444
- const viewport = document.querySelector('.xterm-viewport');
445
- if (!viewport) return;
446
-
447
- const isNearBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 100;
448
-
449
- if (isNearBottom) {
450
- scrollBtn.classList.remove('visible');
451
- isUserScrolling = false;
452
- } else {
453
- scrollBtn.classList.add('visible');
454
- }
455
- }
456
-
457
- scrollBtn.addEventListener('click', () => {
458
- const viewport = document.querySelector('.xterm-viewport');
459
- if (viewport) {
460
- viewport.scrollTop = viewport.scrollHeight;
461
- }
462
- term.scrollToBottom();
463
- isUserScrolling = false;
464
- scrollBtn.classList.remove('visible');
465
- });
466
-
467
- // Monitor scroll
468
- const viewport = document.querySelector('.xterm-viewport');
469
- if (viewport) {
470
- viewport.addEventListener('scroll', () => {
471
- checkScrollPosition();
472
- });
473
- }
474
-
475
- // Window resize
476
- let resizeTimeout;
477
- window.addEventListener('resize', () => {
478
- clearTimeout(resizeTimeout);
479
- resizeTimeout = setTimeout(() => {
480
- fitAddon.fit();
481
- if (ws && ws.readyState === 1) {
482
- ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
483
- }
484
- }, 100);
485
- });
486
-
487
- // Debug console controls
488
- const contextLengthInput = document.getElementById('context-length');
489
-
490
- // Update context length when changed
491
- if (contextLengthInput) {
492
- contextLengthInput.addEventListener('change', () => {
493
- const length = parseInt(contextLengthInput.value);
494
- if (window.terminalASR) {
495
- window.terminalASR.setMaxContextLength(length);
496
- }
497
- });
498
- }
499
-
500
- // Get terminal context function (kept for other uses)
501
- function getTerminalContext() {
502
- if (window.term) {
503
- const buffer = window.term.buffer.active;
504
- const lines = [];
505
- for (let i = Math.max(0, buffer.length - 50); i < buffer.length; i++) {
506
- const line = buffer.getLine(i);
507
- if (line) {
508
- lines.push(line.translateToString(true));
509
- }
510
- }
511
- return lines;
512
- }
513
- return [];
514
- }
515
-
516
- // Initial connection
517
- connect();