shell-mirror 1.5.97 → 1.5.99

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.
@@ -423,10 +423,20 @@ let heartbeatInterval;
423
423
 
424
424
  async function sendHeartbeat() {
425
425
  try {
426
+ // Get full session list for dashboard display
427
+ const sessionList = sessionManager.getAllSessions().map(session => ({
428
+ id: session.id,
429
+ name: session.name,
430
+ lastActivity: session.lastActivity,
431
+ createdAt: session.createdAt,
432
+ status: session.status
433
+ }));
434
+
426
435
  const heartbeatData = JSON.stringify({
427
436
  agentId: AGENT_ID,
428
437
  timestamp: Date.now(),
429
- activeSessions: Object.keys(sessionManager.sessions).length,
438
+ activeSessions: sessionList.length,
439
+ sessions: sessionList, // Full session list for dashboard
430
440
  localPort: process.env.LOCAL_PORT || 8080,
431
441
  capabilities: ['webrtc', 'direct_websocket']
432
442
  });
@@ -519,7 +529,6 @@ function connectToSignalingServer() {
519
529
  try {
520
530
  let sessionId;
521
531
  let isNewSession = false;
522
- let availableSessions = sessionManager.getAllSessions();
523
532
 
524
533
  // Handle session request from client
525
534
  if (data.sessionRequest) {
@@ -569,15 +578,16 @@ function connectToSignalingServer() {
569
578
  await peerConnection.setLocalDescription(offer);
570
579
 
571
580
  // Send WebRTC offer with session assignment
572
- sendMessage({
573
- type: 'offer',
574
- sdp: offer.sdp,
575
- to: data.from,
581
+ // Get availableSessions AFTER session creation so new session is included
582
+ sendMessage({
583
+ type: 'offer',
584
+ sdp: offer.sdp,
585
+ to: data.from,
576
586
  from: AGENT_ID,
577
587
  sessionId: sessionId,
578
588
  sessionName: sessionManager.getSession(sessionId).name,
579
589
  isNewSession: isNewSession,
580
- availableSessions: availableSessions
590
+ availableSessions: sessionManager.getAllSessions()
581
591
  });
582
592
  logToFile('✅ WebRTC offer sent with session assignment');
583
593
 
@@ -1007,6 +1017,9 @@ process.on('SIGTERM', () => {
1007
1017
  });
1008
1018
 
1009
1019
  // --- Local WebSocket Server for Direct Connections ---
1020
+ // Sessions storage for direct WebSocket connections
1021
+ const directSessions = {};
1022
+
1010
1023
  function startLocalServer() {
1011
1024
  const localPort = process.env.LOCAL_PORT || 8080;
1012
1025
  const localServer = require('ws').Server;
@@ -1039,57 +1052,96 @@ function startLocalServer() {
1039
1052
  break;
1040
1053
 
1041
1054
  case 'create_session':
1042
- // Create new terminal session for direct connection
1043
- const sessionId = uuidv4();
1044
- const ptyProcess = pty.spawn(shell, [], {
1045
- name: 'xterm-color',
1046
- cols: message.cols || 120,
1047
- rows: message.rows || 30,
1048
- cwd: process.env.HOME,
1049
- env: process.env
1050
- });
1055
+ // Check if client requested an existing session
1056
+ let sessionId;
1057
+ let isNewSession = false;
1051
1058
 
1052
- // Store session
1053
- sessions[sessionId] = {
1054
- pty: ptyProcess,
1055
- buffer: new CircularBuffer(),
1056
- lastActivity: Date.now()
1057
- };
1059
+ if (message.sessionId && directSessions[message.sessionId]) {
1060
+ // Reconnect to existing session
1061
+ sessionId = message.sessionId;
1062
+ logToFile(`[LOCAL] Reconnecting to existing session: ${sessionId}`);
1063
+
1064
+ // Update activity timestamp
1065
+ directSessions[sessionId].lastActivity = Date.now();
1066
+
1067
+ // Re-attach output handler for this connection
1068
+ directSessions[sessionId].pty.onData((data) => {
1069
+ if (localWs.readyState === WebSocket.OPEN) {
1070
+ localWs.send(JSON.stringify({
1071
+ type: 'output',
1072
+ sessionId,
1073
+ data
1074
+ }));
1075
+ }
1076
+ });
1058
1077
 
1059
- // Send session output to direct connection
1060
- ptyProcess.onData((data) => {
1061
- if (localWs.readyState === WebSocket.OPEN) {
1078
+ // Send buffered output if available
1079
+ const bufferedOutput = directSessions[sessionId].buffer.getAll();
1080
+ if (bufferedOutput.length > 0) {
1062
1081
  localWs.send(JSON.stringify({
1063
1082
  type: 'output',
1064
1083
  sessionId,
1065
- data
1084
+ data: bufferedOutput.join('')
1066
1085
  }));
1067
1086
  }
1068
- });
1087
+ } else {
1088
+ // Create new terminal session
1089
+ sessionId = uuidv4();
1090
+ isNewSession = true;
1091
+
1092
+ const ptyProcess = pty.spawn(shell, [], {
1093
+ name: 'xterm-color',
1094
+ cols: message.cols || 120,
1095
+ rows: message.rows || 30,
1096
+ cwd: process.env.HOME,
1097
+ env: process.env
1098
+ });
1099
+
1100
+ // Store session
1101
+ directSessions[sessionId] = {
1102
+ pty: ptyProcess,
1103
+ buffer: new CircularBuffer(),
1104
+ lastActivity: Date.now()
1105
+ };
1106
+
1107
+ // Send session output to direct connection
1108
+ ptyProcess.onData((data) => {
1109
+ if (localWs.readyState === WebSocket.OPEN) {
1110
+ localWs.send(JSON.stringify({
1111
+ type: 'output',
1112
+ sessionId,
1113
+ data
1114
+ }));
1115
+ }
1116
+ // Store in buffer for reconnection
1117
+ directSessions[sessionId].buffer.add(data);
1118
+ });
1119
+
1120
+ logToFile(`[LOCAL] Created new direct session: ${sessionId}`);
1121
+ }
1069
1122
 
1070
1123
  localWs.send(JSON.stringify({
1071
1124
  type: 'session_created',
1072
1125
  sessionId,
1073
1126
  sessionName: `Session ${sessionId.slice(0, 8)}`,
1127
+ isNewSession: isNewSession,
1074
1128
  cols: message.cols || 120,
1075
1129
  rows: message.rows || 30
1076
1130
  }));
1077
-
1078
- logToFile(`[LOCAL] Created direct session: ${sessionId}`);
1079
1131
  break;
1080
1132
 
1081
1133
  case 'input':
1082
1134
  // Handle terminal input for direct connection
1083
- if (sessions[message.sessionId]) {
1084
- sessions[message.sessionId].pty.write(message.data);
1085
- sessions[message.sessionId].lastActivity = Date.now();
1135
+ if (directSessions[message.sessionId]) {
1136
+ directSessions[message.sessionId].pty.write(message.data);
1137
+ directSessions[message.sessionId].lastActivity = Date.now();
1086
1138
  }
1087
1139
  break;
1088
1140
 
1089
1141
  case 'resize':
1090
1142
  // Handle terminal resize for direct connection
1091
- if (sessions[message.sessionId]) {
1092
- sessions[message.sessionId].pty.resize(message.cols, message.rows);
1143
+ if (directSessions[message.sessionId]) {
1144
+ directSessions[message.sessionId].pty.resize(message.cols, message.rows);
1093
1145
  }
1094
1146
  break;
1095
1147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.5.97",
3
+ "version": "1.5.99",
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": {
@@ -383,7 +383,8 @@ class ShellMirrorDashboard {
383
383
  updateAgentsDisplay() {
384
384
  const agentsCard = document.querySelector('.dashboard-card');
385
385
  if (agentsCard) {
386
- agentsCard.innerHTML = this.renderActiveAgents();
386
+ // Use outerHTML to replace the entire card, not nest inside it
387
+ agentsCard.outerHTML = this.renderActiveAgents();
387
388
  }
388
389
  }
389
390
 
@@ -421,11 +422,19 @@ class ShellMirrorDashboard {
421
422
  if (agentsData.success && agentsData.data && agentsData.data.agents) {
422
423
  this.agents = agentsData.data.agents;
423
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
+
424
433
  // Don't load stale sessions from localStorage - only show live sessions from agents
425
- // Sessions will be populated via WebSocket updates from connected agents
426
434
  localStorage.removeItem('shell-mirror-sessions'); // Clear any stale data
427
435
  } else {
428
436
  this.agents = [];
437
+ this.agentSessions = {};
429
438
  }
430
439
 
431
440
  // TODO: Load session history when API is available
@@ -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
 
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
+
505
582
  <script src="/app/terminal.js?v=1.5.88"></script>
506
583
  </body>
507
584
  </html>
@@ -1025,17 +1025,27 @@ function updateUrlWithSession(sessionId) {
1025
1025
  console.log('[CLIENT] 📍 URL updated with session:', sessionId);
1026
1026
  }
1027
1027
 
1028
- // Close a session with confirmation
1028
+ // Close a session with confirmation - shows custom modal
1029
1029
  function closeSession(sessionId, event) {
1030
1030
  event.stopPropagation(); // Don't trigger tab switch
1031
1031
 
1032
1032
  const session = availableSessions.find(s => s.id === sessionId);
1033
1033
  const sessionName = session?.name || 'this session';
1034
+ const createdAt = session?.createdAt || null;
1034
1035
 
1035
- if (!confirm(`Close "${sessionName}"?\n\nThis will terminate the terminal session.`)) {
1036
- return;
1036
+ // Show custom modal instead of browser confirm()
1037
+ if (typeof showCloseSessionModal === 'function') {
1038
+ showCloseSessionModal(sessionId, sessionName, createdAt);
1039
+ } else {
1040
+ // Fallback to native confirm if modal not available
1041
+ if (confirm(`Close "${sessionName}"?\n\nThis will terminate the terminal session.`)) {
1042
+ doCloseSession(sessionId);
1043
+ }
1037
1044
  }
1045
+ }
1038
1046
 
1047
+ // Actually close the session (called from modal confirmation)
1048
+ function doCloseSession(sessionId) {
1039
1049
  console.log('[CLIENT] 🗑️ Closing session:', sessionId);
1040
1050
 
1041
1051
  // Send close request to agent