shell-mirror 1.5.39 → 1.5.41

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.
@@ -50,18 +50,45 @@ 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
+
58
+ // Connection status management
59
+ function updateConnectionStatus(status) {
60
+ const statusElement = document.getElementById('connection-status');
61
+ if (!statusElement) return;
62
+
63
+ statusElement.className = 'connection-status';
64
+ switch(status) {
65
+ case 'connecting':
66
+ statusElement.classList.add('connecting');
67
+ break;
68
+ case 'connected':
69
+ statusElement.classList.add('connected');
70
+ break;
71
+ case 'disconnected':
72
+ default:
73
+ // Default red styling already applied
74
+ break;
75
+ }
76
+ }
77
+
53
78
  // Check for agent parameter and connect directly
54
79
  window.addEventListener('load', () => {
55
80
  loadVersionInfo();
56
81
 
57
- // Get agent ID from URL parameter
82
+ // Get agent ID and session ID from URL parameters
58
83
  const urlParams = new URLSearchParams(window.location.search);
59
84
  const agentId = urlParams.get('agent');
85
+ const sessionId = urlParams.get('session');
60
86
 
61
87
  if (agentId) {
62
88
  AGENT_ID = agentId;
63
89
  SELECTED_AGENT = { id: agentId, agentId: agentId };
64
- console.log('[CLIENT] 🔗 Connecting directly to agent:', agentId);
90
+ requestedSessionId = sessionId; // Store for session request
91
+ console.log('[CLIENT] 🔗 Connecting to agent:', agentId, sessionId ? `session: ${sessionId}` : '(new session)');
65
92
  startConnection();
66
93
  } else {
67
94
  // No agent specified, redirect to dashboard
@@ -97,8 +124,9 @@ async function loadVersionInfo() {
97
124
 
98
125
 
99
126
  function startConnection() {
127
+ updateConnectionStatus('connecting');
100
128
  connectContainer.style.display = 'none';
101
- terminalContainer.style.display = 'block';
129
+ terminalContainer.classList.add('show');
102
130
  term.open(document.getElementById('terminal'));
103
131
  // Delay fit to ensure proper dimensions after CSS transitions
104
132
  setTimeout(() => {
@@ -143,7 +171,23 @@ async function initialize() {
143
171
  // Start polling to connect to the agent
144
172
  const intervalId = setInterval(() => {
145
173
  console.log(`[CLIENT] 📞 Sending client-hello to Agent: ${AGENT_ID}`);
146
- const sent = sendMessage({ type: 'client-hello', from: CLIENT_ID, to: AGENT_ID });
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
+ });
147
191
  if (!sent) {
148
192
  console.error(`[CLIENT] ❌ Failed to send client-hello - stopping attempts`);
149
193
  clearInterval(intervalId);
@@ -169,6 +213,24 @@ async function initialize() {
169
213
  console.log('[CLIENT] Received offer from agent. Stopping client-hello retries.');
170
214
  clearInterval(intervalId);
171
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
+
229
+ if (nextData.availableSessions) {
230
+ availableSessions = nextData.availableSessions;
231
+ console.log('[CLIENT] 📚 Available sessions:', availableSessions);
232
+ }
233
+
172
234
  console.log('[CLIENT] Received WebRTC offer from agent.');
173
235
  await createPeerConnection();
174
236
  await peerConnection.setRemoteDescription(new RTCSessionDescription(nextData));
@@ -346,25 +408,31 @@ async function createPeerConnection() {
346
408
  break;
347
409
  case 'connected':
348
410
  console.log('[CLIENT] ✅ WebRTC connection established!');
411
+ updateConnectionStatus('connected');
349
412
  break;
350
413
  case 'completed':
351
414
  console.log('[CLIENT] ✅ ICE connection completed successfully!');
415
+ updateConnectionStatus('connected');
352
416
  break;
353
417
  case 'failed':
354
418
  console.log('[CLIENT] ❌ ICE connection failed - no viable candidates');
355
419
  console.log('[CLIENT] 💡 Troubleshooting: This may be due to firewall/NAT issues or blocked STUN servers');
420
+ updateConnectionStatus('disconnected');
356
421
  term.write('\r\n\r\n❌ Connection failed: Network connectivity issues\r\n');
357
422
  term.write('💡 This may be due to:\r\n');
358
423
  term.write(' • Firewall blocking WebRTC traffic\r\n');
359
424
  term.write(' • Corporate network restrictions\r\n');
360
425
  term.write(' • STUN/TURN servers unreachable\r\n');
361
- term.write('\r\n🔄 Please refresh the page to retry...\r\n');
426
+ term.write(' Agent may have crashed or disconnected\r\n');
427
+ term.write('\r\n🔄 Click Dashboard to return and try another agent\r\n');
362
428
  break;
363
429
  case 'disconnected':
364
430
  console.log('[CLIENT] ⚠️ ICE connection disconnected');
431
+ updateConnectionStatus('disconnected');
365
432
  break;
366
433
  case 'closed':
367
434
  console.log('[CLIENT] 🔐 ICE connection closed');
435
+ updateConnectionStatus('disconnected');
368
436
  break;
369
437
  }
370
438
  };
@@ -432,6 +500,9 @@ function setupDataChannel() {
432
500
  const message = JSON.parse(event.data);
433
501
  if (message.type === 'output') {
434
502
  term.write(message.data);
503
+ } else {
504
+ // Handle session-related messages
505
+ handleSessionMessage(message);
435
506
  }
436
507
  } catch (err) {
437
508
  console.error('[CLIENT] Error parsing data channel message:', err);
@@ -440,12 +511,16 @@ function setupDataChannel() {
440
511
 
441
512
  dataChannel.onclose = () => {
442
513
  console.log('[CLIENT] Data channel closed.');
514
+ updateConnectionStatus('disconnected');
443
515
  term.write('\r\n\r\n\x1b[31m❌ Terminal session ended.\x1b[0m\r\n');
516
+ term.write('🔄 Click Dashboard to return and start a new session\r\n');
444
517
  };
445
518
 
446
519
  dataChannel.onerror = (error) => {
447
520
  console.error('[CLIENT] Data channel error:', error);
521
+ updateConnectionStatus('disconnected');
448
522
  term.write('\r\n\r\n\x1b[31m❌ Data channel error occurred.\x1b[0m\r\n');
523
+ term.write('🔄 Click Dashboard to return and try again\r\n');
449
524
  };
450
525
 
451
526
  term.onData((data) => {
@@ -490,4 +565,133 @@ function sendMessage(message) {
490
565
  console.error(`[CLIENT] ❌ Error sending message:`, error);
491
566
  return false;
492
567
  }
568
+ }
569
+
570
+ // Session Management Functions
571
+ function updateSessionDisplay() {
572
+ const sessionHeader = document.getElementById('session-header');
573
+ const sessionName = document.getElementById('session-name');
574
+ const sessionId = document.getElementById('session-id');
575
+
576
+ if (currentSession) {
577
+ sessionHeader.style.display = 'flex';
578
+ sessionName.textContent = currentSession.name;
579
+ sessionId.textContent = `(${currentSession.id.substring(0, 8)}...)`;
580
+
581
+ // Update available sessions dropdown
582
+ updateSessionDropdown();
583
+
584
+ console.log('[CLIENT] 📋 Session display updated:', currentSession);
585
+ }
586
+ }
587
+
588
+ function updateSessionDropdown() {
589
+ const dropdownContent = document.getElementById('session-dropdown-content');
590
+
591
+ // Clear existing items except "New Session"
592
+ dropdownContent.innerHTML = `
593
+ <div class="session-dropdown-item" onclick="createNewSession()">
594
+ <span>+ New Session</span>
595
+ </div>
596
+ `;
597
+
598
+ // Add available sessions
599
+ if (availableSessions && availableSessions.length > 0) {
600
+ availableSessions.forEach(session => {
601
+ const item = document.createElement('div');
602
+ item.className = 'session-dropdown-item';
603
+ if (currentSession && session.id === currentSession.id) {
604
+ item.classList.add('current');
605
+ }
606
+
607
+ item.innerHTML = `
608
+ <span>${session.name}</span>
609
+ <small>${formatLastActivity(session.lastActivity)}</small>
610
+ `;
611
+ item.onclick = () => switchToSession(session.id);
612
+ dropdownContent.appendChild(item);
613
+ });
614
+ }
615
+ }
616
+
617
+ function formatLastActivity(timestamp) {
618
+ const now = Date.now();
619
+ const diff = now - timestamp;
620
+ const minutes = Math.floor(diff / 60000);
621
+ const hours = Math.floor(diff / 3600000);
622
+ const days = Math.floor(diff / 86400000);
623
+
624
+ if (minutes < 1) return 'now';
625
+ if (minutes < 60) return `${minutes}m`;
626
+ if (hours < 24) return `${hours}h`;
627
+ return `${days}d`;
628
+ }
629
+
630
+ function switchToSession(sessionId) {
631
+ if (!dataChannel || dataChannel.readyState !== 'open') {
632
+ console.error('[CLIENT] ❌ Cannot switch session - data channel not open');
633
+ return;
634
+ }
635
+
636
+ console.log('[CLIENT] 🔄 Switching to session:', sessionId);
637
+ dataChannel.send(JSON.stringify({
638
+ type: 'session-switch',
639
+ sessionId: sessionId
640
+ }));
641
+
642
+ // Hide dropdown
643
+ document.getElementById('session-dropdown-content').classList.remove('show');
644
+ }
645
+
646
+ function createNewSession() {
647
+ // Navigate to terminal with new session request
648
+ const url = new URL(window.location.href);
649
+ url.searchParams.delete('session'); // Remove session param to create new one
650
+ window.location.href = url.toString();
651
+ }
652
+
653
+ // Setup dropdown toggle
654
+ document.addEventListener('DOMContentLoaded', () => {
655
+ const dropdownBtn = document.getElementById('session-dropdown-btn');
656
+ const dropdownContent = document.getElementById('session-dropdown-content');
657
+
658
+ if (dropdownBtn && dropdownContent) {
659
+ dropdownBtn.onclick = (e) => {
660
+ e.stopPropagation();
661
+ dropdownContent.classList.toggle('show');
662
+ };
663
+
664
+ // Close dropdown when clicking outside
665
+ document.addEventListener('click', () => {
666
+ dropdownContent.classList.remove('show');
667
+ });
668
+ }
669
+ });
670
+
671
+ // Handle session-related data channel messages
672
+ function handleSessionMessage(message) {
673
+ switch (message.type) {
674
+ case 'session-switched':
675
+ currentSession = {
676
+ id: message.sessionId,
677
+ name: message.sessionName || 'Terminal Session'
678
+ };
679
+ updateSessionDisplay();
680
+ term.clear(); // Clear terminal for new session
681
+ console.log('[CLIENT] ✅ Switched to session:', currentSession);
682
+ break;
683
+ case 'session-ended':
684
+ term.write(`\r\n\x1b[31m❌ Session ended: ${message.reason}\x1b[0m\r\n`);
685
+ if (message.code) {
686
+ term.write(`Exit code: ${message.code}\r\n`);
687
+ }
688
+ break;
689
+ case 'session-terminated':
690
+ term.write(`\r\n\x1b[31m❌ Session terminated\x1b[0m\r\n`);
691
+ term.write('🔄 Click Dashboard to start a new session\r\n');
692
+ break;
693
+ case 'error':
694
+ term.write(`\r\n\x1b[31m❌ Error: ${message.message}\x1b[0m\r\n`);
695
+ break;
696
+ }
493
697
  }