shell-mirror 1.5.38 → 1.5.40

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.
@@ -212,6 +212,11 @@ async function createPeerConnection(clientId) {
212
212
  };
213
213
 
214
214
  peerConnection.oniceconnectionstatechange = () => {
215
+ if (!peerConnection) {
216
+ logToFile('[AGENT] ⚠️ ICE connection state change after peerConnection was closed');
217
+ return;
218
+ }
219
+
215
220
  logToFile(`[AGENT] 📊 ICE connection state changed: ${peerConnection.iceConnectionState}`);
216
221
  logToFile(`[AGENT] 📊 ICE gathering state: ${peerConnection.iceGatheringState}`);
217
222
 
@@ -243,6 +248,11 @@ async function createPeerConnection(clientId) {
243
248
  };
244
249
 
245
250
  peerConnection.onconnectionstatechange = () => {
251
+ if (!peerConnection) {
252
+ logToFile('[AGENT] ⚠️ Connection state change after peerConnection was closed');
253
+ return;
254
+ }
255
+
246
256
  logToFile(`[AGENT] 📡 Connection state changed: ${peerConnection.connectionState}`);
247
257
 
248
258
  switch (peerConnection.connectionState) {
@@ -268,6 +278,11 @@ async function createPeerConnection(clientId) {
268
278
  };
269
279
 
270
280
  peerConnection.onicegatheringstatechange = () => {
281
+ if (!peerConnection) {
282
+ logToFile('[AGENT] ⚠️ ICE gathering state change after peerConnection was closed');
283
+ return;
284
+ }
285
+
271
286
  logToFile(`[AGENT] 🔍 ICE gathering state changed: ${peerConnection.iceGatheringState}`);
272
287
 
273
288
  switch (peerConnection.iceGatheringState) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.5.38",
3
+ "version": "1.5.40",
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": {
@@ -294,6 +294,10 @@ body {
294
294
  border-bottom: 1px solid #f0f0f0;
295
295
  }
296
296
 
297
+ .session-item.compact {
298
+ padding: 12px 0;
299
+ }
300
+
297
301
  .session-item:last-child {
298
302
  border-bottom: none;
299
303
  }
@@ -345,6 +349,11 @@ body {
345
349
  color: #f57c00;
346
350
  }
347
351
 
352
+ .session-status.disconnected {
353
+ background: #f5f5f5;
354
+ color: #666;
355
+ }
356
+
348
357
  /* Empty States */
349
358
  .no-data {
350
359
  text-align: center;
@@ -20,6 +20,7 @@ class ShellMirrorDashboard {
20
20
  this.user = authStatus.user;
21
21
  await this.loadDashboardData();
22
22
  this.renderAuthenticatedDashboard();
23
+ this.startAutoRefresh(); // Start auto-refresh for authenticated users
23
24
  } else {
24
25
  this.renderUnauthenticatedDashboard();
25
26
  }
@@ -31,6 +32,24 @@ class ShellMirrorDashboard {
31
32
  }
32
33
  }
33
34
 
35
+ startAutoRefresh() {
36
+ // Refresh agent data every 30 seconds to detect disconnected agents
37
+ this.refreshInterval = setInterval(async () => {
38
+ if (this.isAuthenticated) {
39
+ await this.loadDashboardData();
40
+ // Only re-render the agents section to avoid full page flash
41
+ this.updateAgentsDisplay();
42
+ }
43
+ }, 30000);
44
+ }
45
+
46
+ updateAgentsDisplay() {
47
+ const agentsCard = document.querySelector('.dashboard-card');
48
+ if (agentsCard) {
49
+ agentsCard.innerHTML = this.renderActiveAgents();
50
+ }
51
+ }
52
+
34
53
  showLoading() {
35
54
  document.getElementById('loading-overlay').style.display = 'flex';
36
55
  }
@@ -148,8 +167,14 @@ class ShellMirrorDashboard {
148
167
  }
149
168
 
150
169
  renderActiveAgents() {
151
- const agentCount = this.agents.length;
152
- const agentsHtml = this.agents.map(agent => `
170
+ // Filter for recently active agents (online or seen within last 5 minutes)
171
+ const activeAgents = this.agents.filter(agent => {
172
+ const timeSinceLastSeen = Date.now() / 1000 - agent.lastSeen;
173
+ return agent.onlineStatus === 'online' || timeSinceLastSeen < 300; // 5 minutes
174
+ });
175
+
176
+ const agentCount = activeAgents.length;
177
+ const agentsHtml = activeAgents.map(agent => `
153
178
  <div class="agent-item">
154
179
  <div class="agent-info">
155
180
  <div class="agent-name">${agent.machineName || agent.agentId}</div>
@@ -200,8 +225,31 @@ class ShellMirrorDashboard {
200
225
  }
201
226
 
202
227
  renderRecentSessions() {
203
- const sessionsHtml = this.sessions.map(session => `
204
- <div class="session-item">
228
+ // Get inactive agents (not seen in last 5 minutes and offline)
229
+ const inactiveAgents = this.agents.filter(agent => {
230
+ const timeSinceLastSeen = Date.now() / 1000 - agent.lastSeen;
231
+ return agent.onlineStatus === 'offline' && timeSinceLastSeen >= 300; // More than 5 minutes
232
+ });
233
+
234
+ // Combine real sessions with inactive agents as past connections
235
+ const allSessions = [
236
+ // Add inactive agents as past connections
237
+ ...inactiveAgents.map(agent => ({
238
+ type: 'past_agent',
239
+ agentId: agent.machineName || agent.agentId,
240
+ startTime: new Date(agent.lastSeen * 1000),
241
+ duration: 'Last connection',
242
+ status: 'disconnected'
243
+ })),
244
+ // Add actual session history
245
+ ...this.sessions
246
+ ];
247
+
248
+ // Sort by most recent
249
+ allSessions.sort((a, b) => b.startTime - a.startTime);
250
+
251
+ const sessionsHtml = allSessions.map(session => `
252
+ <div class="session-item compact">
205
253
  <div class="session-info">
206
254
  <div class="session-agent">${session.agentId}</div>
207
255
  <div class="session-time">${this.formatDate(session.startTime)}</div>
@@ -217,7 +265,7 @@ class ShellMirrorDashboard {
217
265
  <div class="dashboard-card full-width">
218
266
  <h2>📊 Recent Sessions</h2>
219
267
  <div class="card-content">
220
- ${this.sessions.length > 0 ? sessionsHtml : '<p class="no-data">No recent sessions</p>'}
268
+ ${allSessions.length > 0 ? sessionsHtml : '<p class="no-data">No recent sessions</p>'}
221
269
  </div>
222
270
  </div>
223
271
  `;
@@ -35,7 +35,7 @@
35
35
  .back-to-dashboard {
36
36
  position: fixed;
37
37
  top: 20px;
38
- left: 20px;
38
+ right: 20px;
39
39
  background: rgba(66, 133, 244, 0.9);
40
40
  color: white;
41
41
  border: none;
@@ -55,11 +55,37 @@
55
55
  background: rgba(51, 103, 214, 0.9);
56
56
  transform: translateY(-1px);
57
57
  }
58
+
59
+ /* Connection Status Indicator */
60
+ .connection-status {
61
+ width: 8px;
62
+ height: 8px;
63
+ border-radius: 50%;
64
+ background: #ff4444;
65
+ margin-right: 4px;
66
+ transition: all 0.3s ease;
67
+ }
68
+
69
+ .connection-status.connected {
70
+ background: #44ff44;
71
+ box-shadow: 0 0 8px rgba(68, 255, 68, 0.5);
72
+ }
73
+
74
+ .connection-status.connecting {
75
+ background: #ffaa44;
76
+ animation: pulse 1.5s ease-in-out infinite alternate;
77
+ }
78
+
79
+ @keyframes pulse {
80
+ from { opacity: 1; }
81
+ to { opacity: 0.4; }
82
+ }
58
83
  </style>
59
84
  </head>
60
85
  <body>
61
86
  <!-- Back to Dashboard Button -->
62
- <a href="/app/dashboard.html" class="back-to-dashboard">
87
+ <a href="/app/dashboard.html" class="back-to-dashboard" id="dashboard-btn">
88
+ <div class="connection-status" id="connection-status"></div>
63
89
  <span>←</span>
64
90
  <span>Dashboard</span>
65
91
  </a>
@@ -50,6 +50,26 @@ let AGENT_ID;
50
50
  let CLIENT_ID;
51
51
  let SELECTED_AGENT; // Store full agent data including WebSocket URL
52
52
 
53
+ // Connection status management
54
+ function updateConnectionStatus(status) {
55
+ const statusElement = document.getElementById('connection-status');
56
+ if (!statusElement) return;
57
+
58
+ statusElement.className = 'connection-status';
59
+ switch(status) {
60
+ case 'connecting':
61
+ statusElement.classList.add('connecting');
62
+ break;
63
+ case 'connected':
64
+ statusElement.classList.add('connected');
65
+ break;
66
+ case 'disconnected':
67
+ default:
68
+ // Default red styling already applied
69
+ break;
70
+ }
71
+ }
72
+
53
73
  // Check for agent parameter and connect directly
54
74
  window.addEventListener('load', () => {
55
75
  loadVersionInfo();
@@ -97,6 +117,7 @@ async function loadVersionInfo() {
97
117
 
98
118
 
99
119
  function startConnection() {
120
+ updateConnectionStatus('connecting');
100
121
  connectContainer.style.display = 'none';
101
122
  terminalContainer.style.display = 'block';
102
123
  term.open(document.getElementById('terminal'));
@@ -346,25 +367,31 @@ async function createPeerConnection() {
346
367
  break;
347
368
  case 'connected':
348
369
  console.log('[CLIENT] ✅ WebRTC connection established!');
370
+ updateConnectionStatus('connected');
349
371
  break;
350
372
  case 'completed':
351
373
  console.log('[CLIENT] ✅ ICE connection completed successfully!');
374
+ updateConnectionStatus('connected');
352
375
  break;
353
376
  case 'failed':
354
377
  console.log('[CLIENT] ❌ ICE connection failed - no viable candidates');
355
378
  console.log('[CLIENT] 💡 Troubleshooting: This may be due to firewall/NAT issues or blocked STUN servers');
379
+ updateConnectionStatus('disconnected');
356
380
  term.write('\r\n\r\n❌ Connection failed: Network connectivity issues\r\n');
357
381
  term.write('💡 This may be due to:\r\n');
358
382
  term.write(' • Firewall blocking WebRTC traffic\r\n');
359
383
  term.write(' • Corporate network restrictions\r\n');
360
384
  term.write(' • STUN/TURN servers unreachable\r\n');
361
- term.write('\r\n🔄 Please refresh the page to retry...\r\n');
385
+ term.write(' Agent may have crashed or disconnected\r\n');
386
+ term.write('\r\n🔄 Click Dashboard to return and try another agent\r\n');
362
387
  break;
363
388
  case 'disconnected':
364
389
  console.log('[CLIENT] ⚠️ ICE connection disconnected');
390
+ updateConnectionStatus('disconnected');
365
391
  break;
366
392
  case 'closed':
367
393
  console.log('[CLIENT] 🔐 ICE connection closed');
394
+ updateConnectionStatus('disconnected');
368
395
  break;
369
396
  }
370
397
  };
@@ -440,12 +467,16 @@ function setupDataChannel() {
440
467
 
441
468
  dataChannel.onclose = () => {
442
469
  console.log('[CLIENT] Data channel closed.');
470
+ updateConnectionStatus('disconnected');
443
471
  term.write('\r\n\r\n\x1b[31m❌ Terminal session ended.\x1b[0m\r\n');
472
+ term.write('🔄 Click Dashboard to return and start a new session\r\n');
444
473
  };
445
474
 
446
475
  dataChannel.onerror = (error) => {
447
476
  console.error('[CLIENT] Data channel error:', error);
477
+ updateConnectionStatus('disconnected');
448
478
  term.write('\r\n\r\n\x1b[31m❌ Data channel error occurred.\x1b[0m\r\n');
479
+ term.write('🔄 Click Dashboard to return and try again\r\n');
449
480
  };
450
481
 
451
482
  term.onData((data) => {