treesap 0.1.8 → 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/layouts/Layout.js +1 -1
- package/dist/layouts/Layout.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/server.d.ts.map +1 -1
- package/dist/server.js +81 -11
- package/dist/server.js.map +1 -1
- package/dist/services/terminal.d.ts +25 -1
- package/dist/services/terminal.d.ts.map +1 -1
- package/dist/services/terminal.js +135 -6
- package/dist/services/terminal.js.map +1 -1
- package/dist/services/websocket.d.ts +45 -0
- package/dist/services/websocket.d.ts.map +1 -0
- package/dist/services/websocket.js +306 -0
- package/dist/services/websocket.js.map +1 -0
- 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 -25
- package/package.json +6 -2
- package/src/components/Sidebar.tsx +92 -0
- package/src/components/SimpleLivePreview.tsx +4 -4
- package/src/layouts/Layout.tsx +1 -1
- package/src/pages/Code.tsx +18 -145
- package/src/server.tsx +97 -12
- package/src/services/terminal.ts +164 -6
- package/src/services/websocket.ts +374 -0
- 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 -25
- package/tailwind.config.ts +10 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// SimpleLivePreview component JavaScript
|
|
2
|
+
import { sidebarStore } from '/signals/SidebarSignal.js';
|
|
3
|
+
|
|
2
4
|
class SimpleLivePreviewManager {
|
|
3
5
|
constructor(id = 'simple-preview') {
|
|
4
6
|
this.id = id;
|
|
@@ -17,8 +19,8 @@ class SimpleLivePreviewManager {
|
|
|
17
19
|
// Get preview port from iframe data attribute
|
|
18
20
|
this.previewPort = this.iframe?.getAttribute('data-preview-port') || 5173;
|
|
19
21
|
|
|
20
|
-
//
|
|
21
|
-
this.
|
|
22
|
+
// Reference to the sidebar store
|
|
23
|
+
this.store = sidebarStore;
|
|
22
24
|
|
|
23
25
|
this.init();
|
|
24
26
|
}
|
|
@@ -34,12 +36,15 @@ class SimpleLivePreviewManager {
|
|
|
34
36
|
|
|
35
37
|
// Set up event listeners
|
|
36
38
|
this.setupEventListeners();
|
|
39
|
+
|
|
40
|
+
// Subscribe to sidebar store changes
|
|
41
|
+
this.subscribeToStore();
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
setupEventListeners() {
|
|
40
45
|
// Hide sidebar toggle (both sidebar and floating button)
|
|
41
|
-
this.hideSidebarBtn?.addEventListener('click', () => this.
|
|
42
|
-
this.floatingHideSidebarBtn?.addEventListener('click', () => this.
|
|
46
|
+
this.hideSidebarBtn?.addEventListener('click', () => this.store.toggle());
|
|
47
|
+
this.floatingHideSidebarBtn?.addEventListener('click', () => this.store.toggle());
|
|
43
48
|
|
|
44
49
|
// Refresh button
|
|
45
50
|
this.refreshBtn?.addEventListener('click', () => this.refreshIframe());
|
|
@@ -62,6 +67,21 @@ class SimpleLivePreviewManager {
|
|
|
62
67
|
this.loadUrl();
|
|
63
68
|
});
|
|
64
69
|
|
|
70
|
+
// Listen for events from Sidebar component
|
|
71
|
+
document.addEventListener('preview:refresh', () => this.refreshIframe());
|
|
72
|
+
document.addEventListener('preview:loadUrl', (e) => {
|
|
73
|
+
if (e.detail && e.detail.path !== undefined) {
|
|
74
|
+
this.loadUrlFromPath(e.detail.path);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Legacy: Listen for sidebar state changes (for backward compatibility)
|
|
79
|
+
document.addEventListener('sidebar:stateChanged', (e) => {
|
|
80
|
+
if (e.detail) {
|
|
81
|
+
this.handleSidebarStateChange(e.detail);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
65
85
|
// Keyboard shortcuts
|
|
66
86
|
document.addEventListener('keydown', (e) => {
|
|
67
87
|
// Cmd/Ctrl + R for refresh
|
|
@@ -69,11 +89,6 @@ class SimpleLivePreviewManager {
|
|
|
69
89
|
e.preventDefault();
|
|
70
90
|
this.refreshIframe();
|
|
71
91
|
}
|
|
72
|
-
// Cmd/Ctrl + B to toggle sidebar panel
|
|
73
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'b') {
|
|
74
|
-
e.preventDefault();
|
|
75
|
-
this.toggleSidebarVisibility();
|
|
76
|
-
}
|
|
77
92
|
});
|
|
78
93
|
|
|
79
94
|
// Handle iframe load errors (for X-Frame-Options violations)
|
|
@@ -129,55 +144,41 @@ class SimpleLivePreviewManager {
|
|
|
129
144
|
}
|
|
130
145
|
}
|
|
131
146
|
|
|
132
|
-
|
|
133
|
-
|
|
147
|
+
subscribeToStore() {
|
|
148
|
+
// Subscribe to sidebar store changes
|
|
149
|
+
this.store.shouldShowFloatingButton.subscribe(() => this.updateFloatingButton());
|
|
150
|
+
this.store.isOpen.subscribe(() => this.updateFloatingButton());
|
|
134
151
|
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
// Initial update
|
|
153
|
+
this.updateFloatingButton();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
updateFloatingButton() {
|
|
157
|
+
const floatingBtn = this.floatingHideSidebarBtn;
|
|
158
|
+
const floatingIcon = this.floatingHideSidebarIcon;
|
|
159
|
+
const shouldShow = this.store.shouldShowFloatingButton.value;
|
|
137
160
|
|
|
138
|
-
if (
|
|
139
|
-
if (
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Update button icons and titles
|
|
146
|
-
if (this.hideSidebarIcon) {
|
|
147
|
-
this.hideSidebarIcon.setAttribute('icon', 'ph:sidebar-simple-fill');
|
|
148
|
-
}
|
|
149
|
-
if (this.floatingHideSidebarIcon) {
|
|
150
|
-
this.floatingHideSidebarIcon.setAttribute('icon', 'ph:sidebar-simple-fill');
|
|
151
|
-
}
|
|
152
|
-
if (this.hideSidebarBtn) {
|
|
153
|
-
this.hideSidebarBtn.setAttribute('title', 'Show Sidebar');
|
|
154
|
-
}
|
|
155
|
-
if (this.floatingHideSidebarBtn) {
|
|
156
|
-
this.floatingHideSidebarBtn.setAttribute('title', 'Show Sidebar');
|
|
161
|
+
if (floatingBtn) {
|
|
162
|
+
if (shouldShow) {
|
|
163
|
+
// Show floating button when sidebar is closed
|
|
164
|
+
floatingBtn.style.display = 'flex';
|
|
165
|
+
// Update icon and title
|
|
166
|
+
if (floatingIcon) {
|
|
167
|
+
floatingIcon.setAttribute('icon', 'ph:sidebar-simple-fill');
|
|
157
168
|
}
|
|
169
|
+
floatingBtn.setAttribute('title', 'Show Sidebar');
|
|
158
170
|
} else {
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
previewPane.classList.remove('w-full');
|
|
162
|
-
previewPane.classList.add('flex-1');
|
|
163
|
-
|
|
164
|
-
// Update button icons and titles
|
|
165
|
-
if (this.hideSidebarIcon) {
|
|
166
|
-
this.hideSidebarIcon.setAttribute('icon', 'ph:sidebar-simple');
|
|
167
|
-
}
|
|
168
|
-
if (this.floatingHideSidebarIcon) {
|
|
169
|
-
this.floatingHideSidebarIcon.setAttribute('icon', 'ph:sidebar-simple');
|
|
170
|
-
}
|
|
171
|
-
if (this.hideSidebarBtn) {
|
|
172
|
-
this.hideSidebarBtn.setAttribute('title', 'Hide Sidebar');
|
|
173
|
-
}
|
|
174
|
-
if (this.floatingHideSidebarBtn) {
|
|
175
|
-
this.floatingHideSidebarBtn.setAttribute('title', 'Hide Sidebar');
|
|
176
|
-
}
|
|
171
|
+
// Hide floating button when sidebar is open
|
|
172
|
+
floatingBtn.style.display = 'none';
|
|
177
173
|
}
|
|
178
174
|
}
|
|
179
175
|
}
|
|
180
176
|
|
|
177
|
+
toggleSidebarVisibility() {
|
|
178
|
+
// Use the store to toggle
|
|
179
|
+
this.store.toggle();
|
|
180
|
+
}
|
|
181
|
+
|
|
181
182
|
refreshIframe() {
|
|
182
183
|
if (this.iframe) {
|
|
183
184
|
console.log('Refreshing iframe...');
|
|
@@ -191,7 +192,13 @@ class SimpleLivePreviewManager {
|
|
|
191
192
|
loadUrl() {
|
|
192
193
|
if (this.urlInput && this.iframe) {
|
|
193
194
|
const path = this.urlInput.value.trim();
|
|
194
|
-
|
|
195
|
+
this.loadUrlFromPath(path);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
loadUrlFromPath(path) {
|
|
200
|
+
if (this.iframe) {
|
|
201
|
+
console.log('loadUrlFromPath called with path:', path);
|
|
195
202
|
|
|
196
203
|
// Check if it's an external URL (starts with http:// or https://)
|
|
197
204
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
@@ -201,8 +208,10 @@ class SimpleLivePreviewManager {
|
|
|
201
208
|
// Open external URLs in a new tab
|
|
202
209
|
console.log('Opening external URL in new tab:', path);
|
|
203
210
|
window.open(path, '_blank');
|
|
204
|
-
// Clear the input
|
|
205
|
-
this.urlInput
|
|
211
|
+
// Clear the input if it exists
|
|
212
|
+
if (this.urlInput) {
|
|
213
|
+
this.urlInput.value = '';
|
|
214
|
+
}
|
|
206
215
|
return;
|
|
207
216
|
}
|
|
208
217
|
}
|
|
@@ -211,9 +220,20 @@ class SimpleLivePreviewManager {
|
|
|
211
220
|
const newUrl = path ? baseUrl + '/' + path.replace(/^\//, '') : baseUrl;
|
|
212
221
|
console.log('Loading URL in iframe:', newUrl);
|
|
213
222
|
this.iframe.src = newUrl;
|
|
223
|
+
|
|
224
|
+
// Update input if it exists
|
|
225
|
+
if (this.urlInput) {
|
|
226
|
+
this.urlInput.value = path;
|
|
227
|
+
}
|
|
214
228
|
}
|
|
215
229
|
}
|
|
216
230
|
|
|
231
|
+
handleSidebarStateChange(state) {
|
|
232
|
+
// Legacy method for backward compatibility
|
|
233
|
+
console.log('Legacy sidebar state changed:', state);
|
|
234
|
+
// The floating button is now handled by updateFloatingButton()
|
|
235
|
+
}
|
|
236
|
+
|
|
217
237
|
handleIframeError() {
|
|
218
238
|
// Get the current iframe src and open it in a new tab if it's external
|
|
219
239
|
if (this.iframe && this.iframe.src) {
|
|
@@ -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
|
+
}
|