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.
@@ -235,6 +235,19 @@ body {
235
235
  margin-top: 2px;
236
236
  }
237
237
 
238
+ .agent-sessions {
239
+ font-size: 0.8rem;
240
+ color: #4285f4;
241
+ margin-top: 2px;
242
+ font-weight: 500;
243
+ }
244
+
245
+ .agent-actions {
246
+ display: flex;
247
+ gap: 8px;
248
+ align-items: center;
249
+ }
250
+
238
251
  .btn-connect {
239
252
  background: #4caf50;
240
253
  color: white;
@@ -244,6 +257,7 @@ body {
244
257
  font-weight: 500;
245
258
  cursor: pointer;
246
259
  transition: all 0.2s ease;
260
+ font-size: 0.9rem;
247
261
  }
248
262
 
249
263
  .btn-connect:hover {
@@ -251,6 +265,23 @@ body {
251
265
  transform: translateY(-1px);
252
266
  }
253
267
 
268
+ .btn-sessions {
269
+ background: #4285f4;
270
+ color: white;
271
+ border: none;
272
+ padding: 8px 16px;
273
+ border-radius: 6px;
274
+ font-weight: 500;
275
+ cursor: pointer;
276
+ transition: all 0.2s ease;
277
+ font-size: 0.9rem;
278
+ }
279
+
280
+ .btn-sessions:hover {
281
+ background: #3367d6;
282
+ transform: translateY(-1px);
283
+ }
284
+
254
285
  /* Quick Actions */
255
286
  .action-buttons {
256
287
  display: flex;
@@ -522,4 +553,145 @@ body {
522
553
  flex-direction: row;
523
554
  gap: 12px;
524
555
  }
556
+ }
557
+
558
+ /* Modal Styles */
559
+ .modal-overlay {
560
+ position: fixed;
561
+ top: 0;
562
+ left: 0;
563
+ right: 0;
564
+ bottom: 0;
565
+ background: rgba(0, 0, 0, 0.5);
566
+ display: flex;
567
+ align-items: center;
568
+ justify-content: center;
569
+ z-index: 2000;
570
+ }
571
+
572
+ .modal {
573
+ background: white;
574
+ border-radius: 12px;
575
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
576
+ max-width: 500px;
577
+ width: 90%;
578
+ max-height: 80vh;
579
+ overflow: hidden;
580
+ }
581
+
582
+ .modal-header {
583
+ padding: 20px 24px;
584
+ border-bottom: 1px solid #eee;
585
+ display: flex;
586
+ justify-content: space-between;
587
+ align-items: center;
588
+ }
589
+
590
+ .modal-header h3 {
591
+ margin: 0;
592
+ font-size: 1.2rem;
593
+ font-weight: 600;
594
+ color: #333;
595
+ }
596
+
597
+ .modal-close {
598
+ background: none;
599
+ border: none;
600
+ font-size: 1.5rem;
601
+ cursor: pointer;
602
+ padding: 0;
603
+ width: 30px;
604
+ height: 30px;
605
+ display: flex;
606
+ align-items: center;
607
+ justify-content: center;
608
+ border-radius: 50%;
609
+ color: #666;
610
+ }
611
+
612
+ .modal-close:hover {
613
+ background: #f5f5f5;
614
+ color: #333;
615
+ }
616
+
617
+ .modal-body {
618
+ padding: 24px;
619
+ max-height: 60vh;
620
+ overflow-y: auto;
621
+ }
622
+
623
+ /* Session List Styles */
624
+ .sessions-modal-content p {
625
+ margin-top: 0;
626
+ margin-bottom: 16px;
627
+ color: #666;
628
+ }
629
+
630
+ .session-list {
631
+ margin-bottom: 20px;
632
+ }
633
+
634
+ .session-list-item {
635
+ display: flex;
636
+ justify-content: space-between;
637
+ align-items: center;
638
+ padding: 12px;
639
+ border: 1px solid #eee;
640
+ border-radius: 8px;
641
+ margin-bottom: 8px;
642
+ cursor: pointer;
643
+ transition: all 0.2s ease;
644
+ }
645
+
646
+ .session-list-item:hover {
647
+ background: #f8f9fa;
648
+ border-color: #4285f4;
649
+ }
650
+
651
+ .session-list-info {
652
+ flex: 1;
653
+ }
654
+
655
+ .session-list-name {
656
+ font-weight: 600;
657
+ color: #333;
658
+ margin-bottom: 4px;
659
+ }
660
+
661
+ .session-list-details {
662
+ display: flex;
663
+ gap: 12px;
664
+ font-size: 0.8rem;
665
+ color: #666;
666
+ }
667
+
668
+ .session-list-id {
669
+ font-family: monospace;
670
+ background: #f5f5f5;
671
+ padding: 2px 6px;
672
+ border-radius: 4px;
673
+ }
674
+
675
+ .session-list-status {
676
+ padding: 4px 8px;
677
+ border-radius: 12px;
678
+ font-size: 0.75rem;
679
+ font-weight: 500;
680
+ text-transform: uppercase;
681
+ }
682
+
683
+ .session-list-status.active {
684
+ background: #e8f5e8;
685
+ color: #2e7d32;
686
+ }
687
+
688
+ .session-list-status.crashed {
689
+ background: #ffebee;
690
+ color: #c62828;
691
+ }
692
+
693
+ .sessions-modal-actions {
694
+ border-top: 1px solid #eee;
695
+ padding-top: 16px;
696
+ text-align: center;
525
697
  }
@@ -5,6 +5,7 @@ class ShellMirrorDashboard {
5
5
  this.user = null;
6
6
  this.agents = [];
7
7
  this.sessions = [];
8
+ this.agentSessions = {}; // Maps agentId to sessions array
8
9
  this.init();
9
10
  }
10
11
 
@@ -80,6 +81,9 @@ class ShellMirrorDashboard {
80
81
 
81
82
  if (agentsData.success && agentsData.data && agentsData.data.agents) {
82
83
  this.agents = agentsData.data.agents;
84
+
85
+ // Load session data from localStorage (persisted from terminal connections)
86
+ this.loadSessionsFromStorage();
83
87
  }
84
88
 
85
89
  // TODO: Load session history when API is available
@@ -174,18 +178,29 @@ class ShellMirrorDashboard {
174
178
  });
175
179
 
176
180
  const agentCount = activeAgents.length;
177
- const agentsHtml = activeAgents.map(agent => `
181
+ const agentsHtml = activeAgents.map(agent => {
182
+ const sessions = this.agentSessions[agent.agentId] || [];
183
+ const sessionCount = sessions.length;
184
+
185
+ return `
178
186
  <div class="agent-item">
179
187
  <div class="agent-info">
180
188
  <div class="agent-name">${agent.machineName || agent.agentId}</div>
181
189
  <div class="agent-status ${agent.onlineStatus}">${agent.onlineStatus}</div>
182
190
  <div class="agent-last-seen">Last seen: ${this.formatLastSeen(agent.lastSeen)}</div>
191
+ ${sessionCount > 0 ? `<div class="agent-sessions">${sessionCount} active session${sessionCount !== 1 ? 's' : ''}</div>` : ''}
192
+ </div>
193
+ <div class="agent-actions">
194
+ <button class="btn-connect" onclick="dashboard.connectToAgent('${agent.agentId}')">
195
+ ${sessionCount > 0 ? 'Resume Session' : 'New Session'}
196
+ </button>
197
+ ${sessionCount > 0 ? `<button class="btn-sessions" onclick="dashboard.showAgentSessions('${agent.agentId}')">
198
+ All Sessions
199
+ </button>` : ''}
183
200
  </div>
184
- <button class="btn-connect" onclick="dashboard.connectToAgent('${agent.agentId}')">
185
- Connect
186
- </button>
187
201
  </div>
188
- `).join('');
202
+ `;
203
+ }).join('');
189
204
 
190
205
  return `
191
206
  <div class="dashboard-card">
@@ -398,13 +413,151 @@ class ShellMirrorDashboard {
398
413
 
399
414
  // Action handlers
400
415
  async connectToAgent(agentId) {
416
+ // Check if there are existing sessions for this agent
417
+ const sessions = this.agentSessions[agentId] || [];
418
+ const activeSessions = sessions.filter(s => s.status === 'active');
419
+
420
+ if (activeSessions.length > 0) {
421
+ // Reconnect to the most recently active session
422
+ const mostRecentSession = activeSessions.reduce((latest, session) =>
423
+ session.lastActivity > latest.lastActivity ? session : latest
424
+ );
425
+ console.log(`[DASHBOARD] Reconnecting to existing session: ${mostRecentSession.id}`);
426
+ window.location.href = `/app/terminal.html?agent=${agentId}&session=${mostRecentSession.id}`;
427
+ } else {
428
+ // No existing sessions, create new one
429
+ console.log(`[DASHBOARD] Creating new session for agent: ${agentId}`);
430
+ window.location.href = `/app/terminal.html?agent=${agentId}`;
431
+ }
432
+ }
433
+
434
+ async connectToSession(agentId, sessionId) {
435
+ window.location.href = `/app/terminal.html?agent=${agentId}&session=${sessionId}`;
436
+ }
437
+
438
+ async createNewSession(agentId) {
439
+ // Force creation of new session by not passing session parameter
440
+ console.log(`[DASHBOARD] Creating new session for agent: ${agentId}`);
401
441
  window.location.href = `/app/terminal.html?agent=${agentId}`;
402
442
  }
403
443
 
404
444
  startNewSession() {
405
- window.location.href = '/app/terminal.html';
445
+ // Get first available agent for new session
446
+ const activeAgents = this.agents.filter(agent => {
447
+ const timeSinceLastSeen = Date.now() / 1000 - agent.lastSeen;
448
+ return agent.onlineStatus === 'online' || timeSinceLastSeen < 300;
449
+ });
450
+
451
+ if (activeAgents.length > 0) {
452
+ this.connectToAgent(activeAgents[0].agentId);
453
+ } else {
454
+ alert('No active agents available. Please ensure an agent is running on your Mac.');
455
+ }
406
456
  }
407
457
 
458
+ showAgentSessions(agentId) {
459
+ const sessions = this.agentSessions[agentId] || [];
460
+ const agent = this.agents.find(a => a.agentId === agentId);
461
+ const agentName = agent ? (agent.machineName || agent.agentId) : agentId;
462
+
463
+ if (sessions.length === 0) {
464
+ alert(`No active sessions found for ${agentName}`);
465
+ return;
466
+ }
467
+
468
+ // Create session list modal
469
+ const sessionsList = sessions.map(session => `
470
+ <div class="session-list-item" onclick="dashboard.connectToSession('${agentId}', '${session.id}')">
471
+ <div class="session-list-info">
472
+ <div class="session-list-name">${session.name}</div>
473
+ <div class="session-list-details">
474
+ <span class="session-list-id">${session.id.substring(0, 8)}...</span>
475
+ <span class="session-list-activity">Last activity: ${this.formatLastActivity(session.lastActivity)}</span>
476
+ </div>
477
+ </div>
478
+ <div class="session-list-status ${session.status}">${session.status}</div>
479
+ </div>
480
+ `).join('');
481
+
482
+ // Show modal with sessions
483
+ this.showModal(`Sessions on ${agentName}`, `
484
+ <div class="sessions-modal-content">
485
+ <p>Active terminal sessions (${sessions.length}):</p>
486
+ <div class="session-list">
487
+ ${sessionsList}
488
+ </div>
489
+ <div class="sessions-modal-actions">
490
+ <button class="btn-primary" onclick="dashboard.createNewSession('${agentId}')">
491
+ + Create New Session
492
+ </button>
493
+ </div>
494
+ </div>
495
+ `);
496
+ }
497
+
498
+ showModal(title, content) {
499
+ // Create modal overlay
500
+ const modalOverlay = document.createElement('div');
501
+ modalOverlay.className = 'modal-overlay';
502
+ modalOverlay.onclick = () => document.body.removeChild(modalOverlay);
503
+
504
+ modalOverlay.innerHTML = `
505
+ <div class="modal" onclick="event.stopPropagation()">
506
+ <div class="modal-header">
507
+ <h3>${title}</h3>
508
+ <button class="modal-close" onclick="document.body.removeChild(this.closest('.modal-overlay'))">×</button>
509
+ </div>
510
+ <div class="modal-body">
511
+ ${content}
512
+ </div>
513
+ </div>
514
+ `;
515
+
516
+ document.body.appendChild(modalOverlay);
517
+ }
518
+
519
+ formatLastActivity(timestamp) {
520
+ const now = Date.now();
521
+ const diff = now - timestamp;
522
+ const minutes = Math.floor(diff / 60000);
523
+ const hours = Math.floor(diff / 3600000);
524
+ const days = Math.floor(diff / 86400000);
525
+
526
+ if (minutes < 1) return 'now';
527
+ if (minutes < 60) return `${minutes}m ago`;
528
+ if (hours < 24) return `${hours}h ago`;
529
+ return `${days}d ago`;
530
+ }
531
+
532
+ // Session storage management
533
+ loadSessionsFromStorage() {
534
+ try {
535
+ const storedSessions = localStorage.getItem('shell-mirror-sessions');
536
+ if (storedSessions) {
537
+ const sessionData = JSON.parse(storedSessions);
538
+
539
+ // Filter out old sessions (older than 24 hours)
540
+ const now = Date.now();
541
+ const maxAge = 24 * 60 * 60 * 1000; // 24 hours
542
+
543
+ Object.keys(sessionData).forEach(agentId => {
544
+ const validSessions = sessionData[agentId].filter(session =>
545
+ (now - session.lastActivity) < maxAge
546
+ );
547
+
548
+ if (validSessions.length > 0) {
549
+ this.agentSessions[agentId] = validSessions;
550
+ }
551
+ });
552
+
553
+ console.log('[DASHBOARD] Loaded sessions from storage:', this.agentSessions);
554
+ }
555
+ } catch (error) {
556
+ console.error('[DASHBOARD] Error loading sessions from storage:', error);
557
+ }
558
+ }
559
+
560
+
408
561
  showAgentInstructions() {
409
562
  // TODO: Show modal with agent setup instructions
410
563
  alert('Agent setup instructions coming soon!');
@@ -21,11 +21,109 @@
21
21
  width: 100%;
22
22
  background-color: #000000;
23
23
  }
24
+
25
+ #terminal-container.show {
26
+ display: flex;
27
+ flex-direction: column;
28
+ }
29
+
30
+ /* Session Header */
31
+ .session-header {
32
+ background: #2a2a2a;
33
+ color: #ccc;
34
+ padding: 8px 16px;
35
+ border-bottom: 1px solid #444;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ font-size: 0.9em;
40
+ z-index: 100;
41
+ }
42
+
43
+ .session-info {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 12px;
47
+ }
48
+
49
+ .session-name {
50
+ font-weight: bold;
51
+ color: #fff;
52
+ }
53
+
54
+ .session-controls {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 8px;
58
+ }
59
+
60
+ .session-dropdown {
61
+ position: relative;
62
+ display: inline-block;
63
+ }
64
+
65
+ .session-dropdown-btn {
66
+ background: #3a3a3a;
67
+ color: #ccc;
68
+ border: 1px solid #555;
69
+ padding: 4px 8px;
70
+ border-radius: 4px;
71
+ cursor: pointer;
72
+ font-size: 0.8em;
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 4px;
76
+ }
77
+
78
+ .session-dropdown-btn:hover {
79
+ background: #4a4a4a;
80
+ }
81
+
82
+ .session-dropdown-content {
83
+ display: none;
84
+ position: absolute;
85
+ background: #2a2a2a;
86
+ border: 1px solid #555;
87
+ border-radius: 4px;
88
+ right: 0;
89
+ top: 100%;
90
+ min-width: 200px;
91
+ z-index: 1000;
92
+ box-shadow: 0 4px 8px rgba(0,0,0,0.3);
93
+ }
94
+
95
+ .session-dropdown-content.show {
96
+ display: block;
97
+ }
98
+
99
+ .session-dropdown-item {
100
+ padding: 8px 12px;
101
+ cursor: pointer;
102
+ border-bottom: 1px solid #444;
103
+ display: flex;
104
+ justify-content: space-between;
105
+ align-items: center;
106
+ }
107
+
108
+ .session-dropdown-item:last-child {
109
+ border-bottom: none;
110
+ }
111
+
112
+ .session-dropdown-item:hover {
113
+ background: #3a3a3a;
114
+ }
115
+
116
+ .session-dropdown-item.current {
117
+ background: #4285f4;
118
+ color: white;
119
+ }
120
+
24
121
  #terminal {
25
122
  padding: 8px; /* Mac Terminal.app padding */
26
123
  background-color: #000000;
27
- height: calc(100% - 16px);
124
+ height: calc(100% - 16px - 40px); /* Subtract session header height */
28
125
  width: calc(100% - 16px);
126
+ flex: 1;
29
127
  }
30
128
  #connect-container { padding: 2em; text-align: center; }
31
129
  #agent-id-input { font-size: 1.2em; padding: 8px; width: 400px; margin-bottom: 1em; }
@@ -95,6 +193,25 @@
95
193
  <p>Connecting to terminal...</p>
96
194
  </div>
97
195
  <div id="terminal-container">
196
+ <div class="session-header" id="session-header" style="display: none;">
197
+ <div class="session-info">
198
+ <span class="session-name" id="session-name">Terminal Session</span>
199
+ <span class="session-id" id="session-id"></span>
200
+ </div>
201
+ <div class="session-controls">
202
+ <div class="session-dropdown">
203
+ <button class="session-dropdown-btn" id="session-dropdown-btn">
204
+ <span>Sessions</span>
205
+ <span>▼</span>
206
+ </button>
207
+ <div class="session-dropdown-content" id="session-dropdown-content">
208
+ <div class="session-dropdown-item" onclick="createNewSession()">
209
+ <span>+ New Session</span>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </div>
98
215
  <div id="terminal"></div>
99
216
  </div>
100
217