shell-mirror 1.5.98 → 1.5.100

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.
@@ -169,11 +169,14 @@ class SessionManager {
169
169
  this.maxSessions = 10;
170
170
  this.defaultSessionTimeout = 24 * 60 * 60 * 1000; // 24 hours
171
171
  this.clientSessions = {}; // Maps clientId to sessionId
172
+ this.sessionCounter = 0; // Incrementing counter for unique session names (never resets)
172
173
  }
173
174
 
174
175
  createSession(sessionName = null, clientId = null) {
175
176
  const sessionId = `ses_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
176
- const name = sessionName || `Session ${Object.keys(this.sessions).length + 1}`;
177
+ // Use incrementing counter for unique names (doesn't reuse after deletion)
178
+ this.sessionCounter++;
179
+ const name = sessionName || `Session ${this.sessionCounter}`;
177
180
 
178
181
  logToFile(`[SESSION] Creating new session: ${sessionId} (${name})`);
179
182
 
@@ -423,10 +426,20 @@ let heartbeatInterval;
423
426
 
424
427
  async function sendHeartbeat() {
425
428
  try {
429
+ // Get full session list for dashboard display
430
+ const sessionList = sessionManager.getAllSessions().map(session => ({
431
+ id: session.id,
432
+ name: session.name,
433
+ lastActivity: session.lastActivity,
434
+ createdAt: session.createdAt,
435
+ status: session.status
436
+ }));
437
+
426
438
  const heartbeatData = JSON.stringify({
427
439
  agentId: AGENT_ID,
428
440
  timestamp: Date.now(),
429
- activeSessions: Object.keys(sessionManager.sessions).length,
441
+ activeSessions: sessionList.length,
442
+ sessions: sessionList, // Full session list for dashboard
430
443
  localPort: process.env.LOCAL_PORT || 8080,
431
444
  capabilities: ['webrtc', 'direct_websocket']
432
445
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.5.98",
3
+ "version": "1.5.100",
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": {
@@ -225,6 +225,49 @@ body {
225
225
  transform: scale(1.05);
226
226
  }
227
227
 
228
+ /* Text Action Buttons (No Emoji) */
229
+ .btn-text-action {
230
+ background: #f8f9fa;
231
+ border: 1px solid #dee2e6;
232
+ border-radius: 6px;
233
+ color: #495057;
234
+ padding: 6px 12px;
235
+ font-size: 0.8rem;
236
+ font-weight: 500;
237
+ cursor: pointer;
238
+ transition: all 0.15s ease;
239
+ }
240
+
241
+ .btn-text-action:hover:not(:disabled) {
242
+ background: #e9ecef;
243
+ border-color: #ced4da;
244
+ color: #212529;
245
+ }
246
+
247
+ .btn-text-action.loading {
248
+ opacity: 0.7;
249
+ cursor: not-allowed;
250
+ }
251
+
252
+ .btn-text-action.btn-cleanup {
253
+ background: #fff5f5;
254
+ border-color: #feb2b2;
255
+ color: #c53030;
256
+ }
257
+
258
+ .btn-text-action.btn-cleanup:hover:not(:disabled) {
259
+ background: #fed7d7;
260
+ border-color: #fc8181;
261
+ }
262
+
263
+ /* Refresh Time in Card Header */
264
+ .refresh-time {
265
+ font-size: 0.75rem;
266
+ color: #718096;
267
+ font-weight: 400;
268
+ margin-left: 8px;
269
+ }
270
+
228
271
  .connection-status {
229
272
  font-size: 0.8rem;
230
273
  font-weight: 500;
@@ -422,11 +422,19 @@ class ShellMirrorDashboard {
422
422
  if (agentsData.success && agentsData.data && agentsData.data.agents) {
423
423
  this.agents = agentsData.data.agents;
424
424
 
425
+ // Populate agentSessions from API response (sessions are sent via agent heartbeat)
426
+ this.agentSessions = {};
427
+ this.agents.forEach(agent => {
428
+ if (agent.sessions && agent.sessions.length > 0) {
429
+ this.agentSessions[agent.agentId] = agent.sessions;
430
+ }
431
+ });
432
+
425
433
  // Don't load stale sessions from localStorage - only show live sessions from agents
426
- // Sessions will be populated via WebSocket updates from connected agents
427
434
  localStorage.removeItem('shell-mirror-sessions'); // Clear any stale data
428
435
  } else {
429
436
  this.agents = [];
437
+ this.agentSessions = {};
430
438
  }
431
439
 
432
440
  // TODO: Load session history when API is available
@@ -505,10 +513,9 @@ class ShellMirrorDashboard {
505
513
  document.getElementById('user-section').innerHTML = `
506
514
  <div class="dashboard-controls">
507
515
  <span id="connection-status" class="connection-status" style="display: none;"></span>
508
- <span id="refresh-status" class="refresh-status">Loading...</span>
509
516
  </div>
510
517
  <button class="help-button" onclick="dashboard.showAgentInstructions()" title="How to Use">
511
- 📖 How to Use
518
+ How to Use
512
519
  </button>
513
520
  <div class="user-info">
514
521
  <span class="user-name">${this.user?.name || this.user?.email || 'User'}</span>
@@ -624,19 +631,23 @@ class ShellMirrorDashboard {
624
631
  const offlineAgents = this.agents.filter(agent => agent.status === 'offline');
625
632
  const showCleanup = offlineAgents.length > 0;
626
633
 
634
+ // Format last refresh time
635
+ const refreshTime = this.lastRefresh ? new Date(this.lastRefresh).toLocaleTimeString() : 'Loading...';
636
+
627
637
  return `
628
638
  <div class="dashboard-card">
629
639
  <div class="card-header">
630
640
  <div class="card-title-section">
631
- <h2>🖥️ Active Agents</h2>
632
- <span class="agent-count">${agentCount} agent${agentCount !== 1 ? 's' : ''}</span>
641
+ <h2>Active Agents</h2>
642
+ <span class="agent-count">${agentCount}</span>
643
+ <span class="refresh-time">Updated ${refreshTime}</span>
633
644
  </div>
634
645
  <div class="agent-actions-header">
635
- <button id="refresh-btn" class="refresh-btn-inline" onclick="dashboard.manualRefresh()" title="Refresh agents">
636
- <span class="refresh-icon">🔄</span>
646
+ <button id="refresh-btn" class="btn-text-action" onclick="dashboard.manualRefresh()" title="Refresh agents">
647
+ Refresh
637
648
  </button>
638
- ${showCleanup ? `<button class="cleanup-btn-inline" onclick="dashboard.cleanupOfflineAgents()" title="Remove offline agents">
639
- <span>🧹</span>
649
+ ${showCleanup ? `<button class="btn-text-action btn-cleanup" onclick="dashboard.cleanupOfflineAgents()" title="Remove offline agents">
650
+ Clean
640
651
  </button>` : ''}
641
652
  </div>
642
653
  </div>
@@ -651,7 +662,7 @@ class ShellMirrorDashboard {
651
662
  return `
652
663
  <div class="empty-agent-state">
653
664
  <div class="empty-state-header">
654
- <h3>🚀 Get Started with Shell Mirror</h3>
665
+ <h3>Get Started with Shell Mirror</h3>
655
666
  <p>Connect your Mac in 2 simple steps:</p>
656
667
  </div>
657
668
 
@@ -680,7 +691,7 @@ class ShellMirrorDashboard {
680
691
  </div>
681
692
 
682
693
  <div class="empty-state-footer">
683
- <p>✨ Your agent will appear here once connected</p>
694
+ <p>Your agent will appear here once connected</p>
684
695
  </div>
685
696
  </div>
686
697
  `;
@@ -499,9 +499,86 @@
499
499
  if (event.target === modal) {
500
500
  closeHelpModal();
501
501
  }
502
+ const closeModal = document.getElementById('close-session-modal');
503
+ if (event.target === closeModal) {
504
+ hideCloseSessionModal();
505
+ }
502
506
  });
503
507
  </script>
504
508
 
505
- <script src="/app/terminal.js?v=1.5.88"></script>
509
+ <!-- Close Session Confirmation Modal -->
510
+ <div id="close-session-modal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.85); align-items: center; justify-content: center; z-index: 20000;">
511
+ <div style="background: #2a2a2a; border-radius: 12px; max-width: 400px; width: 90%; overflow: hidden; border: 1px solid #444; box-shadow: 0 10px 40px rgba(0,0,0,0.5);">
512
+ <!-- Header -->
513
+ <div style="padding: 20px 24px; border-bottom: 1px solid #444; display: flex; justify-content: space-between; align-items: center;">
514
+ <h3 style="margin: 0; font-size: 1.1rem; color: #fff;">Close Session?</h3>
515
+ <button onclick="hideCloseSessionModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #888;">&times;</button>
516
+ </div>
517
+
518
+ <!-- Content -->
519
+ <div style="padding: 24px;">
520
+ <div style="display: flex; align-items: center; gap: 16px; margin-bottom: 20px;">
521
+ <div style="font-size: 2.5rem;">🗑️</div>
522
+ <div>
523
+ <div id="close-session-name" style="font-size: 1.1rem; color: #fff; font-weight: 500; margin-bottom: 4px;">Session 1</div>
524
+ <div id="close-session-duration" style="font-size: 0.85rem; color: #888;">Duration: 5 minutes</div>
525
+ </div>
526
+ </div>
527
+
528
+ <p style="color: #bbb; margin: 0 0 24px 0; font-size: 0.9rem; line-height: 1.5;">
529
+ This will terminate the terminal session. Any running processes will be stopped.
530
+ </p>
531
+
532
+ <!-- Buttons -->
533
+ <div style="display: flex; gap: 12px; justify-content: flex-end;">
534
+ <button onclick="hideCloseSessionModal()" style="padding: 10px 20px; background: #444; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: background 0.2s;">Cancel</button>
535
+ <button id="confirm-close-session-btn" onclick="confirmCloseSession()" style="padding: 10px 20px; background: #dc3545; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem; font-weight: 500; transition: background 0.2s;">Close Session</button>
536
+ </div>
537
+ </div>
538
+ </div>
539
+ </div>
540
+
541
+ <script>
542
+ // Close Session Modal Functions
543
+ let pendingCloseSessionId = null;
544
+
545
+ function showCloseSessionModal(sessionId, sessionName, createdAt) {
546
+ pendingCloseSessionId = sessionId;
547
+
548
+ // Calculate duration
549
+ const duration = createdAt ? formatDuration(Date.now() - createdAt) : 'Unknown';
550
+
551
+ document.getElementById('close-session-name').textContent = sessionName || 'Session';
552
+ document.getElementById('close-session-duration').textContent = `Duration: ${duration}`;
553
+ document.getElementById('close-session-modal').style.display = 'flex';
554
+ }
555
+
556
+ function hideCloseSessionModal() {
557
+ document.getElementById('close-session-modal').style.display = 'none';
558
+ pendingCloseSessionId = null;
559
+ }
560
+
561
+ function confirmCloseSession() {
562
+ if (pendingCloseSessionId) {
563
+ // Call the actual close function from terminal.js
564
+ doCloseSession(pendingCloseSessionId);
565
+ }
566
+ hideCloseSessionModal();
567
+ }
568
+
569
+ function formatDuration(ms) {
570
+ const seconds = Math.floor(ms / 1000);
571
+ const minutes = Math.floor(seconds / 60);
572
+ const hours = Math.floor(minutes / 60);
573
+ const days = Math.floor(hours / 24);
574
+
575
+ if (days > 0) return `${days}d ${hours % 24}h`;
576
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
577
+ if (minutes > 0) return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
578
+ return `${seconds} second${seconds !== 1 ? 's' : ''}`;
579
+ }
580
+ </script>
581
+
582
+ <script src="/app/terminal.js?v=1.5.89"></script>
506
583
  </body>
507
584
  </html>
@@ -965,6 +965,28 @@ function updateSessionDisplay() {
965
965
  }
966
966
  }
