shell-mirror 1.5.79 → 1.5.81

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.79",
3
+ "version": "1.5.81",
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": {
@@ -46,6 +46,7 @@ class ShellMirrorDashboard {
46
46
  this.user = authStatus.user;
47
47
  await this.loadDashboardData();
48
48
  this.renderAuthenticatedDashboard();
49
+ this.updateLastRefreshTime();
49
50
  this.enableHttpOnlyMode(); // Use HTTP-only mode (no persistent WebSocket)
50
51
  this.startAutoRefresh(); // Start auto-refresh for authenticated users
51
52
  } else {
@@ -99,7 +100,7 @@ class ShellMirrorDashboard {
99
100
  this.lastRefresh = Date.now();
100
101
  const refreshStatus = document.getElementById('refresh-status');
101
102
  if (refreshStatus) {
102
- refreshStatus.textContent = `Last updated: ${new Date(this.lastRefresh).toLocaleTimeString()}`;
103
+ refreshStatus.textContent = `Agents updated: ${new Date(this.lastRefresh).toLocaleTimeString()}`;
103
104
  }
104
105
  }
105
106
 
@@ -400,7 +401,7 @@ class ShellMirrorDashboard {
400
401
  const data = await response.json();
401
402
 
402
403
  if (data.success && data.data && data.data.authenticated) {
403
- return { isAuthenticated: true, user: data.data };
404
+ return { isAuthenticated: true, user: data.data.user };
404
405
  }
405
406
  } catch (error) {
406
407
  console.log('Auth check failed:', error);
@@ -110,154 +110,101 @@
110
110
  }
111
111
 
112
112
  /* Session Info Button */
113
- .session-info-btn {
114
- background: transparent;
115
- border: 1px solid #444;
116
- color: #ccc;
117
- padding: 4px 12px;
118
- border-radius: 4px;
119
- cursor: pointer;
113
+ /* Session Tab Bar */
114
+ .session-tab-bar {
120
115
  display: flex;
116
+ gap: 4px;
117
+ padding: 8px;
118
+ background: #2a2a2a;
119
+ overflow-x: auto;
120
+ -webkit-overflow-scrolling: touch;
121
+ flex: 1;
121
122
  align-items: center;
122
- gap: 8px;
123
- font-size: 0.9em;
124
- transition: all 0.2s ease;
125
123
  }
126
124
 
127
- .session-info-btn:hover {
128
- background: #3a3a3a;
129
- border-color: #667eea;
125
+ .session-tab-bar::-webkit-scrollbar {
126
+ height: 4px;
130
127
  }
131
128
 
132
- .session-name {
133
- font-weight: bold;
134
- color: #fff;
129
+ .session-tab-bar::-webkit-scrollbar-thumb {
130
+ background: #555;
131
+ border-radius: 2px;
135
132
  }
136
133
 
137
- .session-id {
134
+ .session-tab {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 6px;
138
+ padding: 8px 12px;
139
+ background: transparent;
140
+ border: none;
141
+ border-bottom: 3px solid transparent;
138
142
  color: #999;
139
- font-size: 0.85em;
140
- }
141
-
142
- .dropdown-arrow {
143
- color: #888;
144
- font-size: 0.7em;
145
- }
146
-
147
- /* Session Info Dropdown */
148
- .session-info-dropdown {
149
- display: none;
150
- position: absolute;
151
- top: 100%;
152
- left: 0;
153
- margin-top: 8px;
154
- background: #2a2a2a;
155
- border: 1px solid #555;
156
- border-radius: 4px;
157
- min-width: 280px;
158
- box-shadow: 0 4px 12px rgba(0,0,0,0.5);
159
- z-index: 1000;
160
- }
161
-
162
- .session-info-dropdown.show {
163
- display: block;
164
- }
165
-
166
- .dropdown-section {
167
- padding: 12px 16px;
143
+ cursor: pointer;
144
+ min-width: 120px;
145
+ white-space: nowrap;
146
+ transition: all 0.2s ease;
147
+ flex-shrink: 0;
168
148
  }
169
149
 
170
- .dropdown-divider {
171
- height: 1px;
172
- background: #444;
150
+ .session-tab:hover {
151
+ background: #3a3a3a;
152
+ color: #ccc;
173
153
  }
174
154
 
175
- .session-detail-name {
176
- font-weight: bold;
155
+ .session-tab.active {
177
156
  color: #fff;
178
- margin-bottom: 4px;
179
- }
180
-
181
- .session-detail-id {
182
- color: #999;
183
- font-size: 0.85em;
184
- font-family: monospace;
157
+ font-weight: 600;
158
+ border-bottom-color: #667eea;
159
+ background: #1a1a1a;
185
160
  }
186
161
 
187
- .connection-detail {
188
- color: #ccc;
189
- font-size: 0.9em;
162
+ .session-tab-status {
163
+ width: 8px;
164
+ height: 8px;
165
+ border-radius: 50%;
166
+ flex-shrink: 0;
190
167
  }
191
168
 
192
- .version-info {
193
- color: #888;
194
- font-size: 0.85em;
169
+ .session-tab-status.connected {
170
+ background: #44ff44;
171
+ box-shadow: 0 0 6px rgba(68, 255, 68, 0.5);
195
172
  }
196
173
 
197
- /* Sessions Dropdown */
198
- .session-dropdown {
199
- position: relative;
200
- display: inline-block;
174
+ .session-tab-status.connecting {
175
+ background: #ffaa44;
176
+ animation: pulse 1.5s ease-in-out infinite;
201
177
  }
202
178
 
203
- .session-dropdown-btn {
204
- background: transparent;
205
- color: #ccc;
206
- border: 1px solid #444;
207
- padding: 4px 12px;
208
- border-radius: 4px;
209
- cursor: pointer;
210
- font-size: 0.9em;
211
- display: flex;
212
- align-items: center;
213
- gap: 4px;
214
- transition: all 0.2s ease;
179
+ .session-tab-status.disconnected {
180
+ background: #ff4444;
215
181
  }
216
182
 
217
- .session-dropdown-btn:hover {
218
- background: #3a3a3a;
219
- border-color: #667eea;
183
+ .session-tab-name {
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
220
186
  }
221
187
 
222
- .session-dropdown-content {
223
- display: none;
224
- position: absolute;
225
- background: #2a2a2a;
226
- border: 1px solid #555;
188
+ .session-tab-new {
189
+ padding: 6px 12px;
190
+ background: #667eea;
227
191
  border-radius: 4px;
228
- left: 50%;
229
- transform: translateX(-50%);
230
- top: 100%;
231
- margin-top: 8px;
232
- min-width: 200px;
233
- z-index: 1000;
234
- box-shadow: 0 4px 8px rgba(0,0,0,0.3);
235
- }
236
-
237
- .session-dropdown-content.show {
238
- display: block;
239
- }
240
-
241
- .session-dropdown-item {
242
- padding: 8px 12px;
192
+ color: white;
193
+ border: none;
243
194
  cursor: pointer;
244
- border-bottom: 1px solid #444;
245
- display: flex;
246
- justify-content: space-between;
247
- align-items: center;
248
- }
249
-
250
- .session-dropdown-item:last-child {
251
- border-bottom: none;
195
+ margin-left: 4px;
196
+ font-weight: 500;
197
+ transition: all 0.2s ease;
198
+ flex-shrink: 0;
252
199
  }
253
200
 
254
- .session-dropdown-item:hover {
255
- background: #3a3a3a;
201
+ .session-tab-new:hover {
202
+ background: #5568d3;
256
203
  }
257
204
 
258
- .session-dropdown-item.current {
259
- background: #4285f4;
260
- color: white;
205
+ @keyframes pulse {
206
+ 0%, 100% { opacity: 1; }
207
+ 50% { opacity: 0.5; }
261
208
  }
262
209
 
263
210
  /* Header Buttons */
@@ -434,47 +381,12 @@
434
381
  </div>
435
382
  <div id="terminal-container">
436
383
  <div class="session-header" id="session-header" style="display: none;">
437
- <div class="header-left">
438
- <div class="connection-status" id="connection-status"></div>
439
- <button class="session-info-btn" id="session-info-btn">
440
- <span class="session-name" id="session-name">Terminal Session</span>
441
- <span class="session-id" id="session-id"></span>
442
- <span class="dropdown-arrow">▼</span>
443
- </button>
444
-
445
- <!-- Session Info Dropdown -->
446
- <div class="session-info-dropdown" id="session-info-dropdown">
447
- <div class="dropdown-section">
448
- <div class="session-detail-name" id="detail-name">Session 1</div>
449
- <div class="session-detail-id" id="detail-id">ses_1764...</div>
450
- </div>
451
- <div class="dropdown-divider"></div>
452
- <div class="dropdown-section">
453
- <div class="connection-detail" id="connection-detail">
454
- 🟢 Connected via WebRTC
455
- </div>
456
- </div>
457
- <div class="dropdown-divider"></div>
458
- <div class="dropdown-section version-info">
459
- <div id="version-info-dropdown">v1.5.76 • Built Nov 26, 2025</div>
460
- </div>
461
- </div>
462
- </div>
463
-
464
- <div class="header-center">
465
- <div class="session-dropdown">
466
- <button class="session-dropdown-btn" id="session-dropdown-btn">
467
- <span>Sessions</span>
468
- <span>▼</span>
469
- </button>
470
- <div class="session-dropdown-content" id="session-dropdown-content">
471
- <div class="session-dropdown-item" onclick="createNewSession()">
472
- <span>+ New Session</span>
473
- </div>
474
- </div>
475
- </div>
384
+ <!-- Session Tab Bar -->
385
+ <div class="session-tab-bar" id="session-tab-bar">
386
+ <!-- Tabs will be rendered here by JavaScript -->
476
387
  </div>
477
388
 
389
+ <!-- Header Right Section -->
478
390
  <div class="header-right">
479
391
  <button class="header-btn help-btn" onclick="showHelpModal()">
480
392
  <span>📖</span>
@@ -170,8 +170,10 @@ function updateConnectionStatus(status) {
170
170
  break;
171
171
  }
172
172
 
173
- // Update connection detail in dropdown
174
- updateConnectionDetail(status);
173
+ // Update tab status indicators
174
+ if (currentSession) {
175
+ renderTabs();
176
+ }
175
177
  }
176
178
 
177
179
  // Cleanup timer for chunk assembler
@@ -246,42 +248,6 @@ async function loadVersionInfo() {
246
248
  }
247
249
 
248
250
  // Update connection detail in dropdown
249
- function updateConnectionDetail(status, method = 'WebRTC') {
250
- const detail = document.getElementById('connection-detail');
251
- if (!detail) return;
252
-
253
- const statusIcons = {
254
- connected: '🟢',
255
- connecting: '🟡',
256
- disconnected: '🔴'
257
- };
258
-
259
- const statusTexts = {
260
- connected: 'Connected',
261
- connecting: 'Connecting',
262
- disconnected: 'Disconnected'
263
- };
264
-
265
- detail.textContent = `${statusIcons[status] || '⚪'} ${statusTexts[status] || 'Unknown'} via ${method}`;
266
- }
267
-
268
- // Update session details in dropdown
269
- function updateSessionDetails() {
270
- if (currentSession) {
271
- const detailName = document.getElementById('detail-name');
272
- const detailId = document.getElementById('detail-id');
273
-
274
- if (detailName) {
275
- detailName.textContent = currentSession.name || 'Terminal Session';
276
- }
277
- if (detailId) {
278
- detailId.textContent = `ID: ${currentSession.id}`;
279
- }
280
- }
281
- }
282
-
283
-
284
-
285
251
  function startConnection() {
286
252
  updateConnectionStatus('connecting');
287
253
  connectContainer.style.display = 'none';
@@ -957,50 +923,54 @@ function sendMessage(message) {
957
923
  // Session Management Functions
958
924
  function updateSessionDisplay() {
959
925
  const sessionHeader = document.getElementById('session-header');
960
- const sessionName = document.getElementById('session-name');
961
- const sessionId = document.getElementById('session-id');
962
926
 
963
927
  if (currentSession) {
964
928
  sessionHeader.style.display = 'flex';
965
- sessionName.textContent = currentSession.name;
966
- sessionId.textContent = `(${currentSession.id.substring(0, 8)}...)`;
967
-
968
- // Update available sessions dropdown
969
- updateSessionDropdown();
970
929
 
971
- // Update session details in dropdown
972
- updateSessionDetails();
930
+ // Update tabs
931
+ renderTabs();
973
932
 
974
933
  console.log('[CLIENT] 📋 Session display updated:', currentSession);
975
934
  }
976
935
  }
977
936
 
978
- function updateSessionDropdown() {
979
- const dropdownContent = document.getElementById('session-dropdown-content');
980
-
981
- // Clear existing items except "New Session"
982
- dropdownContent.innerHTML = `
983
- <div class="session-dropdown-item" onclick="createNewSession()">
984
- <span>+ New Session</span>
985
- </div>
986
- `;
987
-
988
- // Add available sessions
937
+ function renderTabs() {
938
+ const tabBar = document.getElementById('session-tab-bar');
939
+ if (!tabBar) return;
940
+
941
+ // Get connection status
942
+ const connectionStatus = getConnectionStatus();
943
+
944
+ // Build tabs HTML
945
+ let tabsHTML = '';
946
+
989
947
  if (availableSessions && availableSessions.length > 0) {
990
- availableSessions.forEach(session => {
991
- const item = document.createElement('div');
992
- item.className = 'session-dropdown-item';
993
- if (currentSession && session.id === currentSession.id) {
994
- item.classList.add('current');
995
- }
996
-
997
- item.innerHTML = `
998
- <span>${session.name}</span>
999
- <small>${formatLastActivity(session.lastActivity)}</small>
948
+ tabsHTML = availableSessions.map(session => {
949
+ const isActive = currentSession && session.id === currentSession.id;
950
+ return `
951
+ <button class="session-tab ${isActive ? 'active' : ''}"
952
+ onclick="switchToSession('${session.id}')"
953
+ ${isActive ? 'disabled' : ''}>
954
+ <span class="session-tab-status ${connectionStatus}"></span>
955
+ <span class="session-tab-name">${session.name}</span>
956
+ </button>
1000
957
  `;
1001
- item.onclick = () => switchToSession(session.id);
1002
- dropdownContent.appendChild(item);
1003
- });
958
+ }).join('');
959
+ }
960
+
961
+ // Add new session button
962
+ tabsHTML += '<button class="session-tab-new" onclick="createNewSession()">+</button>';
963
+
964
+ tabBar.innerHTML = tabsHTML;
965
+ }
966
+
967
+ function getConnectionStatus() {
968
+ if (dataChannel && dataChannel.readyState === 'open') {
969
+ return 'connected';
970
+ } else if (dataChannel && dataChannel.readyState === 'connecting') {
971
+ return 'connecting';
972
+ } else {
973
+ return 'disconnected';
1004
974
  }
1005
975
  }
1006
976
 
@@ -1022,15 +992,12 @@ function switchToSession(sessionId) {
1022
992
  console.error('[CLIENT] ❌ Cannot switch session - data channel not open');
1023
993
  return;
1024
994
  }
1025
-
995
+
1026
996
  console.log('[CLIENT] 🔄 Switching to session:', sessionId);
1027
- dataChannel.send(JSON.stringify({
1028
- type: 'session-switch',
1029
- sessionId: sessionId
997
+ dataChannel.send(JSON.stringify({
998
+ type: 'session-switch',
999
+ sessionId: sessionId
1030
1000
  }));
1031
-
1032
- // Hide dropdown
1033
- document.getElementById('session-dropdown-content').classList.remove('show');
1034
1001
  }
1035
1002
 
1036
1003
  function createNewSession() {
@@ -1040,50 +1007,6 @@ function createNewSession() {
1040
1007
  window.location.href = url.toString();
1041
1008
  }
1042
1009
 
1043
- // Setup dropdown toggles
1044
- document.addEventListener('DOMContentLoaded', () => {
1045
- // Session info dropdown
1046
- const sessionInfoBtn = document.getElementById('session-info-btn');
1047
- const sessionInfoDropdown = document.getElementById('session-info-dropdown');
1048
-
1049
- // Sessions menu dropdown
1050
- const dropdownBtn = document.getElementById('session-dropdown-btn');
1051
- const dropdownContent = document.getElementById('session-dropdown-content');
1052
-
1053
- // Session info dropdown toggle
1054
- if (sessionInfoBtn && sessionInfoDropdown) {
1055
- sessionInfoBtn.onclick = (e) => {
1056
- e.stopPropagation();
1057
- sessionInfoDropdown.classList.toggle('show');
1058
- // Close sessions dropdown if open
1059
- if (dropdownContent) {
1060
- dropdownContent.classList.remove('show');
1061
- }
1062
- };
1063
- }
1064
-
1065
- // Sessions menu dropdown toggle
1066
- if (dropdownBtn && dropdownContent) {
1067
- dropdownBtn.onclick = (e) => {
1068
- e.stopPropagation();
1069
- dropdownContent.classList.toggle('show');
1070
- // Close session info dropdown if open
1071
- if (sessionInfoDropdown) {
1072
- sessionInfoDropdown.classList.remove('show');
1073
- }
1074
- };
1075
- }
1076
-
1077
- // Close both dropdowns when clicking outside
1078
- document.addEventListener('click', () => {
1079
- if (dropdownContent) {
1080
- dropdownContent.classList.remove('show');
1081
- }
1082
- if (sessionInfoDropdown) {
1083
- sessionInfoDropdown.classList.remove('show');
1084
- }
1085
- });
1086
- });
1087
1010
 
1088
1011
  // Handle session-related data channel messages
1089
1012
  function handleSessionMessage(message) {