treesap 0.1.9 → 0.1.11
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/dist/components/ChatInput.d.ts +7 -0
- package/dist/components/ChatInput.d.ts.map +1 -0
- package/dist/components/ChatInput.js +11 -0
- package/dist/components/ChatInput.js.map +1 -0
- package/dist/components/Sidebar.d.ts +8 -0
- package/dist/components/Sidebar.d.ts.map +1 -0
- package/dist/components/Sidebar.js +7 -0
- package/dist/components/Sidebar.js.map +1 -0
- package/dist/components/SimpleLivePreview.js +1 -1
- package/dist/components/SimpleLivePreview.js.map +1 -1
- package/dist/pages/Code.d.ts.map +1 -1
- package/dist/pages/Code.js +2 -2
- package/dist/pages/Code.js.map +1 -1
- package/dist/services/websocket.d.ts.map +1 -1
- package/dist/services/websocket.js +1 -2
- package/dist/services/websocket.js.map +1 -1
- package/dist/static/components/ChatInput.js +237 -0
- package/dist/static/components/Sidebar.js +225 -0
- package/dist/static/components/SimpleLivePreview.js +73 -53
- package/dist/static/components/Terminal.js +143 -61
- package/dist/static/signals/SidebarSignal.js +123 -0
- package/dist/static/signals/TerminalSignal.js +137 -2
- package/dist/static/styles/main.css +180 -0
- package/package.json +1 -1
- package/src/components/ChatInput.tsx +56 -0
- package/src/components/Sidebar.tsx +99 -0
- package/src/components/SimpleLivePreview.tsx +4 -4
- package/src/pages/Code.tsx +18 -55
- package/src/services/websocket.ts +1 -5
- package/src/static/components/ChatInput.js +237 -0
- package/src/static/components/Sidebar.js +225 -0
- package/src/static/components/SimpleLivePreview.js +73 -53
- package/src/static/components/Terminal.js +143 -61
- package/src/static/signals/SidebarSignal.js +123 -0
- package/src/static/signals/TerminalSignal.js +137 -2
- package/src/static/styles/main.css +180 -0
- package/tailwind.config.ts +10 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Terminal component JavaScript using Xterm.js
|
|
1
|
+
// Terminal component JavaScript using Xterm.js with WebSocket
|
|
2
2
|
import { Terminal } from 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/+esm';
|
|
3
3
|
import { terminalStore } from '/signals/TerminalSignal.js';
|
|
4
|
+
|
|
4
5
|
class TerminalManager {
|
|
5
6
|
constructor(terminalId) {
|
|
6
7
|
this.terminalId = terminalId;
|
|
@@ -25,8 +26,12 @@ class TerminalManager {
|
|
|
25
26
|
terminalStore.addTerminal(this.index);
|
|
26
27
|
terminalStore.updateTerminalStatus(terminalId, 'connecting');
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
// WebSocket connection
|
|
30
|
+
this.websocket = null;
|
|
29
31
|
this.terminal = null;
|
|
32
|
+
this.reconnectAttempts = 0;
|
|
33
|
+
this.maxReconnectAttempts = 5;
|
|
34
|
+
this.reconnectDelay = 1000; // Start with 1 second
|
|
30
35
|
|
|
31
36
|
this.init();
|
|
32
37
|
}
|
|
@@ -48,7 +53,7 @@ class TerminalManager {
|
|
|
48
53
|
// Set up event listeners
|
|
49
54
|
this.setupEventListeners();
|
|
50
55
|
|
|
51
|
-
// Connect to terminal
|
|
56
|
+
// Connect to terminal via WebSocket
|
|
52
57
|
this.connectToTerminal();
|
|
53
58
|
}
|
|
54
59
|
|
|
@@ -94,6 +99,8 @@ class TerminalManager {
|
|
|
94
99
|
|
|
95
100
|
// Handle terminal input - pass through to shell
|
|
96
101
|
this.terminal.onData((data) => {
|
|
102
|
+
// Log the input data for debugging
|
|
103
|
+
console.log('Terminal input (manual typing):', JSON.stringify(data), 'char codes:', data.split('').map(c => c.charCodeAt(0)));
|
|
97
104
|
// Send all input directly to the shell session
|
|
98
105
|
this.sendInput(data);
|
|
99
106
|
});
|
|
@@ -136,65 +143,147 @@ class TerminalManager {
|
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
sendInput(data) {
|
|
139
|
-
// Send input
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
// Send input via WebSocket
|
|
147
|
+
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
|
148
|
+
const message = {
|
|
149
|
+
type: 'input',
|
|
150
|
+
sessionId: this.sessionId,
|
|
151
|
+
terminalId: this.terminalId,
|
|
152
|
+
data: data
|
|
153
|
+
};
|
|
154
|
+
this.websocket.send(JSON.stringify(message));
|
|
155
|
+
} else {
|
|
156
|
+
console.error('WebSocket not connected, cannot send input');
|
|
157
|
+
this.updateStatus('Disconnected');
|
|
158
|
+
terminalStore.updateTerminalStatus(this.terminalId, 'disconnected');
|
|
159
|
+
}
|
|
150
160
|
}
|
|
151
161
|
|
|
152
162
|
connectToTerminal() {
|
|
153
|
-
if (this.
|
|
154
|
-
this.
|
|
163
|
+
if (this.websocket) {
|
|
164
|
+
this.websocket.close();
|
|
155
165
|
}
|
|
156
166
|
|
|
157
167
|
this.updateStatus('Connecting...');
|
|
158
168
|
|
|
159
|
-
|
|
169
|
+
// Create WebSocket connection
|
|
170
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
171
|
+
const wsUrl = `${protocol}//${window.location.host}/terminal/ws`;
|
|
160
172
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
console.log(`Connecting to WebSocket: ${wsUrl}`);
|
|
174
|
+
this.websocket = new WebSocket(wsUrl);
|
|
175
|
+
|
|
176
|
+
this.websocket.onopen = () => {
|
|
177
|
+
console.log('WebSocket connected, joining terminal session');
|
|
178
|
+
this.reconnectAttempts = 0;
|
|
179
|
+
this.reconnectDelay = 1000;
|
|
180
|
+
|
|
181
|
+
// Join the terminal session
|
|
182
|
+
const joinMessage = {
|
|
183
|
+
type: 'join',
|
|
184
|
+
sessionId: this.sessionId,
|
|
185
|
+
terminalId: this.terminalId
|
|
186
|
+
};
|
|
187
|
+
this.websocket.send(JSON.stringify(joinMessage));
|
|
164
188
|
};
|
|
165
189
|
|
|
166
|
-
this.
|
|
190
|
+
this.websocket.onmessage = (event) => {
|
|
167
191
|
try {
|
|
168
192
|
const data = JSON.parse(event.data);
|
|
193
|
+
console.log('Received WebSocket message:', data.type);
|
|
169
194
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
195
|
+
switch (data.type) {
|
|
196
|
+
case 'connected':
|
|
197
|
+
this.updateStatus('Ready');
|
|
198
|
+
terminalStore.updateTerminalStatus(this.terminalId, 'connected');
|
|
199
|
+
console.log('Terminal session joined successfully');
|
|
200
|
+
// Dispatch global status for cross-tab sync
|
|
201
|
+
document.dispatchEvent(new CustomEvent('terminal:global_status', {
|
|
202
|
+
detail: { status: 'connected' }
|
|
203
|
+
}));
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case 'output':
|
|
207
|
+
if (data.content) {
|
|
208
|
+
this.terminal.write(data.content);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
case 'error':
|
|
213
|
+
if (data.content) {
|
|
214
|
+
this.terminal.write(`\x1b[31m${data.content}\x1b[0m`);
|
|
215
|
+
} else if (data.data) {
|
|
216
|
+
this.terminal.write(`\x1b[31m${data.data}\x1b[0m`);
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case 'exit':
|
|
221
|
+
this.terminal.writeln(`\x1b[90mProcess exited with code ${data.code || 0}\x1b[0m`);
|
|
222
|
+
this.terminal.write('\x1b[32m$ \x1b[0m');
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case 'clients_count':
|
|
226
|
+
console.log(`${data.count} clients connected to this session`);
|
|
227
|
+
// Dispatch event for TerminalSignal to handle cross-tab sync
|
|
228
|
+
document.dispatchEvent(new CustomEvent('terminal:clients_count', {
|
|
229
|
+
detail: {
|
|
230
|
+
sessionId: this.sessionId,
|
|
231
|
+
count: data.count
|
|
232
|
+
}
|
|
233
|
+
}));
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case 'session_closed':
|
|
237
|
+
console.log('Terminal session was closed');
|
|
238
|
+
this.updateStatus('Session Closed');
|
|
239
|
+
terminalStore.updateTerminalStatus(this.terminalId, 'closed');
|
|
240
|
+
// Dispatch event for TerminalSignal to handle cross-tab sync
|
|
241
|
+
document.dispatchEvent(new CustomEvent('terminal:session_closed', {
|
|
242
|
+
detail: {
|
|
243
|
+
sessionId: this.sessionId
|
|
244
|
+
}
|
|
245
|
+
}));
|
|
246
|
+
break;
|
|
247
|
+
|
|
248
|
+
case 'pong':
|
|
249
|
+
// Connection health check response
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
default:
|
|
253
|
+
console.log('Unknown message type:', data.type, data);
|
|
180
254
|
}
|
|
181
255
|
} catch (error) {
|
|
182
|
-
console.error('Error parsing
|
|
256
|
+
console.error('Error parsing WebSocket message:', error);
|
|
183
257
|
}
|
|
184
258
|
};
|
|
185
259
|
|
|
186
|
-
this.
|
|
187
|
-
console.error('
|
|
260
|
+
this.websocket.onerror = (error) => {
|
|
261
|
+
console.error('WebSocket error:', error);
|
|
262
|
+
this.updateStatus('Connection Error');
|
|
263
|
+
terminalStore.updateTerminalStatus(this.terminalId, 'error');
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
this.websocket.onclose = (event) => {
|
|
267
|
+
console.log('WebSocket closed:', event.code, event.reason);
|
|
188
268
|
this.updateStatus('Disconnected');
|
|
189
269
|
terminalStore.updateTerminalStatus(this.terminalId, 'disconnected');
|
|
190
270
|
|
|
191
|
-
// Attempt to reconnect
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
271
|
+
// Attempt to reconnect with exponential backoff
|
|
272
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
273
|
+
this.reconnectAttempts++;
|
|
274
|
+
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts}) in ${this.reconnectDelay}ms`);
|
|
275
|
+
|
|
276
|
+
setTimeout(() => {
|
|
195
277
|
this.connectToTerminal();
|
|
196
|
-
}
|
|
197
|
-
|
|
278
|
+
}, this.reconnectDelay);
|
|
279
|
+
|
|
280
|
+
// Exponential backoff: double the delay each time, max 10 seconds
|
|
281
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 10000);
|
|
282
|
+
} else {
|
|
283
|
+
console.error('Max reconnection attempts reached');
|
|
284
|
+
this.updateStatus('Connection Failed');
|
|
285
|
+
terminalStore.updateTerminalStatus(this.terminalId, 'failed');
|
|
286
|
+
}
|
|
198
287
|
};
|
|
199
288
|
}
|
|
200
289
|
|
|
@@ -213,32 +302,25 @@ class TerminalManager {
|
|
|
213
302
|
}
|
|
214
303
|
|
|
215
304
|
async destroy() {
|
|
216
|
-
|
|
217
|
-
|
|
305
|
+
// Send leave message to WebSocket before closing
|
|
306
|
+
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
|
307
|
+
const leaveMessage = {
|
|
308
|
+
type: 'leave',
|
|
309
|
+
sessionId: this.sessionId,
|
|
310
|
+
terminalId: this.terminalId
|
|
311
|
+
};
|
|
312
|
+
this.websocket.send(JSON.stringify(leaveMessage));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (this.websocket) {
|
|
316
|
+
this.websocket.close();
|
|
218
317
|
}
|
|
219
318
|
if (this.terminal) {
|
|
220
319
|
this.terminal.dispose();
|
|
221
320
|
}
|
|
222
321
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
if (sessionId) {
|
|
226
|
-
try {
|
|
227
|
-
const response = await fetch(`/terminal/session/${sessionId}`, {
|
|
228
|
-
method: 'DELETE'
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
if (response.ok) {
|
|
232
|
-
console.log(`Terminal session ${sessionId} destroyed on server`);
|
|
233
|
-
} else {
|
|
234
|
-
console.warn(`Failed to destroy terminal session ${sessionId} on server:`, await response.text());
|
|
235
|
-
}
|
|
236
|
-
} catch (error) {
|
|
237
|
-
console.error(`Error destroying terminal session ${sessionId}:`, error);
|
|
238
|
-
}
|
|
239
|
-
} else {
|
|
240
|
-
console.warn(`No sessionId found for terminal ${this.terminalId}`);
|
|
241
|
-
}
|
|
322
|
+
// Note: We don't destroy the server-side session anymore since other tabs might be using it
|
|
323
|
+
// The WebSocket service will manage session cleanup when all clients disconnect
|
|
242
324
|
|
|
243
325
|
// Remove from store
|
|
244
326
|
terminalStore.removeTerminal(this.terminalId);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { signal, computed } from 'https://esm.sh/@preact/signals@1.2.2';
|
|
2
|
+
|
|
3
|
+
// Sidebar state management
|
|
4
|
+
class SidebarStore {
|
|
5
|
+
constructor() {
|
|
6
|
+
// Core state signals
|
|
7
|
+
this.isOpen = signal(true); // Default to open on desktop
|
|
8
|
+
this.isMobile = signal(false); // Track if we're in mobile viewport
|
|
9
|
+
this.mobileBreakpoint = signal(768); // Breakpoint for mobile/desktop detection
|
|
10
|
+
|
|
11
|
+
// Computed values
|
|
12
|
+
this.shouldShowBackdrop = computed(() => this.isMobile.value && this.isOpen.value);
|
|
13
|
+
this.shouldShowMobileToggle = computed(() => this.isMobile.value && !this.isOpen.value);
|
|
14
|
+
this.shouldShowFloatingButton = computed(() => !this.isMobile.value && !this.isOpen.value);
|
|
15
|
+
|
|
16
|
+
// Initialize mobile detection
|
|
17
|
+
this.initializeMobileDetection();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Initialize mobile detection and set initial state
|
|
21
|
+
initializeMobileDetection() {
|
|
22
|
+
if (typeof window !== 'undefined') {
|
|
23
|
+
this.updateMobileState();
|
|
24
|
+
|
|
25
|
+
// Listen for window resize
|
|
26
|
+
window.addEventListener('resize', () => {
|
|
27
|
+
this.updateMobileState();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Update mobile state based on window width
|
|
33
|
+
updateMobileState() {
|
|
34
|
+
if (typeof window === 'undefined') return;
|
|
35
|
+
|
|
36
|
+
const wasMobile = this.isMobile.value;
|
|
37
|
+
const nowMobile = window.innerWidth < this.mobileBreakpoint.value;
|
|
38
|
+
|
|
39
|
+
this.isMobile.value = nowMobile;
|
|
40
|
+
|
|
41
|
+
// Handle initial state or breakpoint transitions
|
|
42
|
+
if (wasMobile === undefined) {
|
|
43
|
+
// First time: desktop shows sidebar, mobile hides it
|
|
44
|
+
this.isOpen.value = !nowMobile;
|
|
45
|
+
} else if (wasMobile !== nowMobile) {
|
|
46
|
+
// Breakpoint changed
|
|
47
|
+
if (wasMobile && !nowMobile) {
|
|
48
|
+
// Mobile to desktop: show sidebar
|
|
49
|
+
this.isOpen.value = true;
|
|
50
|
+
} else if (!wasMobile && nowMobile) {
|
|
51
|
+
// Desktop to mobile: hide sidebar
|
|
52
|
+
this.isOpen.value = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Actions for managing sidebar state
|
|
58
|
+
toggle() {
|
|
59
|
+
this.isOpen.value = !this.isOpen.value;
|
|
60
|
+
console.log(`Sidebar toggled: ${this.isOpen.value ? 'open' : 'closed'}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
open() {
|
|
64
|
+
this.isOpen.value = true;
|
|
65
|
+
console.log('Sidebar opened');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
close() {
|
|
69
|
+
this.isOpen.value = false;
|
|
70
|
+
console.log('Sidebar closed');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Set mobile breakpoint
|
|
74
|
+
setMobileBreakpoint(breakpoint) {
|
|
75
|
+
this.mobileBreakpoint.value = breakpoint;
|
|
76
|
+
this.updateMobileState();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get current state (useful for components that need to read state once)
|
|
80
|
+
getState() {
|
|
81
|
+
return {
|
|
82
|
+
isOpen: this.isOpen.value,
|
|
83
|
+
isMobile: this.isMobile.value,
|
|
84
|
+
mobileBreakpoint: this.mobileBreakpoint.value,
|
|
85
|
+
shouldShowBackdrop: this.shouldShowBackdrop.value,
|
|
86
|
+
shouldShowMobileToggle: this.shouldShowMobileToggle.value,
|
|
87
|
+
shouldShowFloatingButton: this.shouldShowFloatingButton.value
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Debug helpers
|
|
92
|
+
logState() {
|
|
93
|
+
console.log('Sidebar Store State:', this.getState());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Subscribe to state changes (for components that need to react to changes)
|
|
97
|
+
onStateChange(callback) {
|
|
98
|
+
// Create an effect that runs when any sidebar state changes
|
|
99
|
+
const unsubscribe = () => {
|
|
100
|
+
// This is a simple implementation - in a full app you might want more granular subscriptions
|
|
101
|
+
const state = this.getState();
|
|
102
|
+
callback(state);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Subscribe to all relevant signals
|
|
106
|
+
this.isOpen.subscribe(unsubscribe);
|
|
107
|
+
this.isMobile.subscribe(unsubscribe);
|
|
108
|
+
|
|
109
|
+
// Return unsubscribe function
|
|
110
|
+
return () => {
|
|
111
|
+
// Note: Preact signals don't have a direct unsubscribe method
|
|
112
|
+
// In a production app, you'd want to implement proper cleanup
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Create and export singleton instance
|
|
118
|
+
export const sidebarStore = new SidebarStore();
|
|
119
|
+
|
|
120
|
+
// Make it available globally for debugging
|
|
121
|
+
if (typeof window !== 'undefined') {
|
|
122
|
+
window.sidebarStore = sidebarStore;
|
|
123
|
+
}
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { signal, computed } from 'https://esm.sh/@preact/signals@1.2.2';
|
|
2
2
|
|
|
3
|
-
// Terminal state management for multiple terminals
|
|
3
|
+
// Terminal state management for multiple terminals with cross-tab sync
|
|
4
4
|
class TerminalStore {
|
|
5
5
|
constructor() {
|
|
6
6
|
// Core state signals
|
|
7
|
-
this.terminals = signal([]); // Array of terminal objects: {id, sessionId, status, index}
|
|
7
|
+
this.terminals = signal([]); // Array of terminal objects: {id, sessionId, status, index, clientCount}
|
|
8
8
|
this.activeTerminalId = signal('terminal-1'); // Currently active terminal
|
|
9
9
|
this.nextTerminalIndex = signal(1); // For generating new terminal indices
|
|
10
10
|
|
|
11
|
+
// Cross-tab sync signals
|
|
12
|
+
this.sessionClients = signal(new Map()); // sessionId -> client count
|
|
13
|
+
this.globalStatus = signal('initializing'); // Overall connection status
|
|
14
|
+
|
|
11
15
|
// Computed values
|
|
12
16
|
this.activeTerminal = computed(() =>
|
|
13
17
|
this.terminals.value.find(t => t.id === this.activeTerminalId.value)
|
|
14
18
|
);
|
|
15
19
|
this.terminalCount = computed(() => this.terminals.value.length);
|
|
20
|
+
this.hasConnectedTerminals = computed(() =>
|
|
21
|
+
this.terminals.value.some(t => t.status === 'connected')
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Initialize cross-tab communication
|
|
25
|
+
this.initializeCrossTabSync();
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
// Actions for managing terminals
|
|
@@ -118,6 +128,131 @@ class TerminalStore {
|
|
|
118
128
|
console.log('Terminal Store State:', this.getState());
|
|
119
129
|
}
|
|
120
130
|
|
|
131
|
+
// Initialize cross-tab communication
|
|
132
|
+
initializeCrossTabSync() {
|
|
133
|
+
// Listen for WebSocket events from terminal managers
|
|
134
|
+
this.setupWebSocketEventListeners();
|
|
135
|
+
|
|
136
|
+
// Listen for browser storage events for cross-tab sync
|
|
137
|
+
this.setupStorageEventListeners();
|
|
138
|
+
|
|
139
|
+
// Initialize from any existing state in localStorage
|
|
140
|
+
this.loadStateFromStorage();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
setupWebSocketEventListeners() {
|
|
144
|
+
// Custom events dispatched by TerminalManager
|
|
145
|
+
document.addEventListener('terminal:clients_count', (event) => {
|
|
146
|
+
this.updateSessionClientCount(event.detail.sessionId, event.detail.count);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
document.addEventListener('terminal:session_closed', (event) => {
|
|
150
|
+
this.handleSessionClosed(event.detail.sessionId);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
document.addEventListener('terminal:global_status', (event) => {
|
|
154
|
+
this.globalStatus.value = event.detail.status;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
setupStorageEventListeners() {
|
|
159
|
+
// Listen for changes from other tabs
|
|
160
|
+
window.addEventListener('storage', (event) => {
|
|
161
|
+
if (event.key === 'treesap_terminal_state') {
|
|
162
|
+
this.syncFromStorage(event.newValue);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Save state to storage when terminals change
|
|
167
|
+
this.terminals.subscribe(() => {
|
|
168
|
+
this.saveStateToStorage();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
loadStateFromStorage() {
|
|
173
|
+
try {
|
|
174
|
+
const savedState = localStorage.getItem('treesap_terminal_state');
|
|
175
|
+
if (savedState) {
|
|
176
|
+
const state = JSON.parse(savedState);
|
|
177
|
+
// Only sync certain properties, not connection status which is per-tab
|
|
178
|
+
if (state.activeTerminalId) {
|
|
179
|
+
this.activeTerminalId.value = state.activeTerminalId;
|
|
180
|
+
}
|
|
181
|
+
if (state.nextTerminalIndex) {
|
|
182
|
+
this.nextTerminalIndex.value = state.nextTerminalIndex;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error('Error loading terminal state from storage:', error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
saveStateToStorage() {
|
|
191
|
+
try {
|
|
192
|
+
const state = {
|
|
193
|
+
activeTerminalId: this.activeTerminalId.value,
|
|
194
|
+
nextTerminalIndex: this.nextTerminalIndex.value,
|
|
195
|
+
timestamp: Date.now()
|
|
196
|
+
};
|
|
197
|
+
localStorage.setItem('treesap_terminal_state', JSON.stringify(state));
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('Error saving terminal state to storage:', error);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
syncFromStorage(newValueStr) {
|
|
204
|
+
if (!newValueStr) return;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const newState = JSON.parse(newValueStr);
|
|
208
|
+
// Only sync if the change is from another tab (newer timestamp)
|
|
209
|
+
const currentTimestamp = this.lastSaveTimestamp || 0;
|
|
210
|
+
if (newState.timestamp && newState.timestamp > currentTimestamp) {
|
|
211
|
+
if (newState.activeTerminalId) {
|
|
212
|
+
this.activeTerminalId.value = newState.activeTerminalId;
|
|
213
|
+
}
|
|
214
|
+
if (newState.nextTerminalIndex) {
|
|
215
|
+
this.nextTerminalIndex.value = newState.nextTerminalIndex;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error('Error syncing terminal state from storage:', error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
updateSessionClientCount(sessionId, count) {
|
|
224
|
+
const currentMap = new Map(this.sessionClients.value);
|
|
225
|
+
currentMap.set(sessionId, count);
|
|
226
|
+
this.sessionClients.value = currentMap;
|
|
227
|
+
|
|
228
|
+
// Update terminal objects with client count
|
|
229
|
+
this.terminals.value = this.terminals.value.map(terminal => {
|
|
230
|
+
if (terminal.sessionId === sessionId) {
|
|
231
|
+
return { ...terminal, clientCount: count, lastUpdated: new Date() };
|
|
232
|
+
}
|
|
233
|
+
return terminal;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
console.log(`Session ${sessionId} now has ${count} clients`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
handleSessionClosed(sessionId) {
|
|
240
|
+
console.log(`Session ${sessionId} was closed`);
|
|
241
|
+
|
|
242
|
+
// Update terminals for this session
|
|
243
|
+
this.terminals.value = this.terminals.value.map(terminal => {
|
|
244
|
+
if (terminal.sessionId === sessionId) {
|
|
245
|
+
return { ...terminal, status: 'closed', lastUpdated: new Date() };
|
|
246
|
+
}
|
|
247
|
+
return terminal;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Remove from session clients
|
|
251
|
+
const currentMap = new Map(this.sessionClients.value);
|
|
252
|
+
currentMap.delete(sessionId);
|
|
253
|
+
this.sessionClients.value = currentMap;
|
|
254
|
+
}
|
|
255
|
+
|
|
121
256
|
// Initialize default terminal
|
|
122
257
|
initializeDefaultTerminal() {
|
|
123
258
|
if (this.terminals.value.length === 0) {
|