shell-mirror 1.5.92 → 1.5.93

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.92",
3
+ "version": "1.5.93",
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": {
@@ -521,6 +521,33 @@ body {
521
521
  text-align: center;
522
522
  }
523
523
 
524
+ .agent-name-row {
525
+ display: flex;
526
+ align-items: center;
527
+ gap: 8px;
528
+ }
529
+
530
+ .btn-delete-agent {
531
+ background: none;
532
+ border: none;
533
+ cursor: pointer;
534
+ font-size: 0.9rem;
535
+ opacity: 0.4;
536
+ transition: opacity 0.2s ease;
537
+ padding: 2px 4px;
538
+ }
539
+
540
+ .btn-delete-agent:hover {
541
+ opacity: 1;
542
+ }
543
+
544
+ .agent-session-count {
545
+ font-weight: normal;
546
+ font-size: 0.7rem;
547
+ color: #666;
548
+ margin-left: 6px;
549
+ }
550
+
524
551
  .agent-actions {
525
552
  display: flex;
526
553
  gap: 8px;
@@ -587,9 +587,15 @@ class ShellMirrorDashboard {
587
587
  <div class="agent-item ${!isConnectable ? 'agent-offline' : ''}">
588
588
  <div class="agent-header">
589
589
  <div class="agent-info">
590
- <div class="agent-name">${agent.machineName || agent.agentId}</div>
590
+ <div class="agent-name-row">
591
+ <span class="agent-name">${agent.machineName || agent.agentId}</span>
592
+ <button class="btn-delete-agent" onclick="dashboard.deleteAgent('${agent.agentId}')" title="Remove agent">
593
+ 🗑️
594
+ </button>
595
+ </div>
591
596
  <div class="agent-status ${agent.status}">
592
597
  ${statusIcon} ${statusText}
598
+ ${sessionCount > 0 ? `<span class="agent-session-count">(${sessionCount} session${sessionCount !== 1 ? 's' : ''})</span>` : ''}
593
599
  </div>
594
600
  <div class="agent-last-seen">Last seen: ${lastSeenText}</div>
595
601
  </div>
@@ -597,7 +603,7 @@ class ShellMirrorDashboard {
597
603
  ${isConnectable ? `
598
604
  <div class="agent-sessions-inline">
599
605
  ${sessionCount > 0 ? `
600
- <div class="sessions-label">Sessions (${sessionCount})</div>
606
+ <div class="sessions-label">Active Sessions</div>
601
607
  <div class="sessions-list">
602
608
  ${sessionsHtml}
603
609
  </div>
@@ -1033,6 +1039,10 @@ class ShellMirrorDashboard {
1033
1039
  }
1034
1040
 
1035
1041
  async connectToSession(agentId, sessionId) {
1042
+ // Validate agent is reachable before connecting
1043
+ const isReachable = await this.validateAgentBeforeConnect(agentId);
1044
+ if (!isReachable) return;
1045
+
1036
1046
  // Track specific session connection in Google Analytics
1037
1047
  if (typeof sendGAEvent === 'function') {
1038
1048
  sendGAEvent('terminal_connect', {
@@ -1042,14 +1052,18 @@ class ShellMirrorDashboard {
1042
1052
  session_id: sessionId
1043
1053
  });
1044
1054
  }
1045
-
1046
- window.location.href = `/app/terminal.html?agent=${agentId}&session=${sessionId}`;
1055
+
1056
+ // Open in new browser tab so user can return to dashboard
1057
+ window.open(`/app/terminal.html?agent=${agentId}&session=${sessionId}`, '_blank');
1047
1058
  }
1048
1059
 
1049
1060
  async createNewSession(agentId) {
1050
- // Force creation of new session by not passing session parameter
1061
+ // Validate agent is reachable before connecting
1062
+ const isReachable = await this.validateAgentBeforeConnect(agentId);
1063
+ if (!isReachable) return;
1064
+
1051
1065
  console.log(`[DASHBOARD] Creating new session for agent: ${agentId}`);
1052
-
1066
+
1053
1067
  // Track explicit new session creation in Google Analytics
1054
1068
  if (typeof sendGAEvent === 'function') {
1055
1069
  sendGAEvent('terminal_connect', {
@@ -1058,8 +1072,61 @@ class ShellMirrorDashboard {
1058
1072
  agent_id: agentId
1059
1073
  });
1060
1074
  }
1061
-
1062
- window.location.href = `/app/terminal.html?agent=${agentId}`;
1075
+
1076
+ // Open in new browser tab so user can return to dashboard
1077
+ window.open(`/app/terminal.html?agent=${agentId}&newSession=true`, '_blank');
1078
+ }
1079
+
1080
+ async validateAgentBeforeConnect(agentId) {
1081
+ const agent = this.agents.find(a => a.agentId === agentId);
1082
+ if (!agent) {
1083
+ alert('Agent not found. Please refresh and try again.');
1084
+ return false;
1085
+ }
1086
+
1087
+ // Always test connectivity before connecting (not just for 'recent')
1088
+ console.log('[DASHBOARD] 🔍 Validating agent connectivity...');
1089
+ this.showAgentConnectionTest(agentId, 'testing');
1090
+
1091
+ const isReachable = await this.testAgentConnectivity(agentId);
1092
+ this.showAgentConnectionTest(agentId, 'done');
1093
+
1094
+ if (!isReachable) {
1095
+ this.showConnectionError(agent, 'Agent is not reachable. It may be offline or disconnected.');
1096
+ await this.refreshDashboardData();
1097
+ return false;
1098
+ }
1099
+
1100
+ return true;
1101
+ }
1102
+
1103
+ async deleteAgent(agentId) {
1104
+ const agent = this.agents.find(a => a.agentId === agentId);
1105
+ const agentName = agent ? (agent.machineName || agentId) : agentId;
1106
+
1107
+ if (!confirm(`Are you sure you want to remove "${agentName}" from the dashboard?\n\nThis will unregister the agent. If it's still running, it will re-register on next heartbeat.`)) {
1108
+ return;
1109
+ }
1110
+
1111
+ try {
1112
+ const response = await fetch('/php-backend/api/delete-agent.php', {
1113
+ method: 'POST',
1114
+ headers: { 'Content-Type': 'application/json' },
1115
+ credentials: 'include',
1116
+ body: JSON.stringify({ agentId })
1117
+ });
1118
+
1119
+ const data = await response.json();
1120
+ if (data.success) {
1121
+ console.log('[DASHBOARD] Agent deleted:', agentId);
1122
+ await this.refreshDashboardData();
1123
+ } else {
1124
+ alert('Failed to remove agent: ' + (data.message || 'Unknown error'));
1125
+ }
1126
+ } catch (error) {
1127
+ console.error('[DASHBOARD] Delete agent failed:', error);
1128
+ alert('Failed to remove agent: ' + error.message);
1129
+ }
1063
1130
  }
1064
1131
 
1065
1132
  startNewSession() {
@@ -153,17 +153,17 @@
153
153
  .session-tab {
154
154
  display: flex;
155
155
  align-items: center;
156
- gap: 6px;
157
- padding: 8px 12px;
156
+ gap: 4px;
157
+ padding: 6px 8px;
158
158
  background: transparent;
159
159
  border: none;
160
160
  border-bottom: 3px solid transparent;
161
- color: #999;
162
- cursor: pointer;
163
- min-width: 120px;
161
+ color: #888;
162
+ min-width: 100px;
164
163
  white-space: nowrap;
165
164
  transition: all 0.2s ease;
166
165
  flex-shrink: 0;
166
+ border-radius: 6px 6px 0 0;
167
167
  }
168
168
 
169
169
  .session-tab:hover {
@@ -175,7 +175,40 @@
175
175
  color: #fff;
176
176
  font-weight: 600;
177
177
  border-bottom-color: #667eea;
178
- background: #1a1a1a;
178
+ background: #333;
179
+ box-shadow: inset 0 -3px 0 #667eea;
180
+ }
181
+
182
+ .session-tab-btn {
183
+ background: none;
184
+ border: none;
185
+ color: inherit;
186
+ font: inherit;
187
+ cursor: pointer;
188
+ padding: 2px 4px;
189
+ }
190
+
191
+ .session-tab-close {
192
+ background: none;
193
+ border: none;
194
+ color: #666;
195
+ font-size: 1.1rem;
196
+ cursor: pointer;
197
+ padding: 0 4px;
198
+ line-height: 1;
199
+ border-radius: 3px;
200
+ opacity: 0;
201
+ transition: all 0.2s ease;
202
+ }
203
+
204
+ .session-tab:hover .session-tab-close {
205
+ opacity: 0.7;
206
+ }
207
+
208
+ .session-tab-close:hover {
209
+ opacity: 1 !important;
210
+ background: rgba(255, 100, 100, 0.3);
211
+ color: #ff6b6b;
179
212
  }
180
213
 
181
214
  .session-tab-name {
@@ -411,6 +411,9 @@ function setupDirectConnection(directWs) {
411
411
  term.clear();
412
412
  term.write(`\r\n\x1b[36m✨ New session created: ${currentSession.name}\x1b[0m\r\n\r\n`);
413
413
 
414
+ // Update URL with session ID so refresh reconnects to same session
415
+ updateUrlWithSession(data.sessionId);
416
+
414
417
  // Update UI
415
418
  updateSessionDisplay();
416
419
 
@@ -995,12 +998,14 @@ function renderTabs() {
995
998
  const displayName = session.name || 'Terminal Session';
996
999
 
997
1000
  return `
998
- <button class="session-tab ${isActive ? 'active' : ''}"
999
- onclick="switchToSession('${session.id}')"
1000
- ${isActive ? 'disabled' : ''}
1001
- title="${displayName}">
1002
- <span class="session-tab-name">${displayName}</span>
1003
- </button>
1001
+ <div class="session-tab ${isActive ? 'active' : ''}" title="${displayName}">
1002
+ <button class="session-tab-btn"
1003
+ onclick="switchToSession('${session.id}')"
1004
+ ${isActive ? '' : ''}>
1005
+ <span class="session-tab-name">${displayName}</span>
1006
+ </button>
1007
+ <button class="session-tab-close" onclick="closeSession('${session.id}', event)" title="Close session">×</button>
1008
+ </div>
1004
1009
  `;
1005
1010
  }).join('');
1006
1011
  }
@@ -1012,6 +1017,56 @@ function renderTabs() {
1012
1017
  console.log('[CLIENT] ✅ Tabs rendered:', sessionsToRender.length, 'tabs');
1013
1018
  }
1014
1019
 
1020
+ // Update URL with current session ID so refresh reconnects
1021
+ function updateUrlWithSession(sessionId) {
1022
+ const url = new URL(window.location.href);
1023
+ url.searchParams.set('session', sessionId);
1024
+ window.history.replaceState({}, '', url.toString());
1025
+ console.log('[CLIENT] 📍 URL updated with session:', sessionId);
1026
+ }
1027
+
1028
+ // Close a session with confirmation
1029
+ function closeSession(sessionId, event) {
1030
+ event.stopPropagation(); // Don't trigger tab switch
1031
+
1032
+ const session = availableSessions.find(s => s.id === sessionId);
1033
+ const sessionName = session?.name || 'this session';
1034
+
1035
+ if (!confirm(`Close "${sessionName}"?\n\nThis will terminate the terminal session.`)) {
1036
+ return;
1037
+ }
1038
+
1039
+ console.log('[CLIENT] 🗑️ Closing session:', sessionId);
1040
+
1041
+ // Send close request to agent
1042
+ const closeMessage = {
1043
+ type: 'close_session',
1044
+ sessionId: sessionId
1045
+ };
1046
+
1047
+ if (dataChannel && dataChannel.readyState === 'open') {
1048
+ dataChannel.send(JSON.stringify(closeMessage));
1049
+ } else if (ws && ws.readyState === WebSocket.OPEN) {
1050
+ ws.send(JSON.stringify(closeMessage));
1051
+ }
1052
+
1053
+ // Remove from available sessions
1054
+ availableSessions = availableSessions.filter(s => s.id !== sessionId);
1055
+
1056
+ // If closing current session, switch to another or show message
1057
+ if (currentSession && currentSession.id === sessionId) {
1058
+ if (availableSessions.length > 0) {
1059
+ switchToSession(availableSessions[0].id);
1060
+ } else {
1061
+ currentSession = null;
1062
+ term.clear();
1063
+ term.write('\r\n\x1b[33mSession closed. Click + to create a new session.\x1b[0m\r\n');
1064
+ }
1065
+ }
1066
+
1067
+ renderTabs();
1068
+ }
1069
+
1015
1070
  function getConnectionStatus() {
1016
1071
  // Check direct WebSocket connection
1017
1072
  if (ws && ws.readyState === WebSocket.OPEN) {
@@ -1119,6 +1174,9 @@ function handleSessionMessage(message) {
1119
1174
  term.clear();
1120
1175
  term.write(`\r\n\x1b[36m✨ New session created: ${currentSession.name}\x1b[0m\r\n\r\n`);
1121
1176
 
1177
+ // Update URL with session ID so refresh reconnects to same session
1178
+ updateUrlWithSession(message.sessionId);
1179
+
1122
1180
  // Update UI
1123
1181
  updateSessionDisplay();
1124
1182
 
@@ -1135,6 +1193,9 @@ function handleSessionMessage(message) {
1135
1193
  term.clear(); // Clear terminal for new session
1136
1194
  console.log('[CLIENT] ✅ Switched to session:', currentSession);
1137
1195
 
1196
+ // Update URL so refresh reconnects to this session
1197
+ updateUrlWithSession(message.sessionId);
1198
+
1138
1199
  // Save updated session info
1139
1200
  saveSessionToLocalStorage(AGENT_ID, currentSession);
1140
1201
  break;