shell-mirror 1.5.8 → 1.5.9

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