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.
- package/mac-agent/agent.js +15 -2
- package/package.json +1 -1
- package/public/app/dashboard.css +43 -0
- package/public/app/dashboard.js +22 -11
- package/public/app/terminal.html +78 -1
- package/public/app/terminal.js +53 -6
package/mac-agent/agent.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
package/public/app/dashboard.css
CHANGED
|
@@ -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;
|
package/public/app/dashboard.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
632
|
-
<span class="agent-count">${agentCount}
|
|
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="
|
|
636
|
-
|
|
646
|
+
<button id="refresh-btn" class="btn-text-action" onclick="dashboard.manualRefresh()" title="Refresh agents">
|
|
647
|
+
Refresh
|
|
637
648
|
</button>
|
|
638
|
-
${showCleanup ? `<button class="
|
|
639
|
-
|
|
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
|
|
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
|
|
694
|
+
<p>Your agent will appear here once connected</p>
|
|
684
695
|
</div>
|
|
685
696
|
</div>
|
|
686
697
|
`;
|
package/public/app/terminal.html
CHANGED
|
@@ -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
|
-
|
|
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;">×</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>
|
package/public/app/terminal.js
CHANGED
|
@@ -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
|
-
${
|
|
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
|
-
|
|
1036
|
-
|
|
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`);
|