shell-mirror 1.5.8 → 1.5.10

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": "shell-mirror",
3
- "version": "1.5.8",
3
+ "version": "1.5.10",
4
4
  "description": "Access your Mac shell from any device securely. Perfect for mobile coding with Claude Code CLI, Gemini CLI, and any shell tool.",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -6,12 +6,50 @@
6
6
  <script src="https://cdn.jsdelivr.net/npm/xterm@4.15.0/lib/xterm.js"></script>
7
7
  <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>
8
8
  <style>
9
- body, html { margin: 0; padding: 0; height: 100%; overflow: hidden; background-color: #000; }
10
- #terminal { height: 100%; width: 100%; }
9
+ body, html {
10
+ margin: 0;
11
+ padding: 0;
12
+ height: 100%;
13
+ overflow: hidden;
14
+ background-color: #1e1e1e;
15
+ color: #ccc;
16
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
17
+ }
18
+ #terminal-container {
19
+ display: none;
20
+ height: 100%;
21
+ width: 100%;
22
+ background-color: #000000;
23
+ }
24
+ #terminal {
25
+ padding: 8px; /* Mac Terminal.app padding */
26
+ background-color: #000000;
27
+ height: calc(100% - 16px);
28
+ width: calc(100% - 16px);
29
+ }
30
+ #connect-container { padding: 2em; text-align: center; }
31
+ #agent-id-input { font-size: 1.2em; padding: 8px; width: 400px; margin-bottom: 1em; }
32
+ #connect-btn { font-size: 1.2em; padding: 10px 20px; }
11
33
  </style>
12
34
  </head>
13
35
  <body>
14
- <div id="terminal"></div>
36
+ <div id="connect-container">
37
+ <h2>Terminal Mirror</h2>
38
+ <div id="agent-discovery">
39
+ <p>Discovering available Mac agents...</p>
40
+ <div id="agent-list"></div>
41
+ </div>
42
+ <div id="manual-connect" style="display: none; margin-top: 20px;">
43
+ <p>Or manually enter Agent ID:</p>
44
+ <input type="text" id="agent-id-input" placeholder="e.g., agent-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
45
+ <br>
46
+ <button id="connect-btn">Connect</button>
47
+ </div>
48
+ <button id="show-manual" style="margin-top: 10px;">Manual Connect</button>
49
+ </div>
50
+ <div id="terminal-container">
51
+ <div id="terminal"></div>
52
+ </div>
15
53
  <script src="/app/terminal.js"></script>
16
54
  </body>
17
55
  </html>
@@ -2,91 +2,546 @@ const term = new Terminal({
2
2
  cursorBlink: true,
3
3
  macOptionIsMeta: true,
4
4
  scrollback: 1000,
5
+ // Mac Terminal.app appearance settings
6
+ theme: {
7
+ background: '#000000', // Pure black like Mac Terminal
8
+ foreground: '#ffffff', // White text
9
+ cursor: '#ffffff', // White cursor
10
+ cursorAccent: '#000000', // Black cursor accent
11
+ selection: '#5c5c5c', // Mac selection color
12
+ // Mac Terminal color palette
13
+ black: '#000000',
14
+ red: '#c23621',
15
+ green: '#25bc24',
16
+ yellow: '#adad27',
17
+ blue: '#492ee1',
18
+ magenta: '#d338d3',
19
+ cyan: '#33bbc8',
20
+ white: '#cbcccd',
21
+ brightBlack: '#818383',
22
+ brightRed: '#fc391f',
23
+ brightGreen: '#31e722',
24
+ brightYellow: '#eaec23',
25
+ brightBlue: '#5833ff',
26
+ brightMagenta: '#f935f8',
27
+ brightCyan: '#14f0f0',
28
+ brightWhite: '#e9ebeb'
29
+ },
30
+ fontFamily: '"SF Mono", Monaco, Menlo, "Ubuntu Mono", monospace', // Mac system fonts
31
+ fontSize: 11, // Mac Terminal default size
32
+ lineHeight: 1.2, // Mac Terminal line spacing
33
+ letterSpacing: 0, // Tight character spacing like Mac
34
+ allowTransparency: false, // Solid background
35
+ convertEol: true, // Convert line endings properly
36
+ cols: 120, // Match agent terminal width
37
+ rows: 30 // Match agent terminal height
5
38
  });
6
39
  const fitAddon = new FitAddon.FitAddon();
