shell-mirror 1.5.40 → 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.
@@ -1,94 +1,9 @@
1
- === Mac Agent Debug Log Started 2025-08-06T23:17:12.104Z ===
2
- [2025-08-06T23:17:12.119Z] ✅ @koush/wrtc package loaded successfully
3
- [2025-08-06T23:17:12.122Z] 🆔 Agent ID: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
4
- [2025-08-06T23:17:12.122Z] [AGENT] Host is 0.0.0.0, connecting to localhost instead.
5
- [2025-08-06T23:17:12.122Z] 🌐 Signaling server URL: ws://localhost:3000
6
- [2025-08-06T23:17:12.122Z] 🐚 Shell: bash
7
- [2025-08-06T23:17:12.122Z] 🔌 Connecting to signaling server at ws://localhost:3000?role=agent&agentId=agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
8
- [2025-08-06T23:17:12.132Z] Connected to signaling server.
9
- [2025-08-06T23:17:17.684Z] 🔌 Connecting to signaling server at ws://localhost:3000?role=agent&agentId=agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
10
- [2025-08-06T23:17:17.690Z] ✅ Connected to signaling server.
11
- [2025-08-06T23:17:18.729Z] 📨 Received message of type: ping from: local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731 to: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
12
- [2025-08-06T23:17:18.730Z] [AGENT] ❓ Unknown message type: ping
13
- [2025-08-06T23:17:19.733Z] 📨 Received message of type: client-hello from: local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731 to: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
14
- [2025-08-06T23:17:19.733Z] 🔄 Received client-hello from local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731. Initiating WebRTC connection.
15
- [2025-08-06T23:17:19.734Z] Creating new PeerConnection
16
- [2025-08-06T23:17:19.734Z] 🌐 Configuring ICE servers: stun:stun.l.google.com:19302, stun:stun1.l.google.com:19302, stun:stun.cloudflare.com:3478, stun:stun.services.mozilla.com:3478, turn:openrelay.metered.ca:80, turn:openrelay.metered.ca:443
17
- [2025-08-06T23:17:19.734Z] ⚙️ WebRTC config: {"iceServers":[{"urls":"stun:stun.l.google.com:19302"},{"urls":"stun:stun1.l.google.com:19302"},{"urls":"stun:stun.cloudflare.com:3478"},{"urls":"stun:stun.services.mozilla.com:3478"},{"urls":"turn:openrelay.metered.ca:80","username":"openrelayproject","credential":"openrelayproject"},{"urls":"turn:openrelay.metered.ca:443","username":"openrelayproject","credential":"openrelayproject"}],"iceCandidatePoolSize":10,"iceTransportPolicy":"all","bundlePolicy":"balanced"}
18
- [2025-08-06T23:17:19.748Z] [AGENT] 🔧 Attaching ICE candidate event handler...
19
- [2025-08-06T23:17:19.748Z] [AGENT] Creating data channel...
20
- [2025-08-06T23:17:19.750Z] [AGENT] Spawning new terminal
21
- [2025-08-06T23:17:19.753Z] [AGENT] ✅ Terminal spawned (PID: 70587)
22
- [2025-08-06T23:17:19.753Z] 📡 PeerConnection created, generating offer...
23
- [2025-08-06T23:17:19.755Z] 📋 Offer created: offer
24
- [2025-08-06T23:17:19.756Z] 📤 Sending WebRTC offer to client.
25
- [2025-08-06T23:17:19.756Z] [AGENT] Sending message: offer
26
- [2025-08-06T23:17:19.757Z] ✅ WebRTC offer sent successfully
27
- [2025-08-06T23:17:19.757Z] [AGENT] 🔧 Setting up ICE gathering fallback timer...
28
- [2025-08-06T23:17:19.757Z] [AGENT] 🔍 ICE gathering state changed: gathering
29
- [2025-08-06T23:17:19.758Z] [AGENT] 🔍 ICE gathering in progress...
30
- [2025-08-06T23:17:19.758Z] [AGENT] 🧊 ICE candidate event fired: candidate found
31
- [2025-08-06T23:17:19.758Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:1431643032 1 udp 2122260224 192.168.31.244 61993 typ host generation 0 ufrag fZih network-id 1 network-cost 50","sdpMid":"0","sdpMLineIndex":0}
32
- [2025-08-06T23:17:19.758Z] [AGENT] 📤 Sending ICE candidate to client...
33
- [2025-08-06T23:17:19.758Z] [AGENT] Sending message: candidate
34
- [2025-08-06T23:17:19.759Z] [AGENT] ✅ ICE candidate sent successfully
35
- [2025-08-06T23:17:19.759Z] [AGENT] 🧊 ICE candidate event fired: candidate found
36
- [2025-08-06T23:17:19.759Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:559267639 1 udp 2122202368 ::1 59459 typ host generation 0 ufrag fZih network-id 3","sdpMid":"0","sdpMLineIndex":0}
37
- [2025-08-06T23:17:19.759Z] [AGENT] 📤 Sending ICE candidate to client...
38
- [2025-08-06T23:17:19.759Z] [AGENT] Sending message: candidate
39
- [2025-08-06T23:17:19.759Z] [AGENT] ✅ ICE candidate sent successfully
40
- [2025-08-06T23:17:19.760Z] [AGENT] 🧊 ICE candidate event fired: candidate found
41
- [2025-08-06T23:17:19.760Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:1510613869 1 udp 2122129152 127.0.0.1 52296 typ host generation 0 ufrag fZih network-id 2","sdpMid":"0","sdpMLineIndex":0}
42
- [2025-08-06T23:17:19.760Z] [AGENT] 📤 Sending ICE candidate to client...
43
- [2025-08-06T23:17:19.760Z] [AGENT] Sending message: candidate
44
- [2025-08-06T23:17:19.760Z] [AGENT] ✅ ICE candidate sent successfully
45
- [2025-08-06T23:17:19.785Z] 📨 Received message of type: answer from: local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731 to: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
46
- [2025-08-06T23:17:19.785Z] [AGENT] 📥 Received WebRTC answer from client.
47
- [2025-08-06T23:17:19.786Z] 📨 Received message of type: candidate from: local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731 to: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
48
- [2025-08-06T23:17:19.786Z] [AGENT] 🧊 Received ICE candidate from client.
49
- [2025-08-06T23:17:19.787Z] [AGENT] 📊 ICE connection state changed: new
50
- [2025-08-06T23:17:19.787Z] [AGENT] 📊 ICE gathering state: gathering
51
- [2025-08-06T23:17:19.787Z] [AGENT] 🆕 ICE connection starting...
52
- [2025-08-06T23:17:19.787Z] [AGENT] 📡 Connection state changed: new
53
- [2025-08-06T23:17:19.787Z] [AGENT] 🆕 Connection starting...
54
- [2025-08-06T23:17:19.787Z] [AGENT] ✅ WebRTC answer processed successfully
55
- [2025-08-06T23:17:19.787Z] [AGENT] ✅ ICE candidate added successfully
56
- [2025-08-06T23:17:19.791Z] [AGENT] 🧊 ICE candidate event fired: candidate found
57
- [2025-08-06T23:17:19.792Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:2581077003 1 udp 1686052607 46.34.249.124 1753 typ srflx raddr 192.168.31.244 rport 61993 generation 0 ufrag fZih network-id 1 network-cost 50","sdpMid":"0","sdpMLineIndex":0}
58
- [2025-08-06T23:17:19.792Z] [AGENT] 📤 Sending ICE candidate to client...
59
- [2025-08-06T23:17:19.792Z] [AGENT] Sending message: candidate
60
- [2025-08-06T23:17:19.792Z] [AGENT] ✅ ICE candidate sent successfully
61
- [2025-08-06T23:17:19.797Z] 📨 Received message of type: candidate from: local-test-client-7f2cd128-5a64-4a9e-a565-eab1357ea731 to: agent-496ad28d-dd0c-4fdb-95fd-28bc1023ae3d
62
- [2025-08-06T23:17:19.797Z] [AGENT] 🧊 Received ICE candidate from client.
63
- [2025-08-06T23:17:19.797Z] [AGENT] ✅ ICE candidate added successfully
64
- [2025-08-06T23:17:19.847Z] [AGENT] 🧊 ICE candidate event fired: candidate found
65
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:467066728 1 tcp 1518280447 192.168.31.244 57774 typ host tcptype passive generation 0 ufrag fZih network-id 1 network-cost 50","sdpMid":"0","sdpMLineIndex":0}
66
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 Sending ICE candidate to client...
67
- [2025-08-06T23:17:19.847Z] [AGENT] Sending message: candidate
68
- [2025-08-06T23:17:19.847Z] [AGENT] ✅ ICE candidate sent successfully
69
- [2025-08-06T23:17:19.847Z] [AGENT] 🧊 ICE candidate event fired: candidate found
70
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:1876313031 1 tcp 1518222591 ::1 57775 typ host tcptype passive generation 0 ufrag fZih network-id 3","sdpMid":"0","sdpMLineIndex":0}
71
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 Sending ICE candidate to client...
72
- [2025-08-06T23:17:19.847Z] [AGENT] Sending message: candidate
73
- [2025-08-06T23:17:19.847Z] [AGENT] ✅ ICE candidate sent successfully
74
- [2025-08-06T23:17:19.847Z] [AGENT] 🧊 ICE candidate event fired: candidate found
75
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 ICE candidate details: {"candidate":"candidate:344579997 1 tcp 1518149375 127.0.0.1 57776 typ host tcptype passive generation 0 ufrag fZih network-id 2","sdpMid":"0","sdpMLineIndex":0}
76
- [2025-08-06T23:17:19.847Z] [AGENT] 📤 Sending ICE candidate to client...
77
- [2025-08-06T23:17:19.847Z] [AGENT] Sending message: candidate
78
- [2025-08-06T23:17:19.848Z] [AGENT] ✅ ICE candidate sent successfully
79
- [2025-08-06T23:17:19.953Z] [AGENT] 🔍 ICE gathering state changed: complete
80
- [2025-08-06T23:17:19.953Z] [AGENT] ✅ ICE gathering completed
81
- [2025-08-06T23:17:19.953Z] [AGENT] 🧊 ICE candidate event fired: gathering complete
82
- [2025-08-06T23:17:19.953Z] [AGENT] 🏁 ICE candidate gathering complete.
83
- [2025-08-06T23:17:19.955Z] [AGENT] 📊 ICE connection state changed: connected
84
- [2025-08-06T23:17:19.955Z] [AGENT] 📊 ICE gathering state: complete
85
- [2025-08-06T23:17:19.955Z] [AGENT] ✅ WebRTC connection established!
86
- [2025-08-06T23:17:19.956Z] [AGENT] 📡 Connection state changed: connected
87
- [2025-08-06T23:17:19.956Z] [AGENT] ✅ Peer connection fully established!
88
- [2025-08-06T23:17:19.957Z] [AGENT] ✅ Data channel is open!
89
- [2025-08-06T23:17:21.758Z] [AGENT] ✅ ICE gathering is active: complete
90
- [2025-08-06T23:17:35.042Z] [AGENT] 📊 ICE connection state changed: completed
91
- [2025-08-06T23:17:35.043Z] [AGENT] 📊 ICE gathering state: complete
92
- [2025-08-06T23:17:35.044Z] [AGENT] ✅ ICE connection completed successfully!
93
- [2025-08-06T23:17:35.044Z] [AGENT] 📡 Connection state changed: connected
94
- [2025-08-06T23:17:35.044Z] [AGENT] ✅ Peer connection fully established!
1
+ === Mac Agent Debug Log Started 2025-08-17T10:31:27.217Z ===
2
+ [2025-08-17T10:31:27.469Z] ✅ @koush/wrtc package loaded successfully
3
+ [2025-08-17T10:31:27.471Z] 🆔 Agent ID: agent-69bf639a-7e26-4b2e-b34a-8cc840f35203
4
+ [2025-08-17T10:31:27.471Z] [AGENT] Host is 0.0.0.0, connecting to localhost instead.
5
+ [2025-08-17T10:31:27.471Z] 🌐 Using local WebSocket URL: ws://localhost:8080
6
+ [2025-08-17T10:31:27.471Z] 🐚 Shell: bash
7
+ [2025-08-17T10:31:27.471Z] 🔌 Connecting to signaling server at ws://localhost:8080?role=agent&agentId=agent-69bf639a-7e26-4b2e-b34a-8cc840f35203
8
+ [2025-08-17T10:31:32.485Z] 🔌 Connecting to signaling server at ws://localhost:8080?role=agent&agentId=agent-69bf639a-7e26-4b2e-b34a-8cc840f35203
9
+ [2025-08-17T10:31:37.492Z] 🔌 Connecting to signaling server at ws://localhost:8080?role=agent&agentId=agent-69bf639a-7e26-4b2e-b34a-8cc840f35203
@@ -53,10 +53,268 @@ if (process.env.WEBSOCKET_URL) {
53
53
  const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
54
54
  logToFile(`🐚 Shell: ${shell}`);
55
55
 
56
+ // Circular buffer for session output persistence
57
+ class CircularBuffer {
58
+ constructor(size = 10000) {
59
+ this.size = size;
60
+ this.buffer = [];
61
+ this.index = 0;
62
+ this.full = false;
63
+ }
64
+
65
+ add(data) {
66
+ this.buffer[this.index] = data;
67
+ this.index = (this.index + 1) % this.size;
68
+ if (this.index === 0) this.full = true;
69
+ }
70
+
71
+ getAll() {
72
+ if (!this.full) {
73
+ return this.buffer.slice(0, this.index).join('');
74
+ }
75
+ return this.buffer.slice(this.index).concat(this.buffer.slice(0, this.index)).join('');
76
+ }
77
+
78
+ clear() {
79
+ this.buffer = [];
80
+ this.index = 0;
81
+ this.full = false;
82
+ }
83
+ }
84
+
85
+ // Session Manager for multiple persistent terminal sessions
86
+ class SessionManager {
87
+ constructor() {
88
+ this.sessions = {};
89
+ this.maxSessions = 10;
90
+ this.defaultSessionTimeout = 24 * 60 * 60 * 1000; // 24 hours
91
+ this.clientSessions = {}; // Maps clientId to sessionId
92
+ }
93
+
94
+ createSession(sessionName = null, clientId = null) {
95
+ const sessionId = `ses_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
96
+ const name = sessionName || `Session ${Object.keys(this.sessions).length + 1}`;
97
+
98
+ logToFile(`[SESSION] Creating new session: ${sessionId} (${name})`);
99
+
100
+ // Check session limit
101
+ if (Object.keys(this.sessions).length >= this.maxSessions) {
102
+ logToFile(`[SESSION] ❌ Maximum sessions (${this.maxSessions}) reached`);
103
+ return null;
104
+ }
105
+
106
+ const macShell = os.platform() === 'darwin' ? '/bin/zsh' : shell;
107
+ const terminalEnv = {
108
+ ...process.env,
109
+ TERM: 'xterm-256color',
110
+ COLORTERM: 'truecolor',
111
+ LANG: 'en_US.UTF-8',
112
+ LC_ALL: 'en_US.UTF-8',
113
+ SHELL: macShell,
114
+ TERM_PROGRAM: 'Terminal',
115
+ TERM_PROGRAM_VERSION: '2.12.7'
116
+ };
117
+
118
+ const terminal = pty.spawn(macShell, ['--login'], {
119
+ name: 'xterm-256color',
120
+ cols: 120,
121
+ rows: 30,
122
+ cwd: process.env.HOME,
123
+ env: terminalEnv,
124
+ encoding: 'utf8'
125
+ });
126
+
127
+ const session = {
128
+ id: sessionId,
129
+ name: name,
130
+ terminal: terminal,
131
+ buffer: new CircularBuffer(10000),
132
+ connectedClients: [],
133
+ createdAt: Date.now(),
134
+ lastActivity: Date.now(),
135
+ status: 'active'
136
+ };
137
+
138
+ // Set up terminal event handlers
139
+ terminal.on('data', (data) => {
140
+ session.buffer.add(data);
141
+ session.lastActivity = Date.now();
142
+
143
+ // Send to all connected clients for this session
144
+ session.connectedClients.forEach(clientId => {
145
+ this.sendToClient(clientId, { type: 'output', data: data });
146
+ });
147
+ });
148
+
149
+ terminal.on('exit', (code) => {
150
+ logToFile(`[SESSION] Terminal process exited for session ${sessionId} with code ${code}`);
151
+ session.status = 'crashed';
152
+ // Notify connected clients
153
+ session.connectedClients.forEach(clientId => {
154
+ this.sendToClient(clientId, {
155
+ type: 'session-ended',
156
+ sessionId: sessionId,
157
+ reason: 'terminal-exit',
158
+ code: code
159
+ });
160
+ });
161
+ });
162
+
163
+ this.sessions[sessionId] = session;
164
+
165
+ // Associate with client if provided
166
+ if (clientId) {
167
+ this.clientSessions[clientId] = sessionId;
168
+ session.connectedClients.push(clientId);
169
+ }
170
+
171
+ logToFile(`[SESSION] ✅ Session created: ${sessionId} (PID: ${terminal.pid})`);
172
+ return sessionId;
173
+ }
174
+
175
+ getSession(sessionId) {
176
+ return this.sessions[sessionId] || null;
177
+ }
178
+
179
+ connectClientToSession(clientId, sessionId) {
180
+ const session = this.sessions[sessionId];
181
+ if (!session) {
182
+ logToFile(`[SESSION] ❌ Cannot connect client ${clientId} - session ${sessionId} not found`);
183
+ return false;
184
+ }
185
+
186
+ // Disconnect client from any existing session
187
+ this.disconnectClient(clientId);
188
+
189
+ // Connect to new session
190
+ this.clientSessions[clientId] = sessionId;
191
+ if (!session.connectedClients.includes(clientId)) {
192
+ session.connectedClients.push(clientId);
193
+ }
194
+ session.lastActivity = Date.now();
195
+
196
+ logToFile(`[SESSION] ✅ Client ${clientId} connected to session ${sessionId}`);
197
+
198
+ // Send buffered output to newly connected client
199
+ const bufferedOutput = session.buffer.getAll();
200
+ if (bufferedOutput) {
201
+ this.sendToClient(clientId, { type: 'output', data: bufferedOutput });
202
+ }
203
+
204
+ return true;
205
+ }
206
+
207
+ disconnectClient(clientId) {
208
+ const sessionId = this.clientSessions[clientId];
209
+ if (sessionId && this.sessions[sessionId]) {
210
+ const session = this.sessions[sessionId];
211
+ session.connectedClients = session.connectedClients.filter(id => id !== clientId);
212
+ logToFile(`[SESSION] Client ${clientId} disconnected from session ${sessionId}`);
213
+ }
214
+ delete this.clientSessions[clientId];
215
+ }
216
+
217
+ getClientSession(clientId) {
218
+ const sessionId = this.clientSessions[clientId];
219
+ return sessionId ? this.sessions[sessionId] : null;
220
+ }
221
+
222
+ getAllSessions() {
223
+ return Object.values(this.sessions).map(session => ({
224
+ id: session.id,
225
+ name: session.name,
226
+ lastActivity: session.lastActivity,
227
+ createdAt: session.createdAt,
228
+ status: session.status,
229
+ connectedClients: session.connectedClients.length
230
+ }));
231
+ }
232
+
233
+ terminateSession(sessionId) {
234
+ const session = this.sessions[sessionId];
235
+ if (!session) return false;
236
+
237
+ logToFile(`[SESSION] Terminating session: ${sessionId}`);
238
+
239
+ // Notify connected clients
240
+ session.connectedClients.forEach(clientId => {
241
+ this.sendToClient(clientId, {
242
+ type: 'session-terminated',
243
+ sessionId: sessionId
244
+ });
245
+ delete this.clientSessions[clientId];
246
+ });
247
+
248
+ // Kill terminal process
249
+ if (session.terminal) {
250
+ session.terminal.kill();
251
+ }
252
+
253
+ delete this.sessions[sessionId];
254
+ logToFile(`[SESSION] ✅ Session terminated: ${sessionId}`);
255
+ return true;
256
+ }
257
+
258
+ sendToClient(clientId, message) {
259
+ // This will be connected to the WebRTC data channel sending logic
260
+ // For now, we'll use a global dataChannel reference
261
+ // In a full implementation, this would use a clientId-to-dataChannel mapping
262
+ if (typeof dataChannel !== 'undefined' && dataChannel && dataChannel.readyState === 'open') {
263
+ try {
264
+ dataChannel.send(JSON.stringify(message));
265
+ } catch (err) {
266
+ logToFile(`[SESSION] Error sending to client ${clientId}: ${err.message}`);
267
+ }
268
+ } else {
269
+ logToFile(`[SESSION] ⚠️ Cannot send to client ${clientId} - data channel not available`);
270
+ }
271
+ }
272
+
273
+ writeToSession(sessionId, data) {
274
+ const session = this.sessions[sessionId];
275
+ if (session && session.terminal) {
276
+ session.terminal.write(data);
277
+ session.lastActivity = Date.now();
278
+ return true;
279
+ }
280
+ return false;
281
+ }
282
+
283
+ resizeSession(sessionId, cols, rows) {
284
+ const session = this.sessions[sessionId];
285
+ if (session && session.terminal) {
286
+ session.terminal.resize(cols, rows);
287
+ session.lastActivity = Date.now();
288
+ return true;
289
+ }
290
+ return false;
291
+ }
292
+
293
+ cleanupIdleSessions() {
294
+ const now = Date.now();
295
+ Object.keys(this.sessions).forEach(sessionId => {
296
+ const session = this.sessions[sessionId];
297
+ const idleTime = now - session.lastActivity;
298
+
299
+ if (idleTime > this.defaultSessionTimeout && session.connectedClients.length === 0) {
300
+ logToFile(`[SESSION] Auto-cleanup idle session: ${sessionId} (idle for ${Math.floor(idleTime / 60000)} minutes)`);
301
+ this.terminateSession(sessionId);
302
+ }
303
+ });
304
+ }
305
+ }
306
+
307
+ // Initialize session manager
308
+ const sessionManager = new SessionManager();
309
+
310
+ // Cleanup idle sessions every 30 minutes
311
+ setInterval(() => {
312
+ sessionManager.cleanupIdleSessions();
313
+ }, 30 * 60 * 1000);
314
+
56
315
  let ws;
57
316
  let peerConnection;
58
317
  let dataChannel;
59
- let term;
60
318
 
61
319
  const iceServers = [
62
320
  // Google STUN servers (primary)
@@ -95,16 +353,71 @@ function connectToSignalingServer() {
95
353
 
96
354
  switch (data.type) {
97
355
  case 'client-hello':
98
- logToFile(`🔄 Received client-hello from ${data.from}. Initiating WebRTC connection.`);
356
+ logToFile(`🔄 Received client-hello from ${data.from}. Processing session request.`);
99
357
  try {
358
+ let sessionId;
359
+ let isNewSession = false;
360
+ let availableSessions = sessionManager.getAllSessions();
361
+
362
+ // Handle session request from client
363
+ if (data.sessionRequest) {
364
+ if (data.sessionRequest.sessionId) {
365
+ // Connect to existing session
366
+ sessionId = data.sessionRequest.sessionId;
367
+ logToFile(`[SESSION] Client requesting existing session: ${sessionId}`);
368
+ if (!sessionManager.getSession(sessionId)) {
369
+ logToFile(`[SESSION] ⚠️ Requested session ${sessionId} not found, creating new session`);
370
+ sessionId = sessionManager.createSession(data.sessionRequest.sessionName, data.from);
371
+ isNewSession = true;
372
+ }
373
+ } else if (data.sessionRequest.newSession) {
374
+ // Create new session
375
+ sessionId = sessionManager.createSession(data.sessionRequest.sessionName, data.from);
376
+ isNewSession = true;
377
+ logToFile(`[SESSION] Client requesting new session: ${sessionId}`);
378
+ } else {
379
+ // Default: create new session if no specific request
380
+ sessionId = sessionManager.createSession(null, data.from);
381
+ isNewSession = true;
382
+ }
383
+ } else {
384
+ // Backward compatibility: no session request means create default session
385
+ sessionId = sessionManager.createSession(null, data.from);
386
+ isNewSession = true;
387
+ }
388
+
389
+ if (!sessionId) {
390
+ logToFile(`[SESSION] ❌ Failed to create/connect to session`);
391
+ sendMessage({
392
+ type: 'error',
393
+ message: 'Failed to create session - maximum sessions reached',
394
+ to: data.from,
395
+ from: AGENT_ID
396
+ });
397
+ break;
398
+ }
399
+
400
+ // Connect client to session
401
+ sessionManager.connectClientToSession(data.from, sessionId);
402
+
100
403
  await createPeerConnection(data.from);
101
404
  logToFile('📡 PeerConnection created, generating offer...');
102
405
  const offer = await peerConnection.createOffer();
103
406
  logToFile(`📋 Offer created: ${offer.type}`);
104
407
  await peerConnection.setLocalDescription(offer);
105
- logToFile('📤 Sending WebRTC offer to client.');
106
- sendMessage({ type: 'offer', sdp: offer.sdp, to: data.from, from: AGENT_ID });
107
- logToFile('✅ WebRTC offer sent successfully');
408
+
409
+ // Send WebRTC offer with session assignment
410
+ sendMessage({
411
+ type: 'offer',
412
+ sdp: offer.sdp,
413
+ to: data.from,
414
+ from: AGENT_ID,
415
+ sessionId: sessionId,
416
+ sessionName: sessionManager.getSession(sessionId).name,
417
+ isNewSession: isNewSession,
418
+ availableSessions: availableSessions
419
+ });
420
+ logToFile('✅ WebRTC offer sent with session assignment');
108
421
 
109
422
  // Force ICE gathering if it hasn't started within 2 seconds
110
423
  logToFile('[AGENT] 🔧 Setting up ICE gathering fallback timer...');
@@ -204,8 +517,7 @@ async function createPeerConnection(clientId) {
204
517
  dataChannel = peerConnection.createDataChannel('terminal', {
205
518
  ordered: true
206
519
  });
207
- setupDataChannel();
208
- setupTerminal();
520
+ setupDataChannel(clientId);
209
521
 
210
522
  peerConnection.ondatachannel = (event) => {
211
523
  logToFile('[AGENT] Additional data channel received (this should not happen)');
@@ -235,11 +547,11 @@ async function createPeerConnection(clientId) {
235
547
  break;
236
548
  case 'failed':
237
549
  logToFile('[AGENT] ❌ ICE connection failed - no viable candidates');
238
- cleanup();
550
+ cleanup(clientId);
239
551
  break;
240
552
  case 'disconnected':
241
553
  logToFile('[AGENT] ⚠️ ICE connection disconnected');
242
- cleanup();
554
+ cleanup(clientId);
243
555
  break;
244
556
  case 'closed':
245
557
  logToFile('[AGENT] 🔐 ICE connection closed');
@@ -299,11 +611,12 @@ async function createPeerConnection(clientId) {
299
611
  };
300
612
  }
301
613
 
302
- function cleanup() {
303
- if (term) {
304
- term.kill();
305
- term = null;
614
+ function cleanup(clientId = null) {
615
+ // Disconnect client from session manager
616
+ if (clientId) {
617
+ sessionManager.disconnectClient(clientId);
306
618
  }
619
+
307
620
  if (dataChannel) {
308
621
  dataChannel.close();
309
622
  dataChannel = null;
@@ -314,7 +627,7 @@ function cleanup() {
314
627
  }
315
628
  }
316
629
 
317
- function setupDataChannel() {
630
+ function setupDataChannel(clientId) {
318
631
  dataChannel.onopen = () => {
319
632
  logToFile('[AGENT] ✅ Data channel is open!');
320
633
  };
@@ -322,11 +635,35 @@ function setupDataChannel() {
322
635
  dataChannel.onmessage = (event) => {
323
636
  try {
324
637
  const message = JSON.parse(event.data);
325
- if (term && message.type === 'input') {
326
- term.write(message.data);
327
- } else if (term && message.type === 'resize') {
328
- logToFile(`[AGENT] Resizing terminal to ${message.cols}x${message.rows}`);
329
- term.resize(message.cols, message.rows);
638
+ const session = sessionManager.getClientSession(clientId);
639
+
640
+ if (!session) {
641
+ logToFile(`[AGENT] ⚠️ No session found for client ${clientId}`);
642
+ return;
643
+ }
644
+
645
+ if (message.type === 'input') {
646
+ sessionManager.writeToSession(session.id, message.data);
647
+ } else if (message.type === 'resize') {
648
+ logToFile(`[AGENT] Resizing session ${session.id} to ${message.cols}x${message.rows}`);
649
+ sessionManager.resizeSession(session.id, message.cols, message.rows);
650
+ } else if (message.type === 'session-switch') {
651
+ // Handle session switching
652
+ logToFile(`[AGENT] Client ${clientId} switching to session ${message.sessionId}`);
653
+ if (sessionManager.connectClientToSession(clientId, message.sessionId)) {
654
+ // Send confirmation and buffered output
655
+ const newSession = sessionManager.getSession(message.sessionId);
656
+ dataChannel.send(JSON.stringify({
657
+ type: 'session-switched',
658
+ sessionId: message.sessionId,
659
+ sessionName: newSession.name
660
+ }));
661
+ } else {
662
+ dataChannel.send(JSON.stringify({
663
+ type: 'error',
664
+ message: `Session ${message.sessionId} not found`
665
+ }));
666
+ }
330
667
  }
331
668
  } catch (err) {
332
669
  logToFile(`[AGENT] Error parsing data channel message: ${err.message}`);
@@ -335,7 +672,7 @@ function setupDataChannel() {
335
672
 
336
673
  dataChannel.onclose = () => {
337
674
  logToFile('[AGENT] Data channel closed.');
338
- cleanup();
675
+ cleanup(clientId);
339
676
  };
340
677
 
341
678
  dataChannel.onerror = (error) => {
@@ -343,50 +680,6 @@ function setupDataChannel() {
343
680
  };
344
681
  }
345
682
 
346
- function setupTerminal() {
347
- logToFile('[AGENT] Spawning new terminal');
348
-
349
- // Use zsh (default on modern macOS) instead of bash for Mac-like experience
350
- const macShell = os.platform() === 'darwin' ? '/bin/zsh' : shell;
351
-
352
- // Create enhanced environment for Mac terminal appearance
353
- const terminalEnv = {
354
- ...process.env,
355
- TERM: 'xterm-256color', // Enable full 256 color support
356
- COLORTERM: 'truecolor', // Enable true color support
357
- LANG: 'en_US.UTF-8', // Proper locale for Mac
358
- LC_ALL: 'en_US.UTF-8', // Full UTF-8 support
359
- SHELL: macShell, // Set proper shell
360
- TERM_PROGRAM: 'Terminal', // Mimic macOS Terminal.app
361
- TERM_PROGRAM_VERSION: '2.12.7' // Recent Terminal.app version
362
- };
363
-
364
- term = pty.spawn(macShell, ['--login'], {
365
- name: 'xterm-256color', // Full color terminal emulation
366
- cols: 120, // Wider default like Mac terminal
367
- rows: 30,
368
- cwd: process.env.HOME,
369
- env: terminalEnv,
370
- encoding: 'utf8' // Ensure UTF-8 encoding
371
- });
372
-
373
- term.on('data', (data) => {
374
- if (dataChannel && dataChannel.readyState === 'open') {
375
- try {
376
- dataChannel.send(JSON.stringify({ type: 'output', data: data }));
377
- } catch (err) {
378
- logToFile(`[AGENT] Error sending terminal output: ${err.message}`);
379
- }
380
- }
381
- });
382
-
383
- term.on('exit', (code) => {
384
- logToFile(`[AGENT] Terminal process exited with code ${code}`);
385
- cleanup();
386
- });
387
-
388
- logToFile(`[AGENT] ✅ Terminal spawned (PID: ${term.pid})`);
389
- }
390
683
 
391
684
  function sendMessage(message) {
392
685
  if (ws && ws.readyState === WebSocket.OPEN) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.5.40",
3
+ "version": "1.5.41",
4
4
  "description": "Access your Mac shell from any device securely. Perfect for mobile coding with Claude Code CLI, Gemini CLI, and any shell tool.",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -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,31 @@ class ShellMirrorDashboard {
80
81
 
81
82
  if (agentsData.success && agentsData.data && agentsData.data.agents) {
82
83
  this.agents = agentsData.data.agents;
84
+
85
+ // Simulate session data for each agent
86
+ this.agents.forEach(agent => {
87
+ if (agent.onlineStatus === 'online') {
88
+ // Mock sessions for online agents
89
+ this.agentSessions[agent.agentId] = [
90
+ {
91
+ id: `ses_${Date.now()}_main`,
92
+ name: 'Main Terminal',
93
+ lastActivity: Date.now() - 5 * 60 * 1000, // 5 minutes ago
94
+ createdAt: Date.now() - 2 * 60 * 60 * 1000, // 2 hours ago
95
+ status: 'active',
96
+ connectedClients: 0
97
+ },
98
+ {
99
+ id: `ses_${Date.now()}_debug`,
100
+ name: 'Debug Server',
101
+ lastActivity: Date.now() - 15 * 60 * 1000, // 15 minutes ago
102
+ createdAt: Date.now() - 1 * 60 * 60 * 1000, // 1 hour ago
103
+ status: 'active',
104
+ connectedClients: 0
105
+ }
106
+ ];
107
+ }
108
+ });
83
109
  }
84
110
 
85
111
  // TODO: Load session history when API is available
@@ -174,18 +200,29 @@ class ShellMirrorDashboard {
174
200
  });
175
201
 
176
202
  const agentCount = activeAgents.length;
177
- const agentsHtml = activeAgents.map(agent => `
203
+ const agentsHtml = activeAgents.map(agent => {
204
+ const sessions = this.agentSessions[agent.agentId] || [];
205
+ const sessionCount = sessions.length;
206
+
207
+ return `
178
208
  <div class="agent-item">
179
209
  <div class="agent-info">
180
210
  <div class="agent-name">${agent.machineName || agent.agentId}</div>
181
211
  <div class="agent-status ${agent.onlineStatus}">${agent.onlineStatus}</div>
182
212
  <div class="agent-last-seen">Last seen: ${this.formatLastSeen(agent.lastSeen)}</div>
213
+ ${sessionCount > 0 ? `<div class="agent-sessions">${sessionCount} active session${sessionCount !== 1 ? 's' : ''}</div>` : ''}
214
+ </div>
215
+ <div class="agent-actions">
216
+ <button class="btn-connect" onclick="dashboard.connectToAgent('${agent.agentId}')">
217
+ New Session
218
+ </button>
219
+ ${sessionCount > 0 ? `<button class="btn-sessions" onclick="dashboard.showAgentSessions('${agent.agentId}')">
220
+ View Sessions
221
+ </button>` : ''}
183
222
  </div>
184
- <button class="btn-connect" onclick="dashboard.connectToAgent('${agent.agentId}')">
185
- Connect
186
- </button>
187
223
  </div>
188
- `).join('');
224
+ `;
225
+ }).join('');
189
226
 
190
227
  return `
191
228
  <div class="dashboard-card">
@@ -401,8 +438,96 @@ class ShellMirrorDashboard {
401
438
  window.location.href = `/app/terminal.html?agent=${agentId}`;
402
439
  }
403
440
 
441
+ async connectToSession(agentId, sessionId) {
442
+ window.location.href = `/app/terminal.html?agent=${agentId}&session=${sessionId}`;
443
+ }
444
+
404
445
  startNewSession() {
405
- window.location.href = '/app/terminal.html';
446
+ // Get first available agent for new session
447
+ const activeAgents = this.agents.filter(agent => {
448
+ const timeSinceLastSeen = Date.now() / 1000 - agent.lastSeen;
449
+ return agent.onlineStatus === 'online' || timeSinceLastSeen < 300;
450
+ });
451
+
452
+ if (activeAgents.length > 0) {
453
+ this.connectToAgent(activeAgents[0].agentId);
454
+ } else {
455
+ alert('No active agents available. Please ensure an agent is running on your Mac.');
456
+ }
457
+ }
458
+
459
+ showAgentSessions(agentId) {
460
+ const sessions = this.agentSessions[agentId] || [];
461
+ const agent = this.agents.find(a => a.agentId === agentId);
462
+ const agentName = agent ? (agent.machineName || agent.agentId) : agentId;
463
+
464
+ if (sessions.length === 0) {
465
+ alert(`No active sessions found for ${agentName}`);
466
+ return;
467
+ }
468
+
469
+ // Create session list modal
470
+ const sessionsList = sessions.map(session => `
471
+ <div class="session-list-item" onclick="dashboard.connectToSession('${agentId}', '${session.id}')">
472
+ <div class="session-list-info">
473
+ <div class="session-list-name">${session.name}</div>
474
+ <div class="session-list-details">
475
+ <span class="session-list-id">${session.id.substring(0, 8)}...</span>
476
+ <span class="session-list-activity">Last activity: ${this.formatLastActivity(session.lastActivity)}</span>
477
+ </div>
478
+ </div>
479
+ <div class="session-list-status ${session.status}">${session.status}</div>
480
+ </div>
481
+ `).join('');
482
+
483
+ // Show modal with sessions
484
+ this.showModal(`Sessions on ${agentName}`, `
485
+ <div class="sessions-modal-content">
486
+ <p>Active terminal sessions (${sessions.length}):</p>
487
+ <div class="session-list">
488
+ ${sessionsList}
489
+ </div>
490
+ <div class="sessions-modal-actions">
491
+ <button class="btn-primary" onclick="dashboard.connectToAgent('${agentId}')">
492
+ + Create New Session
493
+ </button>
494
+ </div>
495
+ </div>
496
+ `);
497
+ }
498
+
499
+ showModal(title, content) {
500
+ // Create modal overlay
501
+ const modalOverlay = document.createElement('div');
502
+ modalOverlay.className = 'modal-overlay';
503
+ modalOverlay.onclick = () => document.body.removeChild(modalOverlay);
504
+
505
+ modalOverlay.innerHTML = `
506
+ <div class="modal" onclick="event.stopPropagation()">
507
+ <div class="modal-header">
508
+ <h3>${title}</h3>
509
+ <button class="modal-close" onclick="document.body.removeChild(this.closest('.modal-overlay'))">×</button>
510
+ </div>
511
+ <div class="modal-body">
512
+ ${content}
513
+ </div>
514
+ </div>
515
+ `;
516
+
517
+ document.body.appendChild(modalOverlay);
518
+ }
519
+
520
+ formatLastActivity(timestamp) {
521
+ const now = Date.now();
522
+ const diff = now - timestamp;
523
+ const minutes = Math.floor(diff / 60000);
524
+ const hours = Math.floor(diff / 3600000);
525
+ const days = Math.floor(diff / 86400000);
526
+
527
+ if (minutes < 1) return 'now';
528
+ if (minutes < 60) return `${minutes}m ago`;
529
+ if (hours < 24) return `${hours}h ago`;
530
+ return `${days}d ago`;
406
531
  }
407
532
 
408
533
  showAgentInstructions() {
@@ -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
 
@@ -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 parameter
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
- 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)');
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.style.display = 'block';
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
- 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
+ });
168
191
  if (!sent) {
169
192
  console.error(`[CLIENT] ❌ Failed to send client-hello - stopping attempts`);
170
193
  clearInterval(intervalId);
@@ -190,6 +213,24 @@ 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
+
229
+ if (nextData.availableSessions) {
230
+ availableSessions = nextData.availableSessions;
231
+ console.log('[CLIENT] 📚 Available sessions:', availableSessions);
232
+ }
233
+
193
234
  console.log('[CLIENT] Received WebRTC offer from agent.');
194
235
  await createPeerConnection();
195
236
  await peerConnection.setRemoteDescription(new RTCSessionDescription(nextData));
@@ -459,6 +500,9 @@ function setupDataChannel() {
459
500
  const message = JSON.parse(event.data);
460
501
  if (message.type === 'output') {
461
502
  term.write(message.data);
503
+ } else {
504
+ // Handle session-related messages
505
+ handleSessionMessage(message);
462
506
  }
463
507
  } catch (err) {
464
508
  console.error('[CLIENT] Error parsing data channel message:', err);
@@ -521,4 +565,133 @@ function sendMessage(message) {
521
565
  console.error(`[CLIENT] ❌ Error sending message:`, error);
522
566
  return false;
523
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
+ }
524
697
  }