shell-mirror 1.5.40 → 1.5.42
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-debug.log +9 -94
- package/mac-agent/agent.js +363 -64
- package/package.json +1 -1
- package/public/app/dashboard.css +172 -0
- package/public/app/dashboard.js +159 -6
- package/public/app/terminal.html +118 -1
- package/public/app/terminal.js +212 -4
package/public/app/terminal.js
CHANGED
|
@@ -50,6 +50,11 @@ let AGENT_ID;
|
|
|
50
50
|
let CLIENT_ID;
|
|
51
51
|
let SELECTED_AGENT; // Store full agent data including WebSocket URL
|
|
52
52
|
|
|
53
|
+
// Session management
|
|
54
|
+
let currentSession = null;
|
|
55
|
+
let availableSessions = [];
|
|
56
|
+
let requestedSessionId = null; // For connecting to specific session from URL
|
|
57
|
+
|
|
53
58
|
// Connection status management
|
|
54
59
|
function updateConnectionStatus(status) {
|
|
55
60
|
const statusElement = document.getElementById('connection-status');
|
|
@@ -74,14 +79,16 @@ function updateConnectionStatus(status) {
|
|
|
74
79
|
window.addEventListener('load', () => {
|
|
75
80
|
loadVersionInfo();
|
|
76
81
|
|
|
77
|
-
// Get agent ID from URL
|
|
82
|
+
// Get agent ID and session ID from URL parameters
|
|
78
83
|
const urlParams = new URLSearchParams(window.location.search);
|
|
79
84
|
const agentId = urlParams.get('agent');
|
|
85
|
+
const sessionId = urlParams.get('session');
|
|
80
86
|
|
|
81
87
|
if (agentId) {
|
|
82
88
|
AGENT_ID = agentId;
|
|
83
89
|
SELECTED_AGENT = { id: agentId, agentId: agentId };
|
|
84
|
-
|
|
90
|
+
requestedSessionId = sessionId; // Store for session request
|
|
91
|
+
console.log('[CLIENT] 🔗 Connecting to agent:', agentId, sessionId ? `session: ${sessionId}` : '(new session)');
|
|
85
92
|
startConnection();
|
|
86
93
|
} else {
|
|
87
94
|
// No agent specified, redirect to dashboard
|
|
@@ -119,7 +126,7 @@ async function loadVersionInfo() {
|
|
|
119
126
|
function startConnection() {
|
|
120
127
|
updateConnectionStatus('connecting');
|
|
121
128
|
connectContainer.style.display = 'none';
|
|
122
|
-
terminalContainer.
|
|
129
|
+
terminalContainer.classList.add('show');
|
|
123
130
|
term.open(document.getElementById('terminal'));
|
|
124
131
|
// Delay fit to ensure proper dimensions after CSS transitions
|
|
125
132
|
setTimeout(() => {
|
|
@@ -164,7 +171,23 @@ async function initialize() {
|
|
|
164
171
|
// Start polling to connect to the agent
|
|
165
172
|
const intervalId = setInterval(() => {
|
|
166
173
|
console.log(`[CLIENT] 📞 Sending client-hello to Agent: ${AGENT_ID}`);
|
|
167
|
-
|
|
174
|
+
|
|
175
|
+
// Build session request
|
|
176
|
+
let sessionRequest = null;
|
|
177
|
+
if (requestedSessionId) {
|
|
178
|
+
sessionRequest = { sessionId: requestedSessionId };
|
|
179
|
+
console.log(`[CLIENT] 🎯 Requesting existing session: ${requestedSessionId}`);
|
|
180
|
+
} else {
|
|
181
|
+
sessionRequest = { newSession: true };
|
|
182
|
+
console.log(`[CLIENT] 🆕 Requesting new session`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const sent = sendMessage({
|
|
186
|
+
type: 'client-hello',
|
|
187
|
+
from: CLIENT_ID,
|
|
188
|
+
to: AGENT_ID,
|
|
189
|
+
sessionRequest: sessionRequest
|
|
190
|
+
});
|
|
168
191
|
if (!sent) {
|
|
169
192
|
console.error(`[CLIENT] ❌ Failed to send client-hello - stopping attempts`);
|
|
170
193
|
clearInterval(intervalId);
|
|
@@ -190,6 +213,27 @@ async function initialize() {
|
|
|
190
213
|
console.log('[CLIENT] Received offer from agent. Stopping client-hello retries.');
|
|
191
214
|
clearInterval(intervalId);
|
|
192
215
|
|
|
216
|
+
// Handle session assignment from agent
|
|
217
|
+
if (nextData.sessionId) {
|
|
218
|
+
currentSession = {
|
|
219
|
+
id: nextData.sessionId,
|
|
220
|
+
name: nextData.sessionName || 'Terminal Session',
|
|
221
|
+
isNewSession: nextData.isNewSession || false
|
|
222
|
+
};
|
|
223
|
+
console.log('[CLIENT] 📋 Session assigned:', currentSession);
|
|
224
|
+
|
|
225
|
+
// Update UI to show session info
|
|
226
|
+
updateSessionDisplay();
|
|
227
|
+
|
|
228
|
+
// Save session info to localStorage for dashboard
|
|
229
|
+
saveSessionToLocalStorage(AGENT_ID, currentSession);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (nextData.availableSessions) {
|
|
233
|
+
availableSessions = nextData.availableSessions;
|
|
234
|
+
console.log('[CLIENT] 📚 Available sessions:', availableSessions);
|
|
235
|
+
}
|
|
236
|
+
|
|
193
237
|
console.log('[CLIENT] Received WebRTC offer from agent.');
|
|
194
238
|
await createPeerConnection();
|
|
195
239
|
await peerConnection.setRemoteDescription(new RTCSessionDescription(nextData));
|
|
@@ -459,6 +503,9 @@ function setupDataChannel() {
|
|
|
459
503
|
const message = JSON.parse(event.data);
|
|
460
504
|
if (message.type === 'output') {
|
|
461
505
|
term.write(message.data);
|
|
506
|
+
} else {
|
|
507
|
+
// Handle session-related messages
|
|
508
|
+
handleSessionMessage(message);
|
|
462
509
|
}
|
|
463
510
|
} catch (err) {
|
|
464
511
|
console.error('[CLIENT] Error parsing data channel message:', err);
|
|
@@ -521,4 +568,165 @@ function sendMessage(message) {
|
|
|
521
568
|
console.error(`[CLIENT] ❌ Error sending message:`, error);
|
|
522
569
|
return false;
|
|
523
570
|
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Session Management Functions
|
|
574
|
+
function updateSessionDisplay() {
|
|
575
|
+
const sessionHeader = document.getElementById('session-header');
|
|
576
|
+
const sessionName = document.getElementById('session-name');
|
|
577
|
+
const sessionId = document.getElementById('session-id');
|
|
578
|
+
|
|
579
|
+
if (currentSession) {
|
|
580
|
+
sessionHeader.style.display = 'flex';
|
|
581
|
+
sessionName.textContent = currentSession.name;
|
|
582
|
+
sessionId.textContent = `(${currentSession.id.substring(0, 8)}...)`;
|
|
583
|
+
|
|
584
|
+
// Update available sessions dropdown
|
|
585
|
+
updateSessionDropdown();
|
|
586
|
+
|
|
587
|
+
console.log('[CLIENT] 📋 Session display updated:', currentSession);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function updateSessionDropdown() {
|
|
592
|
+
const dropdownContent = document.getElementById('session-dropdown-content');
|
|
593
|
+
|
|
594
|
+
// Clear existing items except "New Session"
|
|
595
|
+
dropdownContent.innerHTML = `
|
|
596
|
+
<div class="session-dropdown-item" onclick="createNewSession()">
|
|
597
|
+
<span>+ New Session</span>
|
|
598
|
+
</div>
|
|
599
|
+
`;
|
|
600
|
+
|
|
601
|
+
// Add available sessions
|
|
602
|
+
if (availableSessions && availableSessions.length > 0) {
|
|
603
|
+
availableSessions.forEach(session => {
|
|
604
|
+
const item = document.createElement('div');
|
|
605
|
+
item.className = 'session-dropdown-item';
|
|
606
|
+
if (currentSession && session.id === currentSession.id) {
|
|
607
|
+
item.classList.add('current');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
item.innerHTML = `
|
|
611
|
+
<span>${session.name}</span>
|
|
612
|
+
<small>${formatLastActivity(session.lastActivity)}</small>
|
|
613
|
+
`;
|
|
614
|
+
item.onclick = () => switchToSession(session.id);
|
|
615
|
+
dropdownContent.appendChild(item);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function formatLastActivity(timestamp) {
|
|
621
|
+
const now = Date.now();
|
|
622
|
+
const diff = now - timestamp;
|
|
623
|
+
const minutes = Math.floor(diff / 60000);
|
|
624
|
+
const hours = Math.floor(diff / 3600000);
|
|
625
|
+
const days = Math.floor(diff / 86400000);
|
|
626
|
+
|
|
627
|
+
if (minutes < 1) return 'now';
|
|
628
|
+
if (minutes < 60) return `${minutes}m`;
|
|
629
|
+
if (hours < 24) return `${hours}h`;
|
|
630
|
+
return `${days}d`;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function switchToSession(sessionId) {
|
|
634
|
+
if (!dataChannel || dataChannel.readyState !== 'open') {
|
|
635
|
+
console.error('[CLIENT] ❌ Cannot switch session - data channel not open');
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
console.log('[CLIENT] 🔄 Switching to session:', sessionId);
|
|
640
|
+
dataChannel.send(JSON.stringify({
|
|
641
|
+
type: 'session-switch',
|
|
642
|
+
sessionId: sessionId
|
|
643
|
+
}));
|
|
644
|
+
|
|
645
|
+
// Hide dropdown
|
|
646
|
+
document.getElementById('session-dropdown-content').classList.remove('show');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function createNewSession() {
|
|
650
|
+
// Navigate to terminal with new session request
|
|
651
|
+
const url = new URL(window.location.href);
|
|
652
|
+
url.searchParams.delete('session'); // Remove session param to create new one
|
|
653
|
+
window.location.href = url.toString();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Setup dropdown toggle
|
|
657
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
658
|
+
const dropdownBtn = document.getElementById('session-dropdown-btn');
|
|
659
|
+
const dropdownContent = document.getElementById('session-dropdown-content');
|
|
660
|
+
|
|
661
|
+
if (dropdownBtn && dropdownContent) {
|
|
662
|
+
dropdownBtn.onclick = (e) => {
|
|
663
|
+
e.stopPropagation();
|
|
664
|
+
dropdownContent.classList.toggle('show');
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// Close dropdown when clicking outside
|
|
668
|
+
document.addEventListener('click', () => {
|
|
669
|
+
dropdownContent.classList.remove('show');
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Handle session-related data channel messages
|
|
675
|
+
function handleSessionMessage(message) {
|
|
676
|
+
switch (message.type) {
|
|
677
|
+
case 'session-switched':
|
|
678
|
+
currentSession = {
|
|
679
|
+
id: message.sessionId,
|
|
680
|
+
name: message.sessionName || 'Terminal Session'
|
|
681
|
+
};
|
|
682
|
+
updateSessionDisplay();
|
|
683
|
+
term.clear(); // Clear terminal for new session
|
|
684
|
+
console.log('[CLIENT] ✅ Switched to session:', currentSession);
|
|
685
|
+
|
|
686
|
+
// Save updated session info
|
|
687
|
+
saveSessionToLocalStorage(AGENT_ID, currentSession);
|
|
688
|
+
break;
|
|
689
|
+
case 'session-ended':
|
|
690
|
+
term.write(`\r\n\x1b[31m❌ Session ended: ${message.reason}\x1b[0m\r\n`);
|
|
691
|
+
if (message.code) {
|
|
692
|
+
term.write(`Exit code: ${message.code}\r\n`);
|
|
693
|
+
}
|
|
694
|
+
break;
|
|
695
|
+
case 'session-terminated':
|
|
696
|
+
term.write(`\r\n\x1b[31m❌ Session terminated\x1b[0m\r\n`);
|
|
697
|
+
term.write('🔄 Click Dashboard to start a new session\r\n');
|
|
698
|
+
break;
|
|
699
|
+
case 'error':
|
|
700
|
+
term.write(`\r\n\x1b[31m❌ Error: ${message.message}\x1b[0m\r\n`);
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Session storage helper
|
|
706
|
+
function saveSessionToLocalStorage(agentId, sessionInfo) {
|
|
707
|
+
try {
|
|
708
|
+
const storedSessions = localStorage.getItem('shell-mirror-sessions');
|
|
709
|
+
let sessionData = storedSessions ? JSON.parse(storedSessions) : {};
|
|
710
|
+
|
|
711
|
+
if (!sessionData[agentId]) {
|
|
712
|
+
sessionData[agentId] = [];
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Remove existing session with same ID
|
|
716
|
+
sessionData[agentId] = sessionData[agentId].filter(s => s.id !== sessionInfo.id);
|
|
717
|
+
|
|
718
|
+
// Add updated session info
|
|
719
|
+
sessionData[agentId].push({
|
|
720
|
+
id: sessionInfo.id,
|
|
721
|
+
name: sessionInfo.name,
|
|
722
|
+
lastActivity: Date.now(),
|
|
723
|
+
createdAt: sessionInfo.createdAt || Date.now(),
|
|
724
|
+
status: 'active'
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
localStorage.setItem('shell-mirror-sessions', JSON.stringify(sessionData));
|
|
728
|
+
console.log('[CLIENT] 💾 Session saved to storage:', sessionInfo);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
console.error('[CLIENT] Error saving session to storage:', error);
|
|
731
|
+
}
|
|
524
732
|
}
|