7
40
  term.loadAddon(fitAddon);
8
- term.open(document.getElementById('terminal'));
9
- fitAddon.fit();
41
+
42
+ const connectContainer = document.getElementById('connect-container');
43
+ const terminalContainer = document.getElementById('terminal-container');
44
+ const agentIdInput = document.getElementById('agent-id-input');
45
+ const connectBtn = document.getElementById('connect-btn');
46
+ const showManualBtn = document.getElementById('show-manual');
47
+ const manualConnect = document.getElementById('manual-connect');
48
+ const agentDiscovery = document.getElementById('agent-discovery');
49
+ const agentList = document.getElementById('agent-list');
10
50
 
11
51
  let ws;
12
52
  let peerConnection;
13
53
  let dataChannel;
14
54
  let user;
55
+ let AGENT_ID;
56
+ let CLIENT_ID;
57
+
58
+ // Auto-discover agents on page load
59
+ window.addEventListener('load', () => {
60
+ discoverAgents();
61
+ });
15
62
 
16
- const AGENT_ID = 'your-agent-id'; // This should be dynamically set, perhaps from a user input or a query param
63
+ showManualBtn.onclick = () => {
64
+ agentDiscovery.style.display = 'none';
65
+ manualConnect.style.display = 'block';
66
+ showManualBtn.style.display = 'none';
67
+ };
17
68
 
