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.
- package/mac-agent/agent.js +86 -34
- package/package.json +1 -1
- package/public/app/dashboard.js +11 -2
- package/public/app/terminal.html +77 -0
- package/public/app/terminal.js +13 -3
package/mac-agent/agent.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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:
|
|
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
|
-
//
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
-
|
|
1060
|
-
|
|
1061
|
-
if (
|
|
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 (
|
|
1084
|
-
|
|
1085
|
-
|
|
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 (
|
|
1092
|
-
|
|
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
package/public/app/dashboard.js
CHANGED
|
@@ -383,7 +383,8 @@ class ShellMirrorDashboard {
|
|
|
383
383
|
updateAgentsDisplay() {
|
|
384
384
|
const agentsCard = document.querySelector('.dashboard-card');
|
|
385
385
|
if (agentsCard) {
|
|
386
|
-
|
|
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
|
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
|
|
|
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
|
+
|
|
505
582
|
<script src="/app/terminal.js?v=1.5.88"></script>
|
|
506
583
|
</body>
|
|
507
584
|
</html>
|
package/public/app/terminal.js
CHANGED
|
@@ -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
|
-
|
|
1036
|
-
|
|
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
|