treesap 0.1.9 → 0.1.10
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/Sidebar.d.ts +8 -0
- package/dist/components/Sidebar.d.ts.map +1 -0
- package/dist/components/Sidebar.js +6 -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/Sidebar.js +225 -0
- package/dist/static/components/SimpleLivePreview.js +73 -53
- package/dist/static/components/Terminal.js +141 -61
- package/dist/static/signals/SidebarSignal.js +123 -0
- package/dist/static/signals/TerminalSignal.js +137 -2
- package/dist/static/styles/main.css +111 -0
- package/package.json +1 -1
- package/src/components/Sidebar.tsx +92 -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/Sidebar.js +225 -0
- package/src/static/components/SimpleLivePreview.js +73 -53
- package/src/static/components/Terminal.js +141 -61
- package/src/static/signals/SidebarSignal.js +123 -0
- package/src/static/signals/TerminalSignal.js +137 -2
- package/src/static/styles/main.css +111 -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
|
|
|
@@ -136,65 +141,147 @@ class TerminalManager {
|
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
sendInput(data) {
|
|
139
|
-
// Send input
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
// Send input via WebSocket
|
|
145
|
+
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
|
146
|
+
const message = {
|
|
147
|
+
type: 'input',
|
|
148
|
+
sessionId: this.sessionId,
|
|
149
|
+
terminalId: this.terminalId,
|
|
150
|
+
data: data
|
|
151
|
+
};
|
|
152
|
+
this.websocket.send(JSON.stringify(message));
|
|
153
|
+
} else {
|
|
154
|
+
console.error('WebSocket not connected, cannot send input');
|
|
155
|
+
this.updateStatus('Disconnected');
|
|
156
|
+
terminalStore.updateTerminalStatus(this.terminalId, 'disconnected');
|
|
157
|
+
}
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
connectToTerminal() {
|
|
153
|
-
if (this.
|
|
154
|
-
this.
|
|
161
|
+
if (this.websocket) {
|
|
162
|
+
this.websocket.close();
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
this.updateStatus('Connecting...');
|
|
158
166
|
|
|
159
|
-
|
|
167
|
+
// Create WebSocket connection
|
|
168
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
169
|
+
const wsUrl = `${protocol}//${window.location.host}/terminal/ws`;
|
|
160
170
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
console.log(`Connecting to WebSocket: ${wsUrl}`);
|
|
172
|
+
this.websocket = new WebSocket(wsUrl);
|
|
173
|
+
|
|
174
|
+
this.websocket.onopen = () => {
|
|
175
|
+
console.log('WebSocket connected, joining terminal session');
|
|
176
|
+
this.reconnectAttempts = 0;
|
|
177
|
+
this.reconnectDelay = 1000;
|
|
178
|
+
|
|
179
|
+
// Join the terminal session
|
|
180
|
+
const joinMessage = {
|
|
181
|
+
type: 'join',
|
|
182
|
+
sessionId: this.sessionId,
|
|
183
|
+
terminalId: this.terminalId
|
|
184
|
+
};
|
|
185
|
+
this.websocket.send(JSON.stringify(joinMessage));
|
|
164
186
|
};
|
|
165
187
|
|
|
166
|
-
this.
|
|
188
|
+
this.websocket.onmessage = (event) => {
|
|
167
189
|
try {
|
|
168
190
|
const data = JSON.parse(event.data);
|
|
191
|
+
console.log('Received WebSocket message:', data.type);
|
|
169
192
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
193
|
+
switch (data.type) {
|
|
194
|
+
case 'connected':
|
|
195
|
+
this.updateStatus('Ready');
|
|
196
|
+
terminalStore.updateTerminalStatus(this.terminalId, 'connected');
|
|
197
|
+
console.log('Terminal session joined successfully');
|
|
198
|
+
// Dispatch global status for cross-tab sync
|
|
199
|
+
document.dispatchEvent(new CustomEvent('terminal:global_status', {
|
|
200
|
+
detail: { status: 'connected' }
|
|
201
|
+
}));
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 'output':
|
|
205
|
+
if (data.content) {
|
|
206
|
+
this.terminal.write(data.content);
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
|
|
210
|
+
case 'error':
|
|
211
|
+
if (data.content) {
|
|
212
|
+
this.terminal.write(`\x1b[31m${data.content}\x1b[0m`);
|
|
213
|
+
} else if (data.data) {
|
|
214
|
+
this.terminal.write(`\x1b[31m${data.data}\x1b[0m`);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case 'exit':
|
|
219
|
+
this.terminal.writeln(`\x1b[90mProcess exited with code ${data.code || 0}\x1b[0m`);
|
|
220
|
+
this.terminal.write('\x1b[32m$ \x1b[0m');
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case 'clients_count':
|
|
224
|
+
console.log(`${data.count} clients connected to this session`);
|
|
225
|
+
// Dispatch event for TerminalSignal to handle cross-tab sync
|
|
226
|
+
document.dispatchEvent(new CustomEvent('terminal:clients_count', {
|
|
227
|
+
detail: {
|
|
228
|
+
sessionId: this.sessionId,
|
|
229
|
+
count: data.count
|
|
230
|
+
}
|
|
231
|
+
}));
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
case 'session_closed':
|
|
235
|
+
console.log('Terminal session was closed');
|
|
236
|
+
this.updateStatus('Session Closed');
|
|
237
|
+
terminalStore.updateTerminalStatus(this.terminalId, 'closed');
|
|
238
|
+
// Dispatch event for TerminalSignal to handle cross-tab sync
|
|
239
|
+
document.dispatchEvent(new CustomEvent('terminal:session_closed', {
|
|
240
|
+
detail: {
|
|
241
|
+
sessionId: this.sessionId
|
|
242
|
+
}
|
|
243
|
+
}));
|
|
244
|
+
break;
|
|
245
|
+
|
|
246
|
+
case 'pong':
|
|
247
|
+
// Connection health check response
|
|
248
|
+
break;
|
|
249
|
+
|
|
250
|
+
default:
|
|
251
|
+
console.log('Unknown message type:', data.type, data);
|
|
180
252
|
}
|
|
181
253
|
} catch (error) {
|
|
182
|
-
console.error('Error parsing
|
|
254
|
+
console.error('Error parsing WebSocket message:', error);
|
|
183
255
|
}
|
|
184
256
|
};
|
|
185
257
|
|
|
186
|
-
this.
|
|
187
|
-
console.error('
|
|
258
|
+
this.websocket.onerror = (error) => {
|
|
259
|
+
console.error('WebSocket error:', error);
|
|
260
|
+
this.updateStatus('Connection Error');
|
|
261
|
+
terminalStore.updateTerminalStatus(this.terminalId, 'error');
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
this.websocket.onclose = (event) => {
|
|
265
|
+
console.log('WebSocket closed:', event.code, event.reason);
|
|
188
266
|
this.updateStatus('Disconnected');
|
|
189
267
|
terminalStore.updateTerminalStatus(this.terminalId, 'disconnected');
|
|
190
268
|
|
|
191
|
-
// Attempt to reconnect
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
269
|
+
// Attempt to reconnect with exponential backoff
|
|
270
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
271
|
+
this.reconnectAttempts++;
|
|
272
|
+
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts}) in ${this.reconnectDelay}ms`);
|
|
273
|
+
|
|
274
|
+
setTimeout(() => {
|
|
195
275
|
this.connectToTerminal();
|
|
196
|
-
}
|
|
197
|
-
|
|
276
|
+
}, this.reconnectDelay);
|
|
277
|
+
|
|
278
|
+
// Exponential backoff: double the delay each time, max 10 seconds
|
|
279
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 10000);
|
|
280
|
+
} else {
|
|
281
|
+
console.error('Max reconnection attempts reached');
|
|
282
|
+
this.updateStatus('Connection Failed');
|
|
283
|
+
terminalStore.updateTerminalStatus(this.terminalId, 'failed');
|
|
284
|
+
}
|
|
198
285
|
};
|
|
199
286
|
}
|
|
200
287
|
|
|
@@ -213,32 +300,25 @@ class TerminalManager {
|
|
|
213
300
|
}
|
|
214
301
|
|
|
215
302
|
async destroy() {
|
|
216
|
-
|
|
217
|
-
|
|
303
|
+
// Send leave message to WebSocket before closing
|
|
304
|
+
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
|
305
|
+
const leaveMessage = {
|
|
306
|
+
type: 'leave',
|
|
307
|
+
sessionId: this.sessionId,
|
|
308
|
+
terminalId: this.terminalId
|
|
309
|
+
};
|
|
310
|
+
this.websocket.send(JSON.stringify(leaveMessage));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (this.websocket) {
|
|
314
|
+
this.websocket.close();
|
|
218
315
|
}
|
|
219
316
|
if (this.terminal) {
|
|
220
317
|
this.terminal.dispose();
|
|
221
318
|
}
|
|
222
319
|
|
|
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
|
-
}
|
|
320
|
+
// Note: We don't destroy the server-side session anymore since other tabs might be using it
|
|
321
|
+
// The WebSocket service will manage session cleanup when all clients disconnect
|
|
242
322
|
|
|
243
323
|
// Remove from store
|
|
244
324
|
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) {
|