shell-mirror 1.5.114 → 1.5.116

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.114",
3
+ "version": "1.5.116",
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": {
@@ -278,14 +278,14 @@ body {
278
278
  .agent-menu-dropdown {
279
279
  display: none;
280
280
  position: absolute;
281
- right: 0;
282
- top: calc(100% + 2px);
283
- background: var(--bg-secondary);
284
- border: 1px solid var(--border);
285
- border-radius: 6px;
281
+ right: -8px;
282
+ top: calc(100% + 4px);
283
+ background: var(--bg-primary);
284
+ border: 1px solid var(--border-light);
285
+ border-radius: 8px;
286
286
  min-width: 160px;
287
- z-index: 100;
288
- 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);
289
289
  }
290
290
 
291
291
  .agent-menu-dropdown.show {
@@ -436,14 +436,18 @@ body {
436
436
  filter: brightness(1.1);
437
437
  }
438
438
 
439
- /* New Session Button */
439
+ /* New Session Button - styled like session row */
440
440
  .btn-new-session {
441
- background: transparent;
442
- color: var(--accent);
441
+ display: flex;
442
+ align-items: center;
443
+ justify-content: center;
444
+ gap: 8px;
445
+ background: var(--bg-tertiary);
446
+ color: var(--text-secondary);
443
447
  border: 1px dashed var(--border-light);
444
448
  border-radius: 6px;
445
- padding: 10px 16px;
446
- font-size: 0.85rem;
449
+ padding: 12px 16px;
450
+ font-size: 0.9rem;
447
451
  font-weight: 500;
448
452
  cursor: pointer;
449
453
  width: 100%;
@@ -451,9 +455,15 @@ body {
451
455
  }
452
456
 
453
457
  .btn-new-session:hover {
454
- background: var(--bg-tertiary);
458
+ background: var(--bg-hover);
455
459
  border-color: var(--accent);
456
460
  border-style: solid;
461
+ color: var(--accent);
462
+ }
463
+
464
+ .btn-new-session .plus-icon {
465
+ font-size: 1.1rem;
466
+ font-weight: 400;
457
467
  }
458
468
 
459
469
  /* Offline Agent */
@@ -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
  }
@@ -77,12 +79,49 @@ class ShellMirrorDashboard {
77
79
  clearInterval(this.refreshInterval);
78
80
  }
79
81
 
80
- // Refresh agent data every 5 seconds for responsive session updates
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
- }, 5000);
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 = {};
@@ -606,7 +652,7 @@ class ShellMirrorDashboard {
606
652
  </div>
607
653
  ` : ''}
608
654
  <button class="btn-new-session" onclick="dashboard.createNewSession('${agent.agentId}')">
609
- + New Session
655
+ <span class="plus-icon">+</span> Create New Session
610
656
  </button>
611
657
  </div>
612
658
  ` : `
@@ -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
  }