shell-mirror 1.5.45 โ 1.5.47
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/mac-agent/agent.js +158 -10
- package/package.json +1 -1
- package/public/app/dashboard.html +19 -0
- package/public/app/dashboard.js +41 -0
- package/public/app/terminal.html +20 -1
- package/public/app/terminal.js +130 -0
- package/public/index.html +34 -0
package/mac-agent/agent.js
CHANGED
|
@@ -55,17 +55,79 @@ logToFile(`๐ Shell: ${shell}`);
|
|
|
55
55
|
|
|
56
56
|
// Circular buffer for session output persistence
|
|
57
57
|
class CircularBuffer {
|
|
58
|
-
constructor(size = 10000) {
|
|
58
|
+
constructor(size = 10000, maxTotalSize = 512 * 1024) { // 512KB max total size
|
|
59
59
|
this.size = size;
|
|
60
|
+
this.maxTotalSize = maxTotalSize;
|
|
60
61
|
this.buffer = [];
|
|
61
62
|
this.index = 0;
|
|
62
63
|
this.full = false;
|
|
64
|
+
this.totalSize = 0;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
add(data) {
|
|
68
|
+
const dataSize = Buffer.byteLength(data, 'utf8');
|
|
69
|
+
|
|
70
|
+
// Add new data
|
|
71
|
+
const oldData = this.buffer[this.index];
|
|
72
|
+
if (oldData) {
|
|
73
|
+
this.totalSize -= Buffer.byteLength(oldData, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
this.buffer[this.index] = data;
|
|
77
|
+
this.totalSize += dataSize;
|
|
67
78
|
this.index = (this.index + 1) % this.size;
|
|
68
79
|
if (this.index === 0) this.full = true;
|
|
80
|
+
|
|
81
|
+
// If total size exceeds limit, remove older data
|
|
82
|
+
this.enforceMaxSize();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
enforceMaxSize() {
|
|
86
|
+
if (this.totalSize <= this.maxTotalSize) return;
|
|
87
|
+
|
|
88
|
+
// Remove data from the oldest end until under limit
|
|
89
|
+
let removed = 0;
|
|
90
|
+
while (this.totalSize > this.maxTotalSize && this.getTotalItems() > 0) {
|
|
91
|
+
let oldestIndex;
|
|
92
|
+
if (this.full) {
|
|
93
|
+
oldestIndex = this.index; // Oldest item when buffer is full
|
|
94
|
+
} else {
|
|
95
|
+
oldestIndex = 0; // Start from beginning when not full
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const oldestData = this.buffer[oldestIndex];
|
|
99
|
+
if (oldestData) {
|
|
100
|
+
this.totalSize -= Buffer.byteLength(oldestData, 'utf8');
|
|
101
|
+
this.buffer[oldestIndex] = '';
|
|
102
|
+
removed++;
|
|
103
|
+
|
|
104
|
+
if (this.full) {
|
|
105
|
+
this.index = (this.index + 1) % this.size;
|
|
106
|
+
if (this.getTotalItems() === 0) {
|
|
107
|
+
this.full = false;
|
|
108
|
+
this.index = 0;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// Shift array to remove empty spot
|
|
112
|
+
this.buffer.splice(oldestIndex, 1);
|
|
113
|
+
this.buffer.push('');
|
|
114
|
+
this.index = Math.max(0, this.index - 1);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
break; // Prevent infinite loop
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (removed > 0) {
|
|
122
|
+
logToFile(`[BUFFER] Enforced max size limit: removed ${removed} old entries, total size now ${this.totalSize} bytes`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getTotalItems() {
|
|
127
|
+
if (!this.full) {
|
|
128
|
+
return this.buffer.slice(0, this.index).filter(item => item).length;
|
|
129
|
+
}
|
|
130
|
+
return this.buffer.filter(item => item).length;
|
|
69
131
|
}
|
|
70
132
|
|
|
71
133
|
getAll() {
|
|
@@ -79,6 +141,16 @@ class CircularBuffer {
|
|
|
79
141
|
this.buffer = [];
|
|
80
142
|
this.index = 0;
|
|
81
143
|
this.full = false;
|
|
144
|
+
this.totalSize = 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getStats() {
|
|
148
|
+
return {
|
|
149
|
+
items: this.getTotalItems(),
|
|
150
|
+
totalSize: this.totalSize,
|
|
151
|
+
maxSize: this.maxTotalSize,
|
|
152
|
+
utilizationPercent: Math.round((this.totalSize / this.maxTotalSize) * 100)
|
|
153
|
+
};
|
|
82
154
|
}
|
|
83
155
|
}
|
|
84
156
|
|
|
@@ -261,10 +333,9 @@ class SessionManager {
|
|
|
261
333
|
// For now, we'll use a global dataChannel reference
|
|
262
334
|
// In a full implementation, this would use a clientId-to-dataChannel mapping
|
|
263
335
|
if (typeof dataChannel !== 'undefined' && dataChannel && dataChannel.readyState === 'open') {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
logToFile(`[SESSION] Error sending to client ${clientId}: ${err.message}`);
|
|
336
|
+
const success = sendLargeMessage(dataChannel, message, '[SESSION]');
|
|
337
|
+
if (!success) {
|
|
338
|
+
logToFile(`[SESSION] โ Failed to send message to client ${clientId}`);
|
|
268
339
|
}
|
|
269
340
|
} else {
|
|
270
341
|
logToFile(`[SESSION] โ ๏ธ Cannot send to client ${clientId} - data channel not available`);
|
|
@@ -423,6 +494,11 @@ function connectToSignalingServer() {
|
|
|
423
494
|
// Force ICE gathering if it hasn't started within 2 seconds
|
|
424
495
|
logToFile('[AGENT] ๐ง Setting up ICE gathering fallback timer...');
|
|
425
496
|
setTimeout(() => {
|
|
497
|
+
if (!peerConnection) {
|
|
498
|
+
logToFile('[AGENT] โ ๏ธ ICE gathering timer fired but peerConnection is null (connection already closed)');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
426
502
|
if (peerConnection.iceGatheringState === 'new') {
|
|
427
503
|
logToFile('[AGENT] โ ๏ธ ICE gathering hasn\'t started - checking peer connection state');
|
|
428
504
|
logToFile(`[AGENT] Current ICE gathering state: ${peerConnection.iceGatheringState}`);
|
|
@@ -628,6 +704,80 @@ function cleanup(clientId = null) {
|
|
|
628
704
|
}
|
|
629
705
|
}
|
|
630
706
|
|
|
707
|
+
// WebRTC data channel message size limits and chunking
|
|
708
|
+
const MAX_WEBRTC_MESSAGE_SIZE = 32 * 1024; // 32KB - conservative limit for compatibility
|
|
709
|
+
const CHUNK_TYPE_START = 'chunk_start';
|
|
710
|
+
const CHUNK_TYPE_DATA = 'chunk_data';
|
|
711
|
+
const CHUNK_TYPE_END = 'chunk_end';
|
|
712
|
+
|
|
713
|
+
function sendLargeMessage(dataChannel, message, logPrefix = '[AGENT]') {
|
|
714
|
+
try {
|
|
715
|
+
const messageStr = JSON.stringify(message);
|
|
716
|
+
const messageBytes = Buffer.byteLength(messageStr, 'utf8');
|
|
717
|
+
|
|
718
|
+
if (messageBytes <= MAX_WEBRTC_MESSAGE_SIZE) {
|
|
719
|
+
// Small message, send directly
|
|
720
|
+
dataChannel.send(messageStr);
|
|
721
|
+
logToFile(`${logPrefix} โ
Sent small message (${messageBytes} bytes)`);
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Large message, chunk it
|
|
726
|
+
logToFile(`${logPrefix} ๐ฆ Chunking large message (${messageBytes} bytes) into ${MAX_WEBRTC_MESSAGE_SIZE} byte chunks`);
|
|
727
|
+
|
|
728
|
+
const chunkId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
|
729
|
+
const chunks = [];
|
|
730
|
+
|
|
731
|
+
// Split message string into chunks
|
|
732
|
+
for (let i = 0; i < messageStr.length; i += MAX_WEBRTC_MESSAGE_SIZE - 200) { // Reserve 200 bytes for chunk metadata
|
|
733
|
+
chunks.push(messageStr.slice(i, i + MAX_WEBRTC_MESSAGE_SIZE - 200));
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
logToFile(`${logPrefix} ๐ฆ Split into ${chunks.length} chunks`);
|
|
737
|
+
|
|
738
|
+
// Send chunk start notification
|
|
739
|
+
dataChannel.send(JSON.stringify({
|
|
740
|
+
type: CHUNK_TYPE_START,
|
|
741
|
+
chunkId: chunkId,
|
|
742
|
+
totalChunks: chunks.length,
|
|
743
|
+
totalSize: messageBytes,
|
|
744
|
+
originalType: message.type
|
|
745
|
+
}));
|
|
746
|
+
|
|
747
|
+
// Send each chunk with a small delay to prevent overwhelming
|
|
748
|
+
chunks.forEach((chunk, index) => {
|
|
749
|
+
setTimeout(() => {
|
|
750
|
+
try {
|
|
751
|
+
dataChannel.send(JSON.stringify({
|
|
752
|
+
type: CHUNK_TYPE_DATA,
|
|
753
|
+
chunkId: chunkId,
|
|
754
|
+
chunkIndex: index,
|
|
755
|
+
data: chunk
|
|
756
|
+
}));
|
|
757
|
+
|
|
758
|
+
// Send end notification after last chunk
|
|
759
|
+
if (index === chunks.length - 1) {
|
|
760
|
+
setTimeout(() => {
|
|
761
|
+
dataChannel.send(JSON.stringify({
|
|
762
|
+
type: CHUNK_TYPE_END,
|
|
763
|
+
chunkId: chunkId
|
|
764
|
+
}));
|
|
765
|
+
logToFile(`${logPrefix} โ
Large message sent successfully (${chunks.length} chunks)`);
|
|
766
|
+
}, 10);
|
|
767
|
+
}
|
|
768
|
+
} catch (err) {
|
|
769
|
+
logToFile(`${logPrefix} โ Error sending chunk ${index}: ${err.message}`);
|
|
770
|
+
}
|
|
771
|
+
}, index * 10); // 10ms delay between chunks
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
return true;
|
|
775
|
+
} catch (err) {
|
|
776
|
+
logToFile(`${logPrefix} โ Error in sendLargeMessage: ${err.message}`);
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
631
781
|
function setupDataChannel(clientId) {
|
|
632
782
|
dataChannel.onopen = () => {
|
|
633
783
|
logToFile('[AGENT] โ
Data channel is open!');
|
|
@@ -638,11 +788,9 @@ function setupDataChannel(clientId) {
|
|
|
638
788
|
const bufferedOutput = session.buffer.getAll();
|
|
639
789
|
if (bufferedOutput) {
|
|
640
790
|
logToFile(`[AGENT] ๐ค Sending buffered output to client (${bufferedOutput.length} chars)`);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
logToFile('[AGENT]
|
|
644
|
-
} catch (err) {
|
|
645
|
-
logToFile(`[AGENT] โ Error sending buffered output: ${err.message}`);
|
|
791
|
+
const success = sendLargeMessage(dataChannel, { type: 'output', data: bufferedOutput }, '[AGENT]');
|
|
792
|
+
if (!success) {
|
|
793
|
+
logToFile('[AGENT] โ Failed to send buffered output');
|
|
646
794
|
}
|
|
647
795
|
} else {
|
|
648
796
|
logToFile('[AGENT] โน๏ธ No buffered output to send for this session');
|
package/package.json
CHANGED
|
@@ -4,6 +4,25 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Shell Mirror Dashboard</title>
|
|
7
|
+
|
|
8
|
+
<!-- Google Analytics 4 -->
|
|
9
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-LG7ZGLB8FK"></script>
|
|
10
|
+
<script>
|
|
11
|
+
window.dataLayer = window.dataLayer || [];
|
|
12
|
+
function gtag(){dataLayer.push(arguments);}
|
|
13
|
+
gtag('js', new Date());
|
|
14
|
+
gtag('config', 'G-LG7ZGLB8FK');
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<!-- Microsoft Clarity -->
|
|
18
|
+
<script type="text/javascript">
|
|
19
|
+
(function(c,l,a,r,i,t,y){
|
|
20
|
+
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
|
21
|
+
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
|
22
|
+
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
|
23
|
+
})(window, document, "clarity", "script", "CLARITY_PROJECT_ID");
|
|
24
|
+
</script>
|
|
25
|
+
|
|
7
26
|
<link rel="stylesheet" href="dashboard.css">
|
|
8
27
|
</head>
|
|
9
28
|
<body>
|
package/public/app/dashboard.js
CHANGED
|
@@ -429,21 +429,62 @@ class ShellMirrorDashboard {
|
|
|
429
429
|
session.lastActivity > latest.lastActivity ? session : latest
|
|
430
430
|
);
|
|
431
431
|
console.log(`[DASHBOARD] โ
Reconnecting to existing session: ${mostRecentSession.id}`);
|
|
432
|
+
|
|
433
|
+
// Track terminal connection in Google Analytics
|
|
434
|
+
if (typeof gtag !== 'undefined') {
|
|
435
|
+
gtag('event', 'terminal_connect', {
|
|
436
|
+
event_category: 'terminal',
|
|
437
|
+
event_label: 'existing_session',
|
|
438
|
+
agent_id: agentId,
|
|
439
|
+
session_id: mostRecentSession.id
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
432
443
|
window.location.href = `/app/terminal.html?agent=${agentId}&session=${mostRecentSession.id}`;
|
|
433
444
|
} else {
|
|
434
445
|
// No existing sessions, create new one
|
|
435
446
|
console.log(`[DASHBOARD] ๐ Creating new session for agent: ${agentId}`);
|
|
447
|
+
|
|
448
|
+
// Track new session creation in Google Analytics
|
|
449
|
+
if (typeof gtag !== 'undefined') {
|
|
450
|
+
gtag('event', 'terminal_connect', {
|
|
451
|
+
event_category: 'terminal',
|
|
452
|
+
event_label: 'new_session',
|
|
453
|
+
agent_id: agentId
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
436
457
|
window.location.href = `/app/terminal.html?agent=${agentId}`;
|
|
437
458
|
}
|
|
438
459
|
}
|
|
439
460
|
|
|
440
461
|
async connectToSession(agentId, sessionId) {
|
|
462
|
+
// Track specific session connection in Google Analytics
|
|
463
|
+
if (typeof gtag !== 'undefined') {
|
|
464
|
+
gtag('event', 'terminal_connect', {
|
|
465
|
+
event_category: 'terminal',
|
|
466
|
+
event_label: 'specific_session',
|
|
467
|
+
agent_id: agentId,
|
|
468
|
+
session_id: sessionId
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
441
472
|
window.location.href = `/app/terminal.html?agent=${agentId}&session=${sessionId}`;
|
|
442
473
|
}
|
|
443
474
|
|
|
444
475
|
async createNewSession(agentId) {
|
|
445
476
|
// Force creation of new session by not passing session parameter
|
|
446
477
|
console.log(`[DASHBOARD] Creating new session for agent: ${agentId}`);
|
|
478
|
+
|
|
479
|
+
// Track explicit new session creation in Google Analytics
|
|
480
|
+
if (typeof gtag !== 'undefined') {
|
|
481
|
+
gtag('event', 'terminal_connect', {
|
|
482
|
+
event_category: 'terminal',
|
|
483
|
+
event_label: 'force_new_session',
|
|
484
|
+
agent_id: agentId
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
447
488
|
window.location.href = `/app/terminal.html?agent=${agentId}`;
|
|
448
489
|
}
|
|
449
490
|
|
package/public/app/terminal.html
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<title>Terminal Mirror</title>
|
|
5
|
+
|
|
6
|
+
<!-- Google Analytics 4 -->
|
|
7
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-LG7ZGLB8FK"></script>
|
|
8
|
+
<script>
|
|
9
|
+
window.dataLayer = window.dataLayer || [];
|
|
10
|
+
function gtag(){dataLayer.push(arguments);}
|
|
11
|
+
gtag('js', new Date());
|
|
12
|
+
gtag('config', 'G-LG7ZGLB8FK');
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<!-- Microsoft Clarity -->
|
|
16
|
+
<script type="text/javascript">
|
|
17
|
+
(function(c,l,a,r,i,t,y){
|
|
18
|
+
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
|
19
|
+
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
|
20
|
+
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
|
21
|
+
})(window, document, "clarity", "script", "CLARITY_PROJECT_ID");
|
|
22
|
+
</script>
|
|
23
|
+
|
|
5
24
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.15.0/css/xterm.css" />
|
|
6
25
|
<script src="https://cdn.jsdelivr.net/npm/xterm@4.15.0/lib/xterm.js"></script>
|
|
7
26
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>
|
|
@@ -222,6 +241,6 @@
|
|
|
222
241
|
</div>
|
|
223
242
|
</footer>
|
|
224
243
|
|
|
225
|
-
<script src="/app/terminal.js?v=
|
|
244
|
+
<script src="/app/terminal.js?v=chunked-20250820"></script>
|
|
226
245
|
</body>
|
|
227
246
|
</html>
|
package/public/app/terminal.js
CHANGED
|
@@ -55,6 +55,102 @@ let currentSession = null;
|
|
|
55
55
|
let availableSessions = [];
|
|
56
56
|
let requestedSessionId = null; // For connecting to specific session from URL
|
|
57
57
|
|
|
58
|
+
// Chunk reassembly for large messages
|
|
59
|
+
const chunkAssembler = {
|
|
60
|
+
activeChunks: new Map(),
|
|
61
|
+
|
|
62
|
+
handleChunkedMessage(message) {
|
|
63
|
+
const { type, chunkId } = message;
|
|
64
|
+
|
|
65
|
+
switch (type) {
|
|
66
|
+
case 'chunk_start':
|
|
67
|
+
console.log(`[CLIENT] ๐ฆ Starting chunk reassembly: ${chunkId} (${message.totalChunks} chunks, ${message.totalSize} bytes)`);
|
|
68
|
+
this.activeChunks.set(chunkId, {
|
|
69
|
+
originalType: message.originalType,
|
|
70
|
+
totalChunks: message.totalChunks,
|
|
71
|
+
totalSize: message.totalSize,
|
|
72
|
+
receivedChunks: new Map(),
|
|
73
|
+
startTime: Date.now()
|
|
74
|
+
});
|
|
75
|
+
return true;
|
|
76
|
+
|
|
77
|
+
case 'chunk_data':
|
|
78
|
+
const chunkInfo = this.activeChunks.get(chunkId);
|
|
79
|
+
if (!chunkInfo) {
|
|
80
|
+
console.error(`[CLIENT] โ Received chunk data for unknown chunk ID: ${chunkId}`);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
chunkInfo.receivedChunks.set(message.chunkIndex, message.data);
|
|
85
|
+
console.log(`[CLIENT] ๐ฆ Received chunk ${message.chunkIndex + 1}/${chunkInfo.totalChunks}`);
|
|
86
|
+
return true;
|
|
87
|
+
|
|
88
|
+
case 'chunk_end':
|
|
89
|
+
return this.reassembleChunks(chunkId);
|
|
90
|
+
|
|
91
|
+
default:
|
|
92
|
+
return false; // Not a chunk message
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
reassembleChunks(chunkId) {
|
|
97
|
+
const chunkInfo = this.activeChunks.get(chunkId);
|
|
98
|
+
if (!chunkInfo) {
|
|
99
|
+
console.error(`[CLIENT] โ Cannot reassemble unknown chunk: ${chunkId}`);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Check if we have all chunks
|
|
105
|
+
if (chunkInfo.receivedChunks.size !== chunkInfo.totalChunks) {
|
|
106
|
+
console.error(`[CLIENT] โ Missing chunks: expected ${chunkInfo.totalChunks}, got ${chunkInfo.receivedChunks.size}`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Reassemble chunks in order
|
|
111
|
+
let reassembledData = '';
|
|
112
|
+
for (let i = 0; i < chunkInfo.totalChunks; i++) {
|
|
113
|
+
if (!chunkInfo.receivedChunks.has(i)) {
|
|
114
|
+
console.error(`[CLIENT] โ Missing chunk ${i}`);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
reassembledData += chunkInfo.receivedChunks.get(i);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const elapsed = Date.now() - chunkInfo.startTime;
|
|
121
|
+
console.log(`[CLIENT] โ
Reassembled ${chunkInfo.totalChunks} chunks in ${elapsed}ms (${reassembledData.length} chars)`);
|
|
122
|
+
|
|
123
|
+
// Parse and process the reassembled message
|
|
124
|
+
const originalMessage = JSON.parse(reassembledData);
|
|
125
|
+
this.activeChunks.delete(chunkId);
|
|
126
|
+
|
|
127
|
+
// Process the original message
|
|
128
|
+
if (originalMessage.type === 'output') {
|
|
129
|
+
term.write(originalMessage.data);
|
|
130
|
+
} else {
|
|
131
|
+
handleSessionMessage(originalMessage);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error(`[CLIENT] โ Error reassembling chunks for ${chunkId}:`, err);
|
|
137
|
+
this.activeChunks.delete(chunkId);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
cleanup() {
|
|
143
|
+
// Clean up old incomplete chunks (older than 30 seconds)
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
for (const [chunkId, chunkInfo] of this.activeChunks.entries()) {
|
|
146
|
+
if (now - chunkInfo.startTime > 30000) {
|
|
147
|
+
console.log(`[CLIENT] ๐งน Cleaning up stale chunk: ${chunkId}`);
|
|
148
|
+
this.activeChunks.delete(chunkId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
58
154
|
// Connection status management
|
|
59
155
|
function updateConnectionStatus(status) {
|
|
60
156
|
const statusElement = document.getElementById('connection-status');
|
|
@@ -75,6 +171,11 @@ function updateConnectionStatus(status) {
|
|
|
75
171
|
}
|
|
76
172
|
}
|
|
77
173
|
|
|
174
|
+
// Cleanup timer for chunk assembler
|
|
175
|
+
setInterval(() => {
|
|
176
|
+
chunkAssembler.cleanup();
|
|
177
|
+
}, 30000); // Clean up every 30 seconds
|
|
178
|
+
|
|
78
179
|
// Check for agent parameter and connect directly
|
|
79
180
|
window.addEventListener('load', () => {
|
|
80
181
|
loadVersionInfo();
|
|
@@ -132,6 +233,17 @@ function startConnection() {
|
|
|
132
233
|
connectContainer.style.display = 'none';
|
|
133
234
|
terminalContainer.classList.add('show');
|
|
134
235
|
term.open(document.getElementById('terminal'));
|
|
236
|
+
|
|
237
|
+
// Track terminal session start in Google Analytics
|
|
238
|
+
if (typeof gtag !== 'undefined') {
|
|
239
|
+
gtag('event', 'terminal_session_start', {
|
|
240
|
+
event_category: 'terminal',
|
|
241
|
+
event_label: requestedSessionId ? 'existing_session' : 'new_session',
|
|
242
|
+
agent_id: AGENT_ID,
|
|
243
|
+
session_id: requestedSessionId || 'new'
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
135
247
|
// Delay fit to ensure proper dimensions after CSS transitions
|
|
136
248
|
setTimeout(() => {
|
|
137
249
|
fitAddon.fit();
|
|
@@ -419,6 +531,16 @@ async function createPeerConnection() {
|
|
|
419
531
|
case 'connected':
|
|
420
532
|
console.log('[CLIENT] โ
WebRTC connection established!');
|
|
421
533
|
updateConnectionStatus('connected');
|
|
534
|
+
|
|
535
|
+
// Track successful connection in Google Analytics
|
|
536
|
+
if (typeof gtag !== 'undefined') {
|
|
537
|
+
gtag('event', 'terminal_connection_success', {
|
|
538
|
+
event_category: 'terminal',
|
|
539
|
+
event_label: 'webrtc_established',
|
|
540
|
+
agent_id: AGENT_ID
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
422
544
|
break;
|
|
423
545
|
case 'completed':
|
|
424
546
|
console.log('[CLIENT] โ
ICE connection completed successfully!');
|
|
@@ -508,6 +630,14 @@ function setupDataChannel() {
|
|
|
508
630
|
dataChannel.onmessage = (event) => {
|
|
509
631
|
try {
|
|
510
632
|
const message = JSON.parse(event.data);
|
|
633
|
+
|
|
634
|
+
// Check if this is a chunked message
|
|
635
|
+
if (chunkAssembler.handleChunkedMessage(message)) {
|
|
636
|
+
// Message was handled by chunk assembler
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Handle normal messages
|
|
511
641
|
if (message.type === 'output') {
|
|
512
642
|
term.write(message.data);
|
|
513
643
|
} else {
|
package/public/index.html
CHANGED
|
@@ -18,6 +18,24 @@
|
|
|
18
18
|
<meta property="twitter:title" content="Shell Mirror - Use Claude Code CLI from your phone">
|
|
19
19
|
<meta property="twitter:description" content="Access your Mac terminal from your phone to use Claude Code CLI, Gemini CLI, and other command-line tools.">
|
|
20
20
|
|
|
21
|
+
<!-- Google Analytics 4 -->
|
|
22
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-LG7ZGLB8FK"></script>
|
|
23
|
+
<script>
|
|
24
|
+
window.dataLayer = window.dataLayer || [];
|
|
25
|
+
function gtag(){dataLayer.push(arguments);}
|
|
26
|
+
gtag('js', new Date());
|
|
27
|
+
gtag('config', 'G-LG7ZGLB8FK');
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<!-- Microsoft Clarity -->
|
|
31
|
+
<script type="text/javascript">
|
|
32
|
+
(function(c,l,a,r,i,t,y){
|
|
33
|
+
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
|
34
|
+
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
|
35
|
+
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
|
36
|
+
})(window, document, "clarity", "script", "CLARITY_PROJECT_ID");
|
|
37
|
+
</script>
|
|
38
|
+
|
|
21
39
|
<style>
|
|
22
40
|
* {
|
|
23
41
|
margin: 0;
|
|
@@ -679,12 +697,28 @@
|
|
|
679
697
|
|
|
680
698
|
// Handle Google login - direct web OAuth
|
|
681
699
|
async function handleGoogleLogin() {
|
|
700
|
+
// Track login attempt in Google Analytics
|
|
701
|
+
if (typeof gtag !== 'undefined') {
|
|
702
|
+
gtag('event', 'login_attempt', {
|
|
703
|
+
event_category: 'authentication',
|
|
704
|
+
event_label: 'google_oauth'
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
682
708
|
// Direct OAuth flow using the web backend
|
|
683
709
|
window.location.href = '/php-backend/api/auth-login.php?return=' + encodeURIComponent('/app/dashboard');
|
|
684
710
|
}
|
|
685
711
|
|
|
686
712
|
// Handle dashboard navigation
|
|
687
713
|
async function openDashboard() {
|
|
714
|
+
// Track dashboard access in Google Analytics
|
|
715
|
+
if (typeof gtag !== 'undefined') {
|
|
716
|
+
gtag('event', 'dashboard_access', {
|
|
717
|
+
event_category: 'navigation',
|
|
718
|
+
event_label: 'from_landing_page'
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
688
722
|
window.location.href = '/app/dashboard.html';
|
|
689
723
|
}
|
|
690
724
|
|