967
967
 
968
+ // Session tab color palette (fixed colors by creation order)
969
+ const SESSION_TAB_COLORS = [
970
+ { bg: '#e3f2fd', border: '#2196f3', text: '#1565c0' }, // Blue
971
+ { bg: '#e8f5e9', border: '#4caf50', text: '#2e7d32' }, // Green
972
+ { bg: '#fff3e0', border: '#ff9800', text: '#e65100' }, // Orange
973
+ { bg: '#f3e5f5', border: '#9c27b0', text: '#6a1b9a' }, // Purple
974
+ { bg: '#e0f7fa', border: '#00bcd4', text: '#00838f' }, // Teal
975
+ { bg: '#fce4ec', border: '#e91e63', text: '#ad1457' }, // Pink
976
+ ];
977
+
978
+ // Track color assignments by session ID (persists across renders)
979
+ const sessionColorMap = {};
980
+ let nextColorIndex = 0;
981
+
982
+ function getSessionColor(sessionId) {
983
+ if (!sessionColorMap[sessionId]) {
984
+ sessionColorMap[sessionId] = nextColorIndex;
985
+ nextColorIndex = (nextColorIndex + 1) % SESSION_TAB_COLORS.length;
986
+ }
987
+ return SESSION_TAB_COLORS[sessionColorMap[sessionId]];
988
+ }
989
+
968
990
  function renderTabs() {
969
991
  const tabBar = document.getElementById('session-tab-bar');
970
992
  if (!tabBar) {
@@ -996,15 +1018,24 @@ function renderTabs() {
996
1018
  tabsHTML = sessionsToRender.map(session => {
997
1019
  const isActive = currentSession && session.id === currentSession.id;
998
1020
  const displayName = session.name || 'Terminal Session';
1021
+ const color = getSessionColor(session.id);
1022
+
1023
+ // Active tabs get full color, inactive tabs get muted version
1024
+ const tabStyle = isActive
1025
+ ? `background: ${color.bg}; border-color: ${color.border}; border-bottom: 3px solid ${color.border};`
1026
+ : `background: transparent; border-color: transparent; opacity: 0.7;`;
1027
+ const textStyle = isActive
1028
+ ? `color: ${color.text}; font-weight: 600;`
1029
+ : `color: #888;`;
999
1030
 
1000
1031
  return `
1001
- <div class="session-tab ${isActive ? 'active' : ''}" title="${displayName}">
1032
+ <div class="session-tab ${isActive ? 'active' : ''}" style="${tabStyle}" title="${displayName}" data-color-index="${sessionColorMap[session.id]}">
1002
1033
  <button class="session-tab-btn"
1003
1034
  onclick="switchToSession('${session.id}')"
1004
- ${isActive ? '' : ''}>
1035
+ style="${textStyle}">
1005
1036
  <span class="session-tab-name">${displayName}</span>
1006
1037
  </button>
1007
- <button class="session-tab-close" onclick="closeSession('${session.id}', event)" title="Close session">×</button>
1038
+ <button class="session-tab-close" onclick="closeSession('${session.id}', event)" title="Close session" style="color: ${isActive ? color.text : '#888'}">×</button>
1008
1039
  </div>
1009
1040
  `;
1010
1041
  }).join('');
@@ -1025,17 +1056,27 @@ function updateUrlWithSession(sessionId) {
1025
1056
  console.log('[CLIENT] 📍 URL updated with session:', sessionId);
1026
1057
  }
1027
1058
 
1028
- // Close a session with confirmation
1059
+ // Close a session with confirmation - shows custom modal
1029
1060
  function closeSession(sessionId, event) {
1030
1061
  event.stopPropagation(); // Don't trigger tab switch
1031
1062
 
1032
1063
  const session = availableSessions.find(s => s.id === sessionId);
1033
1064
  const sessionName = session?.name || 'this session';
1065
+ const createdAt = session?.createdAt || null;
1034
1066
 
1035
- if (!confirm(`Close "${sessionName}"?\n\nThis will terminate the terminal session.`)) {
1036
- return;
1067
+ // Show custom modal instead of browser confirm()
1068
+ if (typeof showCloseSessionModal === 'function') {
1069
+ showCloseSessionModal(sessionId, sessionName, createdAt);
1070
+ } else {
1071
+ // Fallback to native confirm if modal not available
1072
+ if (confirm(`Close "${sessionName}"?\n\nThis will terminate the terminal session.`)) {
1073
+ doCloseSession(sessionId);
1074
+ }
1037
1075
  }
1076
+ }
1038
1077
 
1078
+ // Actually close the session (called from modal confirmation)
1079
+ function doCloseSession(sessionId) {
1039
1080
  console.log('[CLIENT] 🗑️ Closing session:', sessionId);
1040
1081
 
1041
1082
  // Send close request to agent
@@ -1182,6 +1223,9 @@ function handleSessionMessage(message) {
1182
1223
 
1183
1224
  // Save to localStorage
1184
1225
  saveSessionToLocalStorage(AGENT_ID, currentSession);
1226
+
1227
+ // Focus terminal for keyboard input
1228
+ term.focus();
1185
1229
  break;
1186
1230
 
1187
1231
  case 'session-switched':
@@ -1198,6 +1242,9 @@ function handleSessionMessage(message) {
1198
1242
 
1199
1243
  // Save updated session info
1200
1244
  saveSessionToLocalStorage(AGENT_ID, currentSession);
1245
+
1246
+ // Focus terminal for keyboard input
1247
+ term.focus();
1201
1248
  break;
1202
1249
  case 'session-ended':
1203
1250
  term.write(`\r\n\x1b[31m❌ Session ended: ${message.reason}\x1b[0m\r\n`);