shell-mirror 1.5.107 → 1.5.109

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.
@@ -980,19 +980,41 @@ function setupDataChannel(clientId) {
980
980
  // Handle session closure request from client
981
981
  logToFile(`[AGENT] Client ${clientId} closing session ${message.sessionId}`);
982
982
 
983
- sessionManager.terminateSession(message.sessionId);
983
+ // Get remaining sessions BEFORE termination
984
+ const closingSessionId = message.sessionId;
985
+ sessionManager.terminateSession(closingSessionId);
986
+
987
+ const remainingSessions = sessionManager.getAllSessions();
984
988
 
985
989
  // Send confirmation with updated session list
986
990
  dataChannel.send(JSON.stringify({
987
991
  type: 'session-closed',
988
- sessionId: message.sessionId,
989
- availableSessions: sessionManager.getAllSessions()
992
+ sessionId: closingSessionId,
993
+ availableSessions: remainingSessions
990
994
  }));
991
995
 
996
+ // If client requested auto-switch to next session, do it atomically
997
+ if (message.switchToSessionId && remainingSessions.find(s => s.id === message.switchToSessionId)) {
998
+ logToFile(`[AGENT] Auto-switching client to session ${message.switchToSessionId}`);
999
+ if (sessionManager.connectClientToSession(clientId, message.switchToSessionId)) {
1000
+ const newSession = sessionManager.getSession(message.switchToSessionId);
1001
+ dataChannel.send(JSON.stringify({
1002
+ type: 'session-switched',
1003
+ sessionId: message.switchToSessionId,
1004
+ sessionName: newSession.name
1005
+ }));
1006
+ // Send buffered output
1007
+ const bufferedOutput = newSession.buffer.getAll();
1008
+ if (bufferedOutput) {
1009
+ sendLargeMessage(dataChannel, { type: 'output', data: bufferedOutput }, '[AGENT]');
1010
+ }
1011
+ }
1012
+ }
1013
+
992
1014
  // Send immediate heartbeat to update dashboard
993
1015
  sendHeartbeat();
994
1016
 
995
- logToFile(`[AGENT] ✅ Session closed: ${message.sessionId}`);
1017
+ logToFile(`[AGENT] ✅ Session closed: ${closingSessionId}`);
996
1018
  }
