shell-mirror 1.5.113 → 1.5.115

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.113",
3
+ "version": "1.5.115",
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": {
@@ -239,7 +239,7 @@ body {
239
239
  .agent-name-row {
240
240
  display: flex;
241
241
  align-items: center;
242
- gap: 8px;
242
+ justify-content: space-between;
243
243
  margin-bottom: 4px;
244
244
  }
245
245
 
@@ -247,11 +247,15 @@ body {
247
247
  font-weight: 600;
248
248
  font-size: 1rem;
249
249
  color: var(--text-primary);
250
+ flex: 1;
251
+ min-width: 0;
250
252
  }
251
253
 
252
254
  /* Agent Menu Dropdown */
253
255
  .agent-menu {
254
256
  position: relative;
257
+ flex-shrink: 0;
258
+ margin-left: 8px;
255
259
  }
256
260
 
257
261
  .btn-agent-menu {
@@ -263,6 +267,7 @@ body {
263
267
  padding: 4px 8px;
264
268
  border-radius: 4px;
265
269
  transition: all 0.15s ease;
270
+ line-height: 1;
266
271
  }
267
272
 
268
273
  .btn-agent-menu:hover {
@@ -273,14 +278,14 @@ body {
273
278
  .agent-menu-dropdown {
274
279
  display: none;
275
280
  position: absolute;
276
- right: 0;
281
+ right: -8px;
277
282
  top: calc(100% + 4px);
278
- background: var(--bg-secondary);
279
- border: 1px solid var(--border);
280
- border-radius: 6px;
283
+ background: var(--bg-primary);
284
+ border: 1px solid var(--border-light);
285
+ border-radius: 8px;
281
286
  min-width: 160px;
282
- z-index: 100;
283
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
287
+ z-index: 1000;
288
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
284
289
  }
285
290
 
286
291
  .agent-menu-dropdown.show {
@@ -25,6 +25,7 @@ class ShellMirrorDashboard {
25
25
  this.isRefreshing = false;
26
26
  this.connectionStatusDebounce = null;
27
27
  this.currentConnectionStatus = null;
28
+ this.sessionBroadcast = null; // For cross-tab session sync
28
29
  this.init();
29
30
  }
30
31
 
@@ -60,6 +61,7 @@ class ShellMirrorDashboard {
60
61
  this.updateLastRefreshTime();
61
62
  this.enableHttpOnlyMode(); // Use HTTP-only mode (no persistent WebSocket)
62
63
  this.startAutoRefresh(); // Start auto-refresh for authenticated users
64
+ this.setupSessionSync(); // Listen for real-time session updates from terminal tabs
63
65
  } else {
64
66
  this.renderUnauthenticatedDashboard();
65
67
  }
@@ -76,13 +78,50 @@ class ShellMirrorDashboard {
76
78
  if (this.refreshInterval) {
77
79
  clearInterval(this.refreshInterval);
78
80
  }
79
-
80
- // Refresh agent data every 30 seconds (HTTP polling only)
81
+
82
+ // Refresh agent data every 10 seconds (sessions sync instantly via BroadcastChannel)
81
83
  this.refreshInterval = setInterval(async () => {
82
84
  if (this.isAuthenticated && !this.isRefreshing) {
83
85
  await this.refreshDashboardData();
84
86
  }
85
- }, 30000);
87
+ }, 10000);
88
+ }
89
+
90
+ setupSessionSync() {
91
+ // Use BroadcastChannel for instant cross-tab session sync
92
+ try {
93
+ this.sessionBroadcast = new BroadcastChannel('shell-mirror-sessions');
94
+ this.sessionBroadcast.onmessage = (event) => {
95
+ console.log('[DASHBOARD] 📡 Received session update from terminal:', event.data);
96
+ if (event.data.type === 'session-update') {
97
+ this.handleSessionUpdate(event.data);
98
+ }
99
+ };
100
+ console.log('[DASHBOARD] 📡 Session sync channel established');
101
+ } catch (e) {
102
+ console.log('[DASHBOARD] BroadcastChannel not supported, using localStorage polling');
103
+ }
104
+
105
+ // Also listen for localStorage changes (fallback for same-origin tabs)
106
+ window.addEventListener('storage', (event) => {
107
+ if (event.key === 'shell-mirror-sessions') {
108
+ console.log('[DASHBOARD] 💾 localStorage session update detected');
109
+ this.loadSessionsFromLocalStorage();
110
+ this.updateAgentsDisplay();
111
+ }
112
+ });
113
+
114
+ // Initial load from localStorage
115
+ this.loadSessionsFromLocalStorage();
116
+ }
117
+
118
+ handleSessionUpdate(data) {
119
+ const { agentId, sessions } = data;
120
+ if (agentId && sessions) {
121
+ this.agentSessions[agentId] = sessions;
122
+ console.log('[DASHBOARD] ✅ Sessions updated for agent:', agentId, sessions);
123
+ this.updateAgentsDisplay();
124
+ }
86
125
  }
87
126
 
88
127
  async refreshDashboardData() {
@@ -434,15 +473,22 @@ class ShellMirrorDashboard {
434
473
  this.agents = agentsData.data.agents;
435
474
 
436
475
  // Populate agentSessions from API response (sessions are sent via agent heartbeat)
437
- this.agentSessions = {};
476
+ // But DON'T reset - merge with localStorage sessions for instant updates
438
477
  this.agents.forEach(agent => {
439
478
  if (agent.sessions && agent.sessions.length > 0) {
440
- this.agentSessions[agent.agentId] = agent.sessions;
479
+ // Merge API sessions with any localStorage sessions
480
+ const localSessions = this.agentSessions[agent.agentId] || [];
481
+ const apiSessionIds = new Set(agent.sessions.map(s => s.id));
482
+
483
+ // Keep local sessions that aren't in API yet (just created)
484
+ const newLocalSessions = localSessions.filter(s => !apiSessionIds.has(s.id));
485
+
486
+ this.agentSessions[agent.agentId] = [...agent.sessions, ...newLocalSessions];
441
487
  }
442
488
  });
443
489
 
444
- // Don't load stale sessions from localStorage - only show live sessions from agents
445
- localStorage.removeItem('shell-mirror-sessions'); // Clear any stale data
490
+ // Load sessions from localStorage (created by terminal tabs)
491
+ this.loadSessionsFromLocalStorage();
446
492
  } else {
447
493
  this.agents = [];
448
494
  this.agentSessions = {};
@@ -1108,7 +1154,7 @@ class ShellMirrorDashboard {
1108
1154
  </div>
1109
1155
  <div class="modal-body">
1110
1156
  <p style="margin-bottom: 12px; color: var(--text-secondary);">
1111
- Are you sure you want to shut down <strong style="color: var(--text-primary);">${agentName}</strong>?
1157
+ Are you sure you want to shut down<br><strong style="color: var(--text-primary); word-break: break-all;">${agentName}</strong>?
1112
1158
  </p>
1113
1159
  <p style="font-size: 0.85rem; color: var(--text-muted);">
1114
1160
  This will unregister the agent from the dashboard. If the agent is still running, it will re-register on next heartbeat.
@@ -1197,34 +1243,41 @@ class ShellMirrorDashboard {
1197
1243
  return `${days}d ago`;
1198
1244
  }
1199
1245
 
1200
- // Session storage management
1201
- loadSessionsFromStorage() {
1246
+ // Session storage management - merge localStorage sessions with API sessions
1247
+ loadSessionsFromLocalStorage() {
1202
1248
  try {
1203
1249
  const storedSessions = localStorage.getItem('shell-mirror-sessions');
1204
-
1250
+
1205
1251
  if (storedSessions) {
1206
1252
  const sessionData = JSON.parse(storedSessions);
1207
-
1208
- // Filter out old sessions (older than 24 hours)
1253
+
1254
+ // Filter out old sessions (older than 1 hour for inactive ones)
1209
1255
  const now = Date.now();
1210
- const maxAge = 24 * 60 * 60 * 1000; // 24 hours
1211
-
1256
+ const maxAge = 60 * 60 * 1000; // 1 hour
1257
+
1212
1258
  Object.keys(sessionData).forEach(agentId => {
1213
- const allSessions = sessionData[agentId];
1214
-
1215
- const validSessions = allSessions.filter(session => {
1259
+ const localSessions = sessionData[agentId] || [];
1260
+
1261
+ // Only keep recent active sessions
1262
+ const validSessions = localSessions.filter(session => {
1216
1263
  const age = now - session.lastActivity;
1217
- return age < maxAge;
1264
+ return session.status === 'active' && age < maxAge;
1218
1265
  });
1219
-
1266
+
1220
1267
  if (validSessions.length > 0) {
1221
- this.agentSessions[agentId] = validSessions;
1268
+ // Merge with existing API sessions (avoid duplicates)
1269
+ const existingSessions = this.agentSessions[agentId] || [];
1270
+ const existingIds = new Set(existingSessions.map(s => s.id));
1271
+
1272
+ const newSessions = validSessions.filter(s => !existingIds.has(s.id));
1273
+ this.agentSessions[agentId] = [...existingSessions, ...newSessions];
1274
+
1275
+ console.log(`[DASHBOARD] 💾 Merged ${newSessions.length} localStorage sessions for ${agentId}`);
1222
1276
  }
1223
1277
  });
1224
- } else {
1225
1278
  }
1226
1279
  } catch (error) {
1227
- console.error('[DASHBOARD] ❌ Error loading sessions from storage:', error);
1280
+ console.error('[DASHBOARD] ❌ Error loading sessions from localStorage:', error);
1228
1281
  }
1229
1282
  }
1230
1283
 
@@ -1324,11 +1324,20 @@ function saveSessionToLocalStorage(agentId, sessionInfo) {
1324
1324
 
1325
1325
  localStorage.setItem('shell-mirror-sessions', JSON.stringify(sessionData));
1326
1326
  console.log('[CLIENT] 💾 Session saved to storage:', sessionToStore);
1327
- console.log('[CLIENT] 🔍 DEBUG: Final stored data:', JSON.stringify(sessionData));
1328
-
1329
- // Verify the save worked
1330
- const verification = localStorage.getItem('shell-mirror-sessions');
1331
- console.log('[CLIENT] ✅ DEBUG: Verification read:', verification);
1327
+
1328
+ // Broadcast to dashboard tabs for instant sync
1329
+ try {
1330
+ const broadcast = new BroadcastChannel('shell-mirror-sessions');
1331
+ broadcast.postMessage({
1332
+ type: 'session-update',
1333
+ agentId: agentId,
1334
+ sessions: sessionData[agentId]
1335
+ });
1336
+ broadcast.close();
1337
+ console.log('[CLIENT] 📡 Session update broadcasted to dashboard');
1338
+ } catch (e) {
1339
+ // BroadcastChannel not supported, localStorage event will handle it
1340
+ }
1332
1341
  } catch (error) {
1333
1342
  console.error('[CLIENT] ❌ Error saving session to storage:', error);
1334
1343
  }