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.
Files changed (37) hide show
  1. package/dist/components/ChatInput.d.ts +7 -0
  2. package/dist/components/ChatInput.d.ts.map +1 -0
  3. package/dist/components/ChatInput.js +11 -0
  4. package/dist/components/ChatInput.js.map +1 -0
  5. package/dist/components/Sidebar.d.ts +8 -0
  6. package/dist/components/Sidebar.d.ts.map +1 -0
  7. package/dist/components/Sidebar.js +7 -0
  8. package/dist/components/Sidebar.js.map +1 -0
  9. package/dist/components/SimpleLivePreview.js +1 -1
  10. package/dist/components/SimpleLivePreview.js.map +1 -1
  11. package/dist/pages/Code.d.ts.map +1 -1
  12. package/dist/pages/Code.js +2 -2
  13. package/dist/pages/Code.js.map +1 -1
  14. package/dist/services/websocket.d.ts.map +1 -1
  15. package/dist/services/websocket.js +1 -2
  16. package/dist/services/websocket.js.map +1 -1
  17. package/dist/static/components/ChatInput.js +237 -0
  18. package/dist/static/components/Sidebar.js +225 -0
  19. package/dist/static/components/SimpleLivePreview.js +73 -53
  20. package/dist/static/components/Terminal.js +143 -61
  21. package/dist/static/signals/SidebarSignal.js +123 -0
  22. package/dist/static/signals/TerminalSignal.js +137 -2
  23. package/dist/static/styles/main.css +180 -0
  24. package/package.json +1 -1
  25. package/src/components/ChatInput.tsx +56 -0
  26. package/src/components/Sidebar.tsx +99 -0
  27. package/src/components/SimpleLivePreview.tsx +4 -4
  28. package/src/pages/Code.tsx +18 -55
  29. package/src/services/websocket.ts +1 -5
  30. package/src/static/components/ChatInput.js +237 -0
  31. package/src/static/components/Sidebar.js +225 -0
  32. package/src/static/components/SimpleLivePreview.js +73 -53
  33. package/src/static/components/Terminal.js +143 -61
  34. package/src/static/signals/SidebarSignal.js +123 -0
  35. package/src/static/signals/TerminalSignal.js +137 -2
  36. package/src/static/styles/main.css +180 -0
  37. 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
- this.eventSource = null;
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 stream
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 directly to shell stdin
140
- fetch(`/terminal/input/${this.sessionId}`, {
141
- method: 'POST',
142
- headers: {
143
- 'Content-Type': 'application/json',
144
- },
145
- body: JSON.stringify({ input: data })
146
- })
147
- .catch(error => {
148
- console.error('Error sending input:', error);
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.eventSource) {
154
- this.eventSource.close();
163
+ if (this.websocket) {
164
+ this.websocket.close();
155
165
  }
156
166
 
157
167
  this.updateStatus('Connecting...');
158
168
 
159
- this.eventSource = new EventSource(`/terminal/stream/${this.sessionId}`);
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
- this.eventSource.onopen = () => {
162
- this.updateStatus('Ready');
163
- terminalStore.updateTerminalStatus(this.terminalId, 'connected');
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.eventSource.onmessage = (event) => {
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
- if (data.type === 'output') {
171
- this.terminal.write(data.content);
172
- } else if (data.type === 'error') {
173
- this.terminal.write(`\x1b[31m${data.content}\x1b[0m`);
174
- } else if (data.type === 'exit') {
175
- this.terminal.writeln(`\x1b[90mProcess exited with code ${data.code}\x1b[0m`);
176
- this.terminal.write('\x1b[32m$ \x1b[0m');
177
- } else if (data.type === 'connected') {
178
- // Terminal connected - shell will show its own prompt
179
- console.log('Terminal connected');
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 terminal data:', error);
256
+ console.error('Error parsing WebSocket message:', error);
183
257
  }
184
258
  };
185
259
 
186
- this.eventSource.onerror = (error) => {
187
- console.error('Terminal stream error:', 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 after a delay
192
- setTimeout(() => {
193
- if (this.eventSource.readyState === EventSource.CLOSED) {
194
- terminalStore.updateTerminalStatus(this.terminalId, 'connecting');
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
- }, 3000);
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
- if (this.eventSource) {
217
- this.eventSource.close();
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
- // Destroy server-side terminal session
224
- const sessionId = terminalStore.getSessionId(this.terminalId);
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) {