shell-mirror 1.5.58 โ†’ 1.5.59

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.58",
3
+ "version": "1.5.59",
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": {
@@ -183,6 +183,28 @@ body {
183
183
  animation: spin 1s linear infinite;
184
184
  }
185
185
 
186
+ /* Cleanup Button */
187
+ .cleanup-btn-inline {
188
+ background: #ffebee;
189
+ border: 1px solid #f48fb1;
190
+ border-radius: 50%;
191
+ color: #d32f2f;
192
+ width: 32px;
193
+ height: 32px;
194
+ cursor: pointer;
195
+ transition: all 0.2s ease;
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ font-size: 0.9rem;
200
+ }
201
+
202
+ .cleanup-btn-inline:hover:not(:disabled) {
203
+ background: #f8bbd9;
204
+ color: #b71c1c;
205
+ transform: scale(1.05);
206
+ }
207
+
186
208
  .connection-status {
187
209
  font-size: 0.8rem;
188
210
  font-weight: 500;
@@ -279,6 +301,12 @@ body {
279
301
  gap: 12px;
280
302
  }
281
303
 
304
+ .agent-actions-header {
305
+ display: flex;
306
+ align-items: center;
307
+ gap: 8px;
308
+ }
309
+
282
310
  .card-header h2 {
283
311
  font-size: 1.3rem;
284
312
  font-weight: 600;
@@ -335,6 +363,11 @@ body {
335
363
  color: #2e7d32;
336
364
  }
337
365
 
366
+ .agent-status.recent {
367
+ background: #fff3e0;
368
+ color: #f57c00;
369
+ }
370
+
338
371
  .agent-status.offline {
339
372
  background: #ffebee;
340
373
  color: #c62828;
@@ -393,6 +426,24 @@ body {
393
426
  transform: translateY(-1px);
394
427
  }
395
428
 
429
+ /* Disabled/Offline Agent Styles */
430
+ .agent-item.agent-offline {
431
+ opacity: 0.7;
432
+ }
433
+
434
+ .btn-disabled {
435
+ background: #f5f5f5 !important;
436
+ color: #999 !important;
437
+ cursor: not-allowed !important;
438
+ border: 1px solid #e0e0e0 !important;
439
+ }
440
+
441
+ .btn-disabled:hover {
442
+ background: #f5f5f5 !important;
443
+ transform: none !important;
444
+ color: #999 !important;
445
+ }
446
+
396
447
  /* Quick Actions */
397
448
  .action-buttons {
398
449
  display: flex;
@@ -889,4 +940,55 @@ body {
889
940
 
890
941
  .api-error button {
891
942
  margin-top: 10px;
943
+ }
944
+
945
+ /* Connection Testing States */
946
+ .btn-connect.testing {
947
+ background: #ffc107 !important;
948
+ color: #333 !important;
949
+ cursor: wait !important;
950
+ }
951
+
952
+ /* Connection Error Notifications */
953
+ .connection-error-notification {
954
+ position: fixed;
955
+ top: 120px;
956
+ right: 20px;
957
+ background: #fff;
958
+ border-left: 4px solid #f44336;
959
+ border-radius: 8px;
960
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
961
+ padding: 16px 20px;
962
+ max-width: 350px;
963
+ z-index: 3000;
964
+ animation: slideInRight 0.3s ease-out;
965
+ display: flex;
966
+ align-items: flex-start;
967
+ gap: 12px;
968
+ }
969
+
970
+ .connection-error-notification .error-content {
971
+ flex: 1;
972
+ font-size: 0.9rem;
973
+ line-height: 1.4;
974
+ }
975
+
976
+ .connection-error-notification button {
977
+ background: none;
978
+ border: none;
979
+ font-size: 1.2rem;
980
+ cursor: pointer;
981
+ color: #999;
982
+ padding: 0;
983
+ width: 20px;
984
+ height: 20px;
985
+ display: flex;
986
+ align-items: center;
987
+ justify-content: center;
988
+ border-radius: 50%;
989
+ }
990
+
991
+ .connection-error-notification button:hover {
992
+ background: #f5f5f5;
993
+ color: #333;
892
994
  }
@@ -537,30 +537,62 @@ class ShellMirrorDashboard {
537
537
  }
538
538
 
539
539
  renderActiveAgents() {
540
- // Filter for recently active agents (online or seen within last 5 minutes)
541
- const activeAgents = this.agents.filter(agent => {
542
- const timeSinceLastSeen = Date.now() / 1000 - agent.lastSeen;
543
- return agent.onlineStatus === 'online' || timeSinceLastSeen < 300; // 5 minutes
540
+ // Filter for agents that should be displayed (online, recent, or offline with sessions)
541
+ const displayAgents = this.agents.filter(agent => {
542
+ // Always show online agents
543
+ if (agent.status === 'online') return true;
544
+
545
+ // Show recent agents (last 2 minutes)
546
+ if (agent.status === 'recent') return true;
547
+
548
+ // Show offline agents only if they have active sessions
549
+ const sessions = this.agentSessions[agent.agentId] || [];
550
+ const activeSessions = sessions.filter(s => s.status === 'active');
551
+ return activeSessions.length > 0;
544
552
  });
553
+
554
+ // Separate truly active vs inactive agents
555
+ const activeAgents = displayAgents.filter(agent => agent.status === 'online' || agent.status === 'recent');
545
556
 
546
- const agentCount = activeAgents.length;
547
- const agentsHtml = activeAgents.map(agent => {
557
+ const agentCount = displayAgents.length;
558
+ const agentsHtml = displayAgents.map(agent => {
548
559
  const sessions = this.agentSessions[agent.agentId] || [];
549
560
  const sessionCount = sessions.length;
550
561
 
562
+ const isConnectable = agent.status === 'online' || agent.status === 'recent';
563
+ const statusIcon = {
564
+ 'online': '๐ŸŸข',
565
+ 'recent': '๐ŸŸก',
566
+ 'offline': '๐Ÿ”ด'
567
+ }[agent.status] || 'โ“';
568
+
569
+ const statusText = {
570
+ 'online': 'Live',
571
+ 'recent': 'Recent',
572
+ 'offline': 'Offline'
573
+ }[agent.status] || agent.status;
574
+
575
+ const lastSeenText = agent.timeSinceLastSeen !== undefined
576
+ ? this.formatPreciseLastSeen(agent.timeSinceLastSeen)
577
+ : this.formatLastSeen(agent.lastSeen);
578
+
551
579
  return `
552
- <div class="agent-item">
580
+ <div class="agent-item ${!isConnectable ? 'agent-offline' : ''}">
553
581
  <div class="agent-info">
554
582
  <div class="agent-name">${agent.machineName || agent.agentId}</div>
555
- <div class="agent-status ${agent.onlineStatus}">${agent.onlineStatus}</div>
556
- <div class="agent-last-seen">Last seen: ${this.formatLastSeen(agent.lastSeen)}</div>
583
+ <div class="agent-status ${agent.status}">
584
+ ${statusIcon} ${statusText}
585
+ </div>
586
+ <div class="agent-last-seen">Last seen: ${lastSeenText}</div>
557
587
  ${sessionCount > 0 ? `<div class="agent-sessions">${sessionCount} active session${sessionCount !== 1 ? 's' : ''}</div>` : ''}
558
588
  </div>
559
589
  <div class="agent-actions">
560
- <button class="btn-connect" onclick="dashboard.connectToAgent('${agent.agentId}')">
561
- ${sessionCount > 0 ? 'Resume Session' : 'New Session'}
590
+ <button class="btn-connect ${!isConnectable ? 'btn-disabled' : ''}"
591
+ onclick="dashboard.connectToAgent('${agent.agentId}')"
592
+ ${!isConnectable ? 'disabled' : ''}>
593
+ ${!isConnectable ? 'Offline' : sessionCount > 0 ? 'Resume Session' : 'New Session'}
562
594
  </button>
563
- ${sessionCount > 0 ? `<button class="btn-sessions" onclick="dashboard.showAgentSessions('${agent.agentId}')">
595
+ ${sessionCount > 0 && isConnectable ? `<button class="btn-sessions" onclick="dashboard.showAgentSessions('${agent.agentId}')">
564
596
  All Sessions
565
597
  </button>` : ''}
566
598
  </div>
@@ -568,6 +600,10 @@ class ShellMirrorDashboard {
568
600
  `;
569
601
  }).join('');
570
602
 
603
+ // Check if there are any offline agents to show cleanup option
604
+ const offlineAgents = this.agents.filter(agent => agent.status === 'offline');
605
+ const showCleanup = offlineAgents.length > 0;
606
+
571
607
  return `
572
608
  <div class="dashboard-card">
573
609
  <div class="card-header">
@@ -575,9 +611,14 @@ class ShellMirrorDashboard {
575
611
  <h2>๐Ÿ–ฅ๏ธ Active Agents</h2>
576
612
  <span class="agent-count">${agentCount} agent${agentCount !== 1 ? 's' : ''}</span>
577
613
  </div>
578
- <button id="refresh-btn" class="refresh-btn-inline" onclick="dashboard.manualRefresh()" title="Refresh agents">
579
- <span class="refresh-icon">๐Ÿ”„</span>
580
- </button>
614
+ <div class="agent-actions-header">
615
+ <button id="refresh-btn" class="refresh-btn-inline" onclick="dashboard.manualRefresh()" title="Refresh agents">
616
+ <span class="refresh-icon">๐Ÿ”„</span>
617
+ </button>
618
+ ${showCleanup ? `<button class="cleanup-btn-inline" onclick="dashboard.cleanupOfflineAgents()" title="Remove offline agents">
619
+ <span>๐Ÿงน</span>
620
+ </button>` : ''}
621
+ </div>
581
622
  </div>
582
623
  <div class="card-content">
583
624
  ${agentCount > 0 ? agentsHtml : '<p class="no-data">No active agents. <a href="#" onclick="dashboard.showAgentInstructions()">Set up an agent</a></p>'}
@@ -749,6 +790,16 @@ class ShellMirrorDashboard {
749
790
  return `${Math.floor(diff / 86400)} days ago`;
750
791
  }
751
792
 
793
+ formatPreciseLastSeen(timeSinceLastSeen) {
794
+ if (!timeSinceLastSeen && timeSinceLastSeen !== 0) return 'Unknown';
795
+
796
+ if (timeSinceLastSeen < 10) return 'Just now';
797
+ if (timeSinceLastSeen < 60) return `${Math.floor(timeSinceLastSeen)} seconds ago`;
798
+ if (timeSinceLastSeen < 3600) return `${Math.floor(timeSinceLastSeen / 60)} minutes ago`;
799
+ if (timeSinceLastSeen < 86400) return `${Math.floor(timeSinceLastSeen / 3600)} hours ago`;
800
+ return `${Math.floor(timeSinceLastSeen / 86400)} days ago`;
801
+ }
802
+
752
803
  formatDate(date) {
753
804
  return new Intl.DateTimeFormat('en-US', {
754
805
  month: 'short',
@@ -782,9 +833,109 @@ class ShellMirrorDashboard {
782
833
  }
783
834
  }
784
835
 
836
+ showAgentConnectionTest(agentId, status) {
837
+ // Find the agent item in the DOM and show loading state
838
+ const agentItems = document.querySelectorAll('.agent-item');
839
+ agentItems.forEach(item => {
840
+ const connectBtn = item.querySelector('.btn-connect');
841
+ if (connectBtn && connectBtn.onclick.toString().includes(agentId)) {
842
+ if (status === 'testing') {
843
+ connectBtn.textContent = 'Testing...';
844
+ connectBtn.disabled = true;
845
+ connectBtn.classList.add('testing');
846
+ } else if (status === 'done') {
847
+ connectBtn.disabled = false;
848
+ connectBtn.classList.remove('testing');
849
+ // Original text will be restored on next refresh
850
+ }
851
+ }
852
+ });
853
+ }
854
+
855
+ showConnectionError(agent, message) {
856
+ // Show a more user-friendly error message
857
+ const notification = document.createElement('div');
858
+ notification.className = 'connection-error-notification';
859
+ notification.innerHTML = `
860
+ <div class="error-content">
861
+ <strong>Connection Failed</strong><br>
862
+ Agent: ${agent.machineName || agent.agentId}<br>
863
+ ${message}
864
+ </div>
865
+ <button onclick="this.parentElement.remove()">ร—</button>
866
+ `;
867
+ document.body.appendChild(notification);
868
+
869
+ // Auto-remove after 10 seconds
870
+ setTimeout(() => {
871
+ if (document.body.contains(notification)) {
872
+ document.body.removeChild(notification);
873
+ }
874
+ }, 10000);
875
+ }
876
+
877
+ async testAgentConnectivity(agentId) {
878
+ try {
879
+ console.log('[DASHBOARD] ๐Ÿ” Testing connectivity for agent:', agentId);
880
+
881
+ // Try to ping the agent through the API
882
+ const response = await fetch(`/php-backend/api/ping-agent.php?agentId=${encodeURIComponent(agentId)}`, {
883
+ method: 'GET',
884
+ credentials: 'include',
885
+ timeout: 5000 // 5 second timeout
886
+ });
887
+
888
+ if (response.ok) {
889
+ const data = await response.json();
890
+ console.log('[DASHBOARD] ๐Ÿ” Ping response:', data);
891
+ return data.success && data.reachable;
892
+ }
893
+
894
+ console.log('[DASHBOARD] โš ๏ธ Ping request failed:', response.status);
895
+ return false;
896
+ } catch (error) {
897
+ console.log('[DASHBOARD] โš ๏ธ Agent connectivity test failed:', error);
898
+ return false;
899
+ }
900
+ }
901
+
785
902
  // Action handlers
786
903
  async connectToAgent(agentId) {
787
904
  console.log('[DASHBOARD] ๐Ÿ” DEBUG: connectToAgent called with agentId:', agentId);
905
+
906
+ // First, test if agent is actually reachable
907
+ const agent = this.agents.find(a => a.agentId === agentId);
908
+ if (!agent) {
909
+ alert('Agent not found. Please refresh and try again.');
910
+ return;
911
+ }
912
+
913
+ // Check agent status
914
+ if (agent.status === 'offline') {
915
+ alert(`Agent "${agent.machineName || agentId}" is offline. Please ensure the agent is running.`);
916
+ return;
917
+ }
918
+
919
+ // Test agent connectivity for recent agents with loading indicator
920
+ if (agent.status === 'recent') {
921
+ console.log('[DASHBOARD] ๐Ÿ” Testing agent connectivity...');
922
+
923
+ // Show loading state on the specific agent
924
+ this.showAgentConnectionTest(agentId, 'testing');
925
+
926
+ const isReachable = await this.testAgentConnectivity(agentId);
927
+
928
+ // Hide loading state
929
+ this.showAgentConnectionTest(agentId, 'done');
930
+
931
+ if (!isReachable) {
932
+ this.showConnectionError(agent, 'Connection test failed - agent may be offline');
933
+ // Refresh agent list to get updated status
934
+ await this.refreshDashboardData();
935
+ return;
936
+ }
937
+ }
938
+
788
939
  console.log('[DASHBOARD] ๐Ÿ” DEBUG: Current agentSessions:', this.agentSessions);
789
940
 
790
941
  // Check if there are existing sessions for this agent
@@ -860,16 +1011,15 @@ class ShellMirrorDashboard {
860
1011
  }
861
1012
 
862
1013
  startNewSession() {
863
- // Get first available agent for new session
864
- const activeAgents = this.agents.filter(agent => {
865
- const timeSinceLastSeen = Date.now() / 1000 - agent.lastSeen;
866
- return agent.onlineStatus === 'online' || timeSinceLastSeen < 300;
867
- });
1014
+ // Get first available agent for new session (only truly active ones)
1015
+ const activeAgents = this.agents.filter(agent =>
1016
+ agent.status === 'online' || agent.status === 'recent'
1017
+ );
868
1018
 
869
1019
  if (activeAgents.length > 0) {
870
1020
  this.connectToAgent(activeAgents[0].agentId);
871
1021
  } else {
872
- alert('No active agents available. Please ensure an agent is running on your Mac.');
1022
+ alert('No active agents available. Please ensure an agent is running on your Mac and try refreshing.');
873
1023
  }
874
1024
  }
875
1025
 
@@ -990,6 +1140,76 @@ class ShellMirrorDashboard {
990
1140
  }
991
1141
  }
992
1142
 
1143
+ async cleanupOfflineAgents() {
1144
+ try {
1145
+ console.log('[DASHBOARD] ๐Ÿงน Cleaning up offline agents...');
1146
+
1147
+ const response = await fetch('/php-backend/api/cleanup-agents.php', {
1148
+ method: 'POST',
1149
+ credentials: 'include'
1150
+ });
1151
+
1152
+ if (response.ok) {
1153
+ const data = await response.json();
1154
+ console.log('[DASHBOARD] โœ… Cleanup response:', data);
1155
+
1156
+ if (data.success) {
1157
+ // Show success message
1158
+ this.showCleanupResult(data.data.message);
1159
+
1160
+ // Refresh the agent list
1161
+ await this.refreshDashboardData();
1162
+ } else {
1163
+ this.showCleanupError('Cleanup failed: ' + (data.message || 'Unknown error'));
1164
+ }
1165
+ } else {
1166
+ this.showCleanupError('Cleanup request failed: ' + response.status);
1167
+ }
1168
+ } catch (error) {
1169
+ console.error('[DASHBOARD] โŒ Cleanup failed:', error);
1170
+ this.showCleanupError('Cleanup failed: ' + error.message);
1171
+ }
1172
+ }
1173
+
1174
+ showCleanupResult(message) {
1175
+ const notification = document.createElement('div');
1176
+ notification.className = 'cleanup-result-notification';
1177
+ notification.innerHTML = `
1178
+ <div class="result-content">
1179
+ <strong>โœ… Cleanup Complete</strong><br>
1180
+ ${message}
1181
+ </div>
1182
+ <button onclick="this.parentElement.remove()">ร—</button>
1183
+ `;
1184
+ document.body.appendChild(notification);
1185
+
1186
+ // Auto-remove after 5 seconds
1187
+ setTimeout(() => {
1188
+ if (document.body.contains(notification)) {
1189
+ document.body.removeChild(notification);
1190
+ }
1191
+ }, 5000);
1192
+ }
1193
+
1194
+ showCleanupError(message) {
1195
+ const notification = document.createElement('div');
1196
+ notification.className = 'connection-error-notification';
1197
+ notification.innerHTML = `
1198
+ <div class="error-content">
1199
+ <strong>Cleanup Failed</strong><br>
1200
+ ${message}
1201
+ </div>
1202
+ <button onclick="this.parentElement.remove()">ร—</button>
1203
+ `;
1204
+ document.body.appendChild(notification);
1205
+
1206
+ // Auto-remove after 8 seconds
1207
+ setTimeout(() => {
1208
+ if (document.body.contains(notification)) {
1209
+ document.body.removeChild(notification);
1210
+ }
1211
+ }, 8000);
1212
+ }
993
1213
 
994
1214
  showAgentInstructions() {
995
1215
  // TODO: Show modal with agent setup instructions