18
- async function initialize() {
19
- const status = await fetch('/api/auth/status').then(res => res.json());
20
- if (!status.authenticated) {
21
- window.location.href = '/';
69
+ connectBtn.onclick = () => {
70
+ AGENT_ID = agentIdInput.value.trim();
71
+ if (!AGENT_ID) {
72
+ alert('Please enter a valid Agent ID.');
73
+ return;
74
+ }
75
+ startConnection();
76
+ };
77
+
78
+ function connectToAgent(agentId) {
79
+ AGENT_ID = agentId;
80
+ startConnection();
81
+ }
82
+
83
+ function startConnection() {
84
+ connectContainer.style.display = 'none';
85
+ terminalContainer.style.display = 'block';
86
+ term.open(document.getElementById('terminal'));
87
+ // Delay fit to ensure proper dimensions after CSS transitions
88
+ setTimeout(() => {
89
+ fitAddon.fit();
90
+ }, 100);
91
+ initialize();
92
+ }
93
+
94
+ async function discoverAgents() {
95
+ console.log('[DISCOVERY] 🔍 Starting agent discovery...');
96
+ agentList.innerHTML = '<p style="color: #ccc;">Searching for Mac agents...</p>';
97
+
98
+ // Production WebSocket server configuration
99
+ let signalingUrl;
100
+ if (window.location.hostname === 'shellmirror.app') {
101
+ // Production: Use separate WebSocket server (to be deployed)
102
+ signalingUrl = 'wss://ws.shellmirror.app';
103
+ } else {
104
+ // Development: Use local server
105
+ signalingUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host;
106
+ }
107
+ const discoveryWs = new WebSocket(`${signalingUrl}?role=discovery`);
108
+
109
+ discoveryWs.onopen = () => {
110
+ console.log('[DISCOVERY] ✅ Connected to signaling server for agent discovery');
111
+ discoveryWs.send(JSON.stringify({ type: 'list-agents' }));
112
+ };
113
+
114
+ discoveryWs.onmessage = (message) => {
115
+ const data = JSON.parse(message.data);
116
+ console.log('[DISCOVERY] 📨 Received:', data);
117
+ if (data.type === 'agent-list') {
118
+ displayAvailableAgents(data.agents);
119
+ }
120
+ };
121
+
122
+ discoveryWs.onclose = () => {
123
+ console.log('[DISCOVERY] 🔌 Discovery connection closed');
124
+ };
125
+
126
+ discoveryWs.onerror = (error) => {
127
+ console.error('[DISCOVERY] ❌ WebSocket error:', error);
128
+ agentList.innerHTML = '<p style="color: #f44336;">Discovery failed. Check server connection.</p>';
129
+ showManualBtn.style.display = 'block';
130
+ };
131
+
132
+ // Timeout after 8 seconds (increased from 5)
133
+ setTimeout(() => {
134
+ if (discoveryWs.readyState === WebSocket.OPEN) {
135
+ discoveryWs.close();
136
+ }
137
+ if (agentList.children.length === 0 || agentList.textContent.includes('Searching')) {
138
+ console.log('[DISCOVERY] ⏰ Discovery timeout - no agents found');
139
+ agentList.innerHTML = '<p style="color: #ff9800;">⚠️ No Mac agents found.<br><small>Make sure your Mac agent is running with: <code>cd mac-agent && npm start</code></small></p>';
140
+ showManualBtn.style.display = 'block';
141
+ }
142
+ }, 8000);
143
+ }
144
+
145
+ function displayAvailableAgents(agents) {
146
+ console.log('[DISCOVERY] 🖥️ Displaying agents:', agents);
147
+ agentList.innerHTML = '';
148
+
149
+ if (agents.length === 0) {
150
+ agentList.innerHTML = '<p style="color: #ff9800;">❌ No Mac agents currently running.</p>';
151
+ showManualBtn.style.display = 'block';
22
152
  return;
23
153
  }
24
- user = status.user;
154
+
155
+ console.log(`[DISCOVERY] ✅ Found ${agents.length} agent(s)`);
156
+
157
+ agents.forEach(agent => {
158
+ const agentDiv = document.createElement('div');
159
+ agentDiv.style.cssText = 'margin: 10px 0; padding: 15px; background: #333; border-radius: 8px; cursor: pointer; border: 2px solid #555; transition: all 0.3s ease;';
160
+ agentDiv.innerHTML = `
161
+ <div style="display: flex; align-items: center; gap: 10px;">
162
+ <div style="width: 12px; height: 12px; background: #4CAF50; border-radius: 50%; animation: pulse 2s infinite;"></div>
163
+ <div>
164
+ <strong style="color: #fff;">${agent.id}</strong><br>
165
+ <small style="color: #aaa;">🖱️ Click to connect to Mac terminal</small>
166
+ </div>
167
+ </div>
168
+ `;
169
+
170
+ agentDiv.onmouseover = () => {
171
+ agentDiv.style.borderColor = '#4CAF50';
172
+ agentDiv.style.background = '#444';
173
+ };
174
+ agentDiv.onmouseout = () => {
175
+ agentDiv.style.borderColor = '#555';
176
+ agentDiv.style.background = '#333';
177
+ };
178
+ agentDiv.onclick = () => {
179
+ console.log(`[DISCOVERY] 🖱️ User clicked on agent: ${agent.id}`);
180
+ connectToAgent(agent.id);
181
+ };
182
+
183
+ agentList.appendChild(agentDiv);
184
+ });
185
+
186
+ showManualBtn.style.display = 'block';
187
+
188
+ // Add CSS animation for pulse effect
189
+ const style = document.createElement('style');
190
+ style.textContent = `
191
+ @keyframes pulse {
192
+ 0% { opacity: 1; }
193
+ 50% { opacity: 0.5; }
194
+ 100% { opacity: 1; }
195
+ }
196
+ `;
197
+ document.head.appendChild(style);
198
+ }
25
199
 
26
- const signalingUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host;
200
+ async function initialize() {
201
+ console.log('[CLIENT] 🚀 Initializing WebRTC connection to agent:', AGENT_ID);
202
+ // Production WebSocket server configuration
203
+ let signalingUrl;
204
+ if (window.location.hostname === 'shellmirror.app') {
205
+ // Production: Use separate WebSocket server (to be deployed)
206
+ signalingUrl = 'wss://ws.shellmirror.app';
207
+ } else {
208
+ // Development: Use local server
209
+ signalingUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host;
210
+ }
27
211
  ws = new WebSocket(`${signalingUrl}?role=client`);
28
212
 
29
213
  ws.onopen = () => {
30
- console.log('Connected to signaling server.');
31
- // Let the agent know we want to connect
32
- sendMessage({ type: 'client-hello', from: user.id, to: AGENT_ID });
214
+ console.log('[CLIENT] ✅ WebSocket connection to signaling server opened.');
33
215
  };
34
216
 
35
217
  ws.onmessage = async (message) => {
36
218
  const data = JSON.parse(message.data);
37
- console.log('Received signaling message:', data.type);
38
-
39
- if (!peerConnection && data.type === 'offer') {
40
- await createPeerConnection();
41
- await peerConnection.setRemoteDescription(new RTCSessionDescription(data));
42
- const answer = await peerConnection.createAnswer();
43
- await peerConnection.setLocalDescription(answer);
44
- sendMessage({ type: 'answer', sdp: answer.sdp, to: AGENT_ID });
45
- } else if (data.type === 'candidate') {
46
- await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
219
+ console.log(`[CLIENT] Received message of type: ${data.type}`);
220
+
221
+ switch (data.type) {
222
+ case 'server-hello':
223
+ CLIENT_ID = data.id;
224
+ console.log(`[CLIENT] Assigned Client ID: ${CLIENT_ID}`);
225
+
226
+ // First send a test message to verify communication
227
+ console.log(`[CLIENT] 🧪 Sending test ping message first...`);
228
+ const testSent = sendMessage({ type: 'ping', from: CLIENT_ID, to: AGENT_ID, timestamp: Date.now() });
229
+
230
+ if (!testSent) {
231
+ console.error(`[CLIENT] ❌ Failed to send test message - WebSocket connection broken`);
232
+ return;
233
+ }
234
+
235
+ // Start polling to connect to the agent
236
+ const intervalId = setInterval(() => {
237
+ console.log(`[CLIENT] 📞 Sending client-hello to Agent: ${AGENT_ID}`);
238
+ const sent = sendMessage({ type: 'client-hello', from: CLIENT_ID, to: AGENT_ID });
239
+ if (!sent) {
240
+ console.error(`[CLIENT] ❌ Failed to send client-hello - stopping attempts`);
241
+ clearInterval(intervalId);
242
+ }
243
+ }, 1000);
244
+
245
+ // This is a bit of a hack for the message handler.
246
+ // We redefine it to handle the next phase of messages.
247
+ ws.onmessage = async (nextMessage) => {
248
+ let messageData = nextMessage.data;
249
+
250
+ // Handle Blob messages by converting to text first
251
+ if (messageData instanceof Blob) {
252
+ console.log(`[CLIENT] 📄 Received Blob message, converting to text...`);
253
+ messageData = await messageData.text();
254
+ }
255
+
256
+ try {
257
+ const nextData = JSON.parse(messageData);
258
+ console.log(`[CLIENT] 📨 Received message of type: ${nextData.type}`);
259
+
260
+ if (nextData.type === 'offer') {
261
+ console.log('[CLIENT] Received offer from agent. Stopping client-hello retries.');
262
+ clearInterval(intervalId);
263
+
264
+ console.log('[CLIENT] Received WebRTC offer from agent.');
265
+ await createPeerConnection();
266
+ await peerConnection.setRemoteDescription(new RTCSessionDescription(nextData));
267
+ const answer = await peerConnection.createAnswer();
268
+ await peerConnection.setLocalDescription(answer);
269
+ console.log('[CLIENT] Sending WebRTC answer to agent.');
270
+ sendMessage({ type: 'answer', sdp: answer.sdp, to: AGENT_ID, from: CLIENT_ID });
271
+
272
+ // Force ICE gathering if it hasn't started within 2 seconds
273
+ console.log('[CLIENT] 🔧 Setting up ICE gathering fallback timer...');
274
+ setTimeout(() => {
275
+ if (peerConnection.iceGatheringState === 'new') {
276
+ console.log('[CLIENT] ⚠️ ICE gathering hasn\'t started - triggering restart');
277
+ try {
278
+ peerConnection.restartIce();
279
+ } catch (error) {
280
+ console.error('[CLIENT] ❌ Failed to restart ICE:', error);
281
+ }
282
+ } else {
283
+ console.log('[CLIENT] ✅ ICE gathering is active:', peerConnection.iceGatheringState);
284
+ }
285
+ }, 2000);
286
+ } else if (nextData.type === 'candidate') {
287
+ console.log('[CLIENT] 🧊 Received ICE candidate from agent:', {
288
+ candidate: nextData.candidate.candidate,
289
+ sdpMid: nextData.candidate.sdpMid,
290
+ sdpMLineIndex: nextData.candidate.sdpMLineIndex
291
+ });
292
+ if (peerConnection) {
293
+ try {
294
+ await peerConnection.addIceCandidate(new RTCIceCandidate(nextData.candidate));
295
+ console.log('[CLIENT] ✅ ICE candidate added successfully');
296
+ } catch (error) {
297
+ console.error('[CLIENT] ❌ Error adding ICE candidate:', error);
298
+ }
299
+ } else {
300
+ console.error('[CLIENT] ❌ Cannot add ICE candidate - no peer connection');
301
+ }
302
+ }
303
+ } catch (error) {
304
+ console.error(`[CLIENT] ❌ Error processing WebRTC message:`, error);
305
+ }
306
+ };
307
+ break;
47
308
  }
48
309
  };
49
310
 
50
- ws.onclose = () => {
51
- console.log('Disconnected from signaling server.');
311
+ ws.onclose = (event) => {
312
+ console.log(`[CLIENT] 🔌 Disconnected from signaling server. Code: ${event.code}, Reason: ${event.reason}`);
52
313
  term.write('\r\n\r\nConnection to server lost. Please refresh.\r\n');
53
314
  };
315
+
316
+ ws.onerror = (error) => {
317
+ console.error('[CLIENT] ❌ WebSocket error:', error);
318
+ };
319
+ }
320
+
321
+ async function testSTUNConnectivity() {
322
+ console.log('[CLIENT] 🧪 Testing STUN server connectivity...');
323
+
324
+ try {
325
+ // Create a test peer connection to check STUN server access
326
+ const testPC = new RTCPeerConnection({
327
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
328
+ });
329
+
330
+ let candidateReceived = false;
331
+
332
+ return new Promise((resolve) => {
333
+ const timeout = setTimeout(() => {
334
+ console.log('[CLIENT] ⚠️ STUN connectivity test timed out - may indicate network restrictions');
335
+ testPC.close();
336
+ resolve(false);
337
+ }, 5000);
338
+
339
+ testPC.onicecandidate = (event) => {
340
+ if (event.candidate && !candidateReceived) {
341
+ candidateReceived = true;
342
+ console.log('[CLIENT] ✅ STUN server connectivity confirmed');
343
+ clearTimeout(timeout);
344
+ testPC.close();
345
+ resolve(true);
346
+ }
347
+ };
348
+
349
+ // Create a dummy data channel to trigger ICE gathering
350
+ testPC.createDataChannel('test');
351
+ testPC.createOffer().then(offer => testPC.setLocalDescription(offer));
352
+ });
353
+ } catch (error) {
354
+ console.error('[CLIENT] ❌ STUN connectivity test failed:', error);
355
+ return false;
356
+ }
54
357
  }
55
358
 
56
359
  async function createPeerConnection() {
57
- peerConnection = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
360
+ console.log('[CLIENT] Creating PeerConnection.');
361
+
362
+ // Test STUN connectivity first
363
+ const stunWorking = await testSTUNConnectivity();
364
+ if (!stunWorking) {
365
+ console.log('[CLIENT] ⚠️ STUN servers may be blocked - using TURN servers for connectivity');
366
+ }
367
+
368
+ // Test STUN server connectivity with multiple backup servers
369
+ console.log('[CLIENT] 🌐 Configuring ICE servers with multiple STUN/TURN options...');
370
+ const iceServers = [
371
+ // Google STUN servers (primary)
372
+ { urls: 'stun:stun.l.google.com:19302' },
373
+ { urls: 'stun:stun1.l.google.com:19302' },
374
+ // Cloudflare STUN servers (backup)
375
+ { urls: 'stun:stun.cloudflare.com:3478' },
376
+ // Mozilla STUN servers (backup)
377
+ { urls: 'stun:stun.services.mozilla.com:3478' },
378
+ // OpenRelay free TURN server (for NAT traversal)
379
+ {
380
+ urls: 'turn:openrelay.metered.ca:80',
381
+ username: 'openrelayproject',
382
+ credential: 'openrelayproject'
383
+ },
384
+ // Alternative TURN server
385
+ {
386
+ urls: 'turn:openrelay.metered.ca:443',
387
+ username: 'openrelayproject',
388
+ credential: 'openrelayproject'
389
+ }
390
+ ];
391
+
392
+ console.log('[CLIENT] 📋 Configured ICE servers:', iceServers.map(server => server.urls));
393
+
394
+ // Enhanced WebRTC configuration for better ICE candidate generation
395
+ const rtcConfig = {
396
+ iceServers: iceServers,
397
+ iceCandidatePoolSize: 10, // Generate more ICE candidates
398
+ iceTransportPolicy: 'all', // Use both STUN and TURN
399
+ bundlePolicy: 'balanced' // Optimize for connection establishment
400
+ };
401
+
402
+ console.log('[CLIENT] ⚙️ WebRTC config:', rtcConfig);
403
+ peerConnection = new RTCPeerConnection(rtcConfig);
58
404
 
405
+ // Debug: Verify event handler is being attached
406
+ console.log('[CLIENT] 🔧 Attaching ICE candidate event handler...');
407
+
59
408
  peerConnection.onicecandidate = (event) => {
409
+ console.log('[CLIENT] 🧊 ICE candidate event fired:', event.candidate ? 'candidate found' : 'gathering complete');
60
410
  if (event.candidate) {
61
- sendMessage({ type: 'candidate', candidate: event.candidate, to: AGENT_ID });
411
+ console.log('[CLIENT] 📤 ICE candidate details:', {
412
+ candidate: event.candidate.candidate,
413
+ sdpMid: event.candidate.sdpMid,
414
+ sdpMLineIndex: event.candidate.sdpMLineIndex
415
+ });
416
+ console.log('[CLIENT] 📤 Sending ICE candidate to agent...');
417
+ const sent = sendMessage({ type: 'candidate', candidate: event.candidate, to: AGENT_ID, from: CLIENT_ID });
418
+ if (sent) {
419
+ console.log('[CLIENT] ✅ ICE candidate sent successfully');
420
+ } else {
421
+ console.log('[CLIENT] ❌ Failed to send ICE candidate');
422
+ }
423
+ } else {
424
+ console.log('[CLIENT] 🏁 ICE candidate gathering complete.');
425
+ }
426
+ };
427
+
428
+ peerConnection.oniceconnectionstatechange = () => {
429
+ console.log(`[CLIENT] 📊 ICE connection state changed: ${peerConnection.iceConnectionState}`);
430
+ console.log(`[CLIENT] 📊 ICE gathering state: ${peerConnection.iceGatheringState}`);
431
+
432
+ switch (peerConnection.iceConnectionState) {
433
+ case 'new':
434
+ console.log('[CLIENT] 🆕 ICE connection starting...');
435
+ break;
436
+ case 'checking':
437
+ console.log('[CLIENT] 🔍 ICE connection checking candidates...');
438
+ break;
439
+ case 'connected':
440
+ console.log('[CLIENT] ✅ WebRTC connection established!');
441
+ break;
442
+ case 'completed':
443
+ console.log('[CLIENT] ✅ ICE connection completed successfully!');
444
+ break;
445
+ case 'failed':
446
+ console.log('[CLIENT] ❌ ICE connection failed - no viable candidates');
447
+ console.log('[CLIENT] 💡 Troubleshooting: This may be due to firewall/NAT issues or blocked STUN servers');
448
+ term.write('\r\n\r\n❌ Connection failed: Network connectivity issues\r\n');
449
+ term.write('💡 This may be due to:\r\n');
450
+ term.write(' • Firewall blocking WebRTC traffic\r\n');
451
+ term.write(' • Corporate network restrictions\r\n');
452
+ term.write(' • STUN/TURN servers unreachable\r\n');
453
+ term.write('\r\n🔄 Please refresh the page to retry...\r\n');
454
+ break;
455
+ case 'disconnected':
456
+ console.log('[CLIENT] ⚠️ ICE connection disconnected');
457
+ break;
458
+ case 'closed':
459
+ console.log('[CLIENT] 🔐 ICE connection closed');
460
+ break;
62
461
  }
63
462
  };
64
463
 
65
- dataChannel = peerConnection.createDataChannel('terminal');
66
- setupDataChannel();
464
+ peerConnection.onconnectionstatechange = () => {
465
+ console.log(`[CLIENT] 📡 Connection state changed: ${peerConnection.connectionState}`);
466
+
467
+ switch (peerConnection.connectionState) {
468
+ case 'new':
469
+ console.log('[CLIENT] 🆕 Connection starting...');
470
+ break;
471
+ case 'connecting':
472
+ console.log('[CLIENT] 🔄 Connection in progress...');
473
+ break;
474
+ case 'connected':
475
+ console.log('[CLIENT] ✅ Peer connection fully established!');
476
+ break;
477
+ case 'disconnected':
478
+ console.log('[CLIENT] ⚠️ Peer connection disconnected');
479
+ break;
480
+ case 'failed':
481
+ console.log('[CLIENT] ❌ Peer connection failed completely');
482
+ break;
483
+ case 'closed':
484
+ console.log('[CLIENT] 🔐 Peer connection closed');
485
+ break;
486
+ }
487
+ };
488
+
489
+ peerConnection.onicegatheringstatechange = () => {
490
+ console.log(`[CLIENT] 🔍 ICE gathering state changed: ${peerConnection.iceGatheringState}`);
491
+
492
+ switch (peerConnection.iceGatheringState) {
493
+ case 'new':
494
+ console.log('[CLIENT] 🆕 ICE gathering not started');
495
+ break;
496
+ case 'gathering':
497
+ console.log('[CLIENT] 🔍 ICE gathering in progress...');
498
+ break;
499
+ case 'complete':
500
+ console.log('[CLIENT] ✅ ICE gathering completed');
501
+ break;
502
+ }
503
+ };
504
+
505
+ // Client waits for data channel from agent
506
+ peerConnection.ondatachannel = (event) => {
507
+ console.log('[CLIENT] 📨 Data channel received from agent!');
508
+ dataChannel = event.channel;
509
+ setupDataChannel();
510
+ };
67
511
  }
68
512
 
69
513
  function setupDataChannel() {
70
514
  dataChannel.onopen = () => {
71
- console.log('Data channel is open!');
515
+ console.log('[CLIENT] ✅ Data channel is open!');
72
516
  term.focus();
73
517
  fitAddon.fit();
518
+ // Mac-style connection message with proper colors
519
+ term.write('\r\n\x1b[32mConnected to Mac Terminal\x1b[0m\r\n');
74
520
  };
75
521
 
76
522
  dataChannel.onmessage = (event) => {
77
- const message = JSON.parse(event.data);
78
- if (message.type === 'output') {
79
- term.write(message.data);
523
+ try {
524
+ const message = JSON.parse(event.data);
525
+ if (message.type === 'output') {
526
+ term.write(message.data);
527
+ }
528
+ } catch (err) {
529
+ console.error('[CLIENT] Error parsing data channel message:', err);
80
530
  }
81
531
  };
82
532
 
83
533
  dataChannel.onclose = () => {
84
- console.log('Data channel closed.');
85
- term.write('\r\n\r\nTerminal session ended.\r\n');
534
+ console.log('[CLIENT] Data channel closed.');
535
+ term.write('\r\n\r\n\x1b[31m❌ Terminal session ended.\x1b[0m\r\n');
536
+ };
537
+
538
+ dataChannel.onerror = (error) => {
539
+ console.error('[CLIENT] Data channel error:', error);
540
+ term.write('\r\n\r\n\x1b[31m❌ Data channel error occurred.\x1b[0m\r\n');
86
541
  };
87
542
 
88
543
  term.onData((data) => {
89
- if (dataChannel.readyState === 'open') {
544
+ if (dataChannel && dataChannel.readyState === 'open') {
90
545
  dataChannel.send(JSON.stringify({ type: 'input', data }));
91
546
  }
92
547
  });
@@ -96,16 +551,35 @@ function setupDataChannel() {
96
551
  });
97
552
 
98
553
  term.onResize(({ cols, rows }) => {
99
- if (dataChannel.readyState === 'open') {
554
+ if (dataChannel && dataChannel.readyState === 'open') {
100
555
  dataChannel.send(JSON.stringify({ type: 'resize', cols, rows }));
101
556
  }
102
557
  });
103
558
  }
104
559
 
105
560
  function sendMessage(message) {
106
- if (ws && ws.readyState === 'open') {
107
- ws.send(JSON.stringify(message));
561
+ console.log(`[CLIENT] 📤 Attempting to send message:`, message);
562
+ console.log(`[CLIENT] 🔍 WebSocket state: ${ws ? ws.readyState : 'null'} (OPEN=1)`);
563
+
564
+ if (!ws) {
565
+ console.error('[CLIENT] ❌ WebSocket is null - cannot send message');
566
+ return false;
108
567
  }
109
- }
110
-
111
- initialize();
568
+
569
+ if (ws.readyState !== 1) { // WebSocket.OPEN = 1
570
+ const states = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
571
+ console.error(`[CLIENT] ❌ WebSocket not open (state: ${ws.readyState} = ${states[ws.readyState] || 'UNKNOWN'}) - cannot send message`);
572
+ return false;
573
+ }
574
+
575
+ try {
576
+ const messageStr = JSON.stringify(message);
577
+ console.log(`[CLIENT] 📨 Sending message: ${messageStr}`);
578
+ ws.send(messageStr);
579
+ console.log(`[CLIENT] ✅ Message sent successfully`);
580
+ return true;
581
+ } catch (error) {
582
+ console.error(`[CLIENT] ❌ Error sending message:`, error);
583
+ return false;
584
+ }
585
+ }