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.
- package/mac-agent/agent-debug.log +9 -94
- package/mac-agent/agent.js +357 -64
- package/package.json +1 -1
- package/public/app/dashboard.css +172 -0
- package/public/app/dashboard.js +131 -6
- package/public/app/terminal.html +118 -1
- package/public/app/terminal.js +177 -4
|
@@ -1,94 +1,9 @@
|
|
|
1
|
-
=== Mac Agent Debug Log Started 2025-08-
|
|
2
|
-
[2025-08-
|
|
3
|
-
[2025-08-
|
|
4
|
-
[2025-08-
|
|
5
|
-
[2025-08-
|
|
6
|
-
[2025-08-
|
|
7
|
-
[2025-08-
|
|
8
|
-
[2025-08-
|
|
9
|
-
[2025-08-
|
|
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
|
package/mac-agent/agent.js
CHANGED
|
@@ -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}.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
logToFile(`[AGENT]
|
|
329
|
-
|
|
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
package/public/app/dashboard.css
CHANGED
|
@@ -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
|
}
|
package/public/app/dashboard.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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() {
|
package/public/app/terminal.html
CHANGED
|
@@ -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
|
|
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,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
|
}
|