997
1019
  } catch (err) {
998
1020
  logToFile(`[AGENT] Error parsing data channel message: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.5.107",
3
+ "version": "1.5.109",
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": {
@@ -176,12 +176,8 @@
176
176
  font-weight: 600;
177
177
  }
178
178
 
179
- .session-tab-btn {
180
- background: none;
181
- border: none;
182
- color: inherit;
183
- font: inherit;
184
- cursor: pointer;
179
+ .session-tab-name {
180
+ flex: 1;
185
181
  padding: 2px 4px;
186
182
  }
187
183
 
@@ -576,6 +572,6 @@
576
572
  }
577
573
  </script>
578
574
 
579
- <script src="/app/terminal.js?v=1.5.91"></script>
575
+ <script src="/app/terminal.js?v=1.5.93"></script>
580
576
  </body>
581
577
  </html>
@@ -1030,13 +1030,9 @@ function renderTabs() {
1030
1030
  : `color: ${color.muted};`;
1031
1031
 
1032
1032
  return `
1033
- <div class="session-tab ${isActive ? 'active' : ''}" style="${tabStyle}" title="${displayName}" data-color-index="${sessionColorMap[session.id]}">
1034
- <button class="session-tab-btn"
1035
- onclick="switchToSession('${session.id}')"
1036
- style="${textStyle}">
1037
- <span class="session-tab-name">${displayName}</span>
1038
- </button>
1039
- <button class="session-tab-close" onclick="closeSession('${session.id}', event)" title="Close session" style="color: ${isActive ? color.text : color.muted}">×</button>
1033
+ <div class="session-tab ${isActive ? 'active' : ''}" style="${tabStyle}; cursor: pointer;" title="${displayName}" data-color-index="${sessionColorMap[session.id]}" onclick="switchToSession('${session.id}')">
1034
+ <span class="session-tab-name" style="${textStyle}">${displayName}</span>
1035
+ <button class="session-tab-close" onclick="event.stopPropagation(); closeSession('${session.id}', event)" title="Close session" style="color: ${isActive ? color.text : color.muted}">×</button>
1040
1036
  </div>
1041
1037
  `;
1042
1038
  }).join('');
@@ -1080,10 +1076,16 @@ function closeSession(sessionId, event) {
1080
1076
  function doCloseSession(sessionId) {
1081
1077
  console.log('[CLIENT] 🗑️ Closing session:', sessionId);
1082
1078
 
1083
- // Send close request to agent
1079
+ // Remove from available sessions
1080
+ const remainingSessions = availableSessions.filter(s => s.id !== sessionId);
1081
+ const isClosingCurrentSession = currentSession && currentSession.id === sessionId;
1082
+ const nextSession = isClosingCurrentSession && remainingSessions.length > 0 ? remainingSessions[0] : null;
1083
+
1084
+ // Send close request to agent (with auto-switch if closing active session)
1084
1085
  const closeMessage = {
1085
1086
  type: 'close_session',
1086
- sessionId: sessionId
1087
+ sessionId: sessionId,
1088
+ switchToSessionId: nextSession ? nextSession.id : null // Atomic close + switch
1087
1089
  };
1088
1090
 
1089
1091
  if (dataChannel && dataChannel.readyState === 'open') {
@@ -1092,25 +1094,25 @@ function doCloseSession(sessionId) {
1092
1094
  ws.send(JSON.stringify(closeMessage));
1093
1095
  }
1094
1096
 
1095
- // Remove from available sessions
1096
- availableSessions = availableSessions.filter(s => s.id !== sessionId);
1097
+ // Update local state
1098
+ availableSessions = remainingSessions;
1097
1099
 
1098
- // If closing current session, switch to another or show message
1099
- if (currentSession && currentSession.id === sessionId) {
1100
- if (availableSessions.length > 0) {
1100
+ // If closing current session, update UI immediately
1101
+ if (isClosingCurrentSession) {
1102
+ // Clear terminal IMMEDIATELY to prevent garbage from closed session
1103
+ term.clear();
1104
+
1105
+ if (nextSession) {
1101
1106
  // Update currentSession IMMEDIATELY so renderTabs shows correct active tab
1102
- const nextSession = availableSessions[0];
1103
1107
  currentSession = {
1104
1108
  id: nextSession.id,
1105
1109
  name: nextSession.name || 'Terminal Session'
1106
1110
  };
1107
- // Tell agent to switch (will send session-switched confirmation)
1108
- switchToSession(nextSession.id);
1109
1111
  // Update URL
1110
1112
  updateUrlWithSession(nextSession.id);
1113
+ // Note: Agent will send session-switched with buffered output
1111
1114
  } else {
1112
1115
  currentSession = null;
1113
- term.clear();
1114
1116
  term.write('\r\n\x1b[33mSession closed. Click + to create a new session.\x1b[0m\r\n');
1115
1117
  }
1116
1118
  }
@@ -1258,14 +1260,20 @@ function handleSessionMessage(message) {
1258
1260
  term.focus();
1259
1261
  break;
1260
1262
  case 'session-ended':
1261
- term.write(`\r\n\x1b[31m❌ Session ended: ${message.reason}\x1b[0m\r\n`);
1262
- if (message.code) {
1263
- term.write(`Exit code: ${message.code}\r\n`);
1263
+ // Only show if this is for the current session (ignore closed session messages)
1264
+ if (!message.sessionId || (currentSession && message.sessionId === currentSession.id)) {
1265
+ term.write(`\r\n\x1b[31m❌ Session ended: ${message.reason}\x1b[0m\r\n`);
1266
+ if (message.code) {
1267
+ term.write(`Exit code: ${message.code}\r\n`);
1268
+ }
1264
1269
  }
1265
1270
  break;
1266
1271
  case 'session-terminated':
1267
- term.write(`\r\n\x1b[31m❌ Session terminated\x1b[0m\r\n`);
1268
- term.write('🔄 Click Dashboard to start a new session\r\n');
1272
+ // Only show if this is for the current session (ignore closed session messages)
1273
+ if (!message.sessionId || (currentSession && message.sessionId === currentSession.id)) {
1274
+ term.write(`\r\n\x1b[31m❌ Session terminated\x1b[0m\r\n`);
1275
+ term.write('🔄 Click Dashboard to start a new session\r\n');
1276
+ }
1269
1277
  break;
1270
1278
  case 'session-closed':
1271
1279
  console.log('[CLIENT] ✅ Session closed confirmed:', message.sessionId);