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.
Files changed (43) hide show
  1. package/dist/components/Sidebar.d.ts +8 -0
  2. package/dist/components/Sidebar.d.ts.map +1 -0
  3. package/dist/components/Sidebar.js +6 -0
  4. package/dist/components/Sidebar.js.map +1 -0
  5. package/dist/components/SimpleLivePreview.js +1 -1
  6. package/dist/components/SimpleLivePreview.js.map +1 -1
  7. package/dist/layouts/Layout.js +1 -1
  8. package/dist/layouts/Layout.js.map +1 -1
  9. package/dist/pages/Code.d.ts.map +1 -1
  10. package/dist/pages/Code.js +2 -2
  11. package/dist/pages/Code.js.map +1 -1
  12. package/dist/server.d.ts.map +1 -1
  13. package/dist/server.js +81 -11
  14. package/dist/server.js.map +1 -1
  15. package/dist/services/terminal.d.ts +25 -1
  16. package/dist/services/terminal.d.ts.map +1 -1
  17. package/dist/services/terminal.js +135 -6
  18. package/dist/services/terminal.js.map +1 -1
  19. package/dist/services/websocket.d.ts +45 -0
  20. package/dist/services/websocket.d.ts.map +1 -0
  21. package/dist/services/websocket.js +306 -0
  22. package/dist/services/websocket.js.map +1 -0
  23. package/dist/static/components/Sidebar.js +225 -0
  24. package/dist/static/components/SimpleLivePreview.js +73 -53
  25. package/dist/static/components/Terminal.js +141 -61
  26. package/dist/static/signals/SidebarSignal.js +123 -0
  27. package/dist/static/signals/TerminalSignal.js +137 -2
  28. package/dist/static/styles/main.css +111 -25
  29. package/package.json +6 -2
  30. package/src/components/Sidebar.tsx +92 -0
  31. package/src/components/SimpleLivePreview.tsx +4 -4
  32. package/src/layouts/Layout.tsx +1 -1
  33. package/src/pages/Code.tsx +18 -145
  34. package/src/server.tsx +97 -12
  35. package/src/services/terminal.ts +164 -6
  36. package/src/services/websocket.ts +374 -0
  37. package/src/static/components/Sidebar.js +225 -0
  38. package/src/static/components/SimpleLivePreview.js +73 -53
  39. package/src/static/components/Terminal.js +141 -61
  40. package/src/static/signals/SidebarSignal.js +123 -0
  41. package/src/static/signals/TerminalSignal.js +137 -2
  42. package/src/static/styles/main.css +111 -25
  43. package/tailwind.config.ts +10 -0
@@ -1,5 +1,5 @@
1
1
  import Layout from "../layouts/Layout.js";
2
- import { Terminal as TerminalComponent } from "../components/Terminal.js";
2
+ import { Sidebar } from "../components/Sidebar.js";
3
3
  import { SimpleLivePreview } from "../components/SimpleLivePreview.js";
4
4
 
5
5
  interface TerminalProps {
@@ -10,152 +10,25 @@ interface TerminalProps {
10
10
  export function Code({ previewPort = 1234, workingDirectory }: TerminalProps) {
11
11
  return (
12
12
  <Layout title="Code Editor">
13
- <div id="code-container" class="h-screen flex bg-[#1e1e1e]">
14
- {/* Left Pane - Tabbed Sidebar */}
15
- <div id="sidebar-pane" class="w-2/5-plus border-r border-[#3c3c3c] transition-all duration-300 flex flex-col bg-[#252526]">
16
- {/* Preview Controls */}
17
- <div class="p-3 border-b border-[#3c3c3c] bg-[#2d2d30]">
18
- <div class="flex items-center gap-2">
19
- <a
20
- href="/"
21
- class="p-2 hover:bg-[#3c3c3c] rounded-md transition-colors flex-shrink-0 text-[#cccccc] hover:text-white"
22
- title="Back to Home"
23
- >
24
- <iconify-icon icon="tabler:arrow-left" width="16" height="16"></iconify-icon>
25
- </a>
26
- <button
27
- type="button"
28
- id="live-preview-refresh-btn"
29
- class="p-2 hover:bg-[#3c3c3c] rounded-md transition-colors flex-shrink-0 text-[#cccccc] hover:text-white"
30
- title="Reload"
31
- >
32
- <iconify-icon icon="tabler:refresh" width="16" height="16"></iconify-icon>
33
- </button>
34
- <div class="flex-1 flex items-center bg-[#1e1e1e] border border-[#3c3c3c] rounded px-3 py-2 hover:border-[#0e639c] focus-within:border-[#0e639c] transition-all">
35
- <iconify-icon icon="tabler:world" width="16" height="16" class="text-[#cccccc] mr-2"></iconify-icon>
36
- <span class="text-[#cccccc] text-sm">localhost:{previewPort}/</span>
37
- <input
38
- id="live-preview-url-input"
39
- type="text"
40
- placeholder="path"
41
- defaultValue=""
42
- class="flex-1 bg-transparent text-sm focus:outline-none text-[#cccccc] ml-1"
43
- />
44
- <button
45
- type="button"
46
- id="live-preview-load-btn"
47
- class="ml-2 p-1 hover:bg-[#3c3c3c] rounded transition-colors flex-shrink-0 text-[#cccccc] hover:text-white"
48
- title="Go"
49
- >
50
- <iconify-icon icon="tabler:chevron-right" width="16" height="16"></iconify-icon>
51
- </button>
52
- </div>
53
- </div>
54
- </div>
55
-
56
- {/* Tab Headers */}
57
- <div class="flex py-2">
58
- <button class="px-3 py-2 text-[#cccccc] disabled:opacity-50 cursor-not-allowed flex items-center justify-center">
59
- <iconify-icon icon="tabler:folder" width="20" height="20"></iconify-icon>
60
- </button>
61
- <button class="px-3 py-2 text-[#cccccc] disabled:opacity-50 cursor-not-allowed flex items-center justify-center">
62
- <iconify-icon icon="tabler:search" width="20" height="20"></iconify-icon>
63
- </button>
64
- <button class="px-3 py-2 text-white flex items-center justify-center">
65
- <iconify-icon icon="tabler:terminal" width="20" height="20"></iconify-icon>
66
- </button>
67
- <button class="px-3 py-2 text-[#cccccc] disabled:opacity-50 cursor-not-allowed flex items-center justify-center">
68
- <iconify-icon icon="tabler:git-merge" width="20" height="20"></iconify-icon>
69
- </button>
70
- </div>
71
-
72
- {/* Tab Content */}
73
- <div class="flex-1 overflow-hidden bg-[#1e1e1e]">
74
- {/* Terminal Tab Content (Active) */}
75
- <div class="h-full flex flex-col">
76
-
77
- {/* Terminal Tabs Bar */}
78
- <div class="border-b border-[#3c3c3c] bg-[#252526] px-3 py-1">
79
- <div class="flex items-center gap-1">
80
- {/* Terminal Tab 1 */}
81
- <button
82
- id="terminal-tab-1"
83
- class="terminal-tab flex items-center px-3 py-1 text-sm text-white bg-[#1e1e1e] border-t-2 border-[#0e639c] rounded-t-sm hover:bg-[#2d2d30] transition-colors"
84
- data-terminal-index="1"
85
- >
86
- <iconify-icon icon="tabler:terminal-2" width="14" height="14" class="mr-1"></iconify-icon>
87
- Terminal 1
88
- <button class="ml-2 hover:bg-[#3c3c3c] rounded p-0.5 text-[#cccccc] hover:text-white terminal-close-btn" data-terminal-index="1" style="display: none;">
89
- <iconify-icon icon="tabler:x" width="12" height="12"></iconify-icon>
90
- </button>
91
- </button>
92
-
93
- {/* Terminal Tab 2 */}
94
- <button
95
- id="terminal-tab-2"
96
- class="terminal-tab flex items-center px-3 py-1 text-sm text-[#cccccc] hover:text-white hover:bg-[#2d2d30] transition-colors rounded-t-sm"
97
- data-terminal-index="2"
98
- style="display: none;"
99
- >
100
- <iconify-icon icon="tabler:terminal-2" width="14" height="14" class="mr-1"></iconify-icon>
101
- Terminal 2
102
- <button class="ml-2 hover:bg-[#3c3c3c] rounded p-0.5 text-[#cccccc] hover:text-white terminal-close-btn" data-terminal-index="2">
103
- <iconify-icon icon="tabler:x" width="12" height="12"></iconify-icon>
104
- </button>
105
- </button>
106
-
107
- {/* Terminal Tab 3 */}
108
- <button
109
- id="terminal-tab-3"
110
- class="terminal-tab flex items-center px-3 py-1 text-sm text-[#cccccc] hover:text-white hover:bg-[#2d2d30] transition-colors rounded-t-sm"
111
- data-terminal-index="3"
112
- style="display: none;"
113
- >
114
- <iconify-icon icon="tabler:terminal-2" width="14" height="14" class="mr-1"></iconify-icon>
115
- Terminal 3
116
- <button class="ml-2 hover:bg-[#3c3c3c] rounded p-0.5 text-[#cccccc] hover:text-white terminal-close-btn" data-terminal-index="3">
117
- <iconify-icon icon="tabler:x" width="12" height="12"></iconify-icon>
118
- </button>
119
- </button>
120
-
121
- {/* Add Terminal Button */}
122
- <button
123
- id="add-terminal-btn"
124
- class="flex items-center px-2 py-1 text-sm text-[#cccccc] hover:text-white hover:bg-[#2d2d30] transition-colors rounded"
125
- title="New Terminal"
126
- >
127
- <iconify-icon icon="tabler:plus" width="14" height="14"></iconify-icon>
128
- </button>
129
- </div>
130
- </div>
131
-
132
- {/* Terminal Content Area */}
133
- <div class="flex-1 overflow-hidden relative">
134
- {/* Terminal 1 Container */}
135
- <div id="terminal-container-1" class="h-full p-4 terminal-container" data-terminal-index="1">
136
- <TerminalComponent index={1} />
137
- </div>
138
-
139
- {/* Terminal 2 Container */}
140
- <div id="terminal-container-2" class="h-full p-4 terminal-container" data-terminal-index="2" style="display: none;">
141
- <TerminalComponent index={2} />
142
- </div>
143
-
144
- {/* Terminal 3 Container */}
145
- <div id="terminal-container-3" class="h-full p-4 terminal-container" data-terminal-index="3" style="display: none;">
146
- <TerminalComponent index={3} />
147
- </div>
148
- </div>
149
- </div>
150
- </div>
151
- </div>
13
+ <div id="code-container" class="h-screen flex bg-[#1e1e1e] relative">
14
+ {/* Mobile toggle button */}
15
+ <button
16
+ type="button"
17
+ id="mobile-sidebar-toggle"
18
+ class="fixed top-4 left-4 z-60 p-3 bg-[#2d2d30] border border-[#3c3c3c] rounded-lg shadow-xl hover:bg-[#3c3c3c] transition-all md:hidden"
19
+ title="Toggle Sidebar"
20
+ >
21
+ <iconify-icon icon="tabler:menu-2" width="20" height="20" class="text-[#cccccc]"></iconify-icon>
22
+ </button>
23
+
24
+ {/* Sidebar */}
25
+ <Sidebar id="sidebar" previewPort={previewPort} workingDirectory={workingDirectory} />
152
26
 
153
- {/* Right Pane - Live Preview */}
154
- <SimpleLivePreview id="live-preview" previewPort={previewPort} />
27
+ {/* Main Content - Live Preview */}
28
+ <div class="flex-1 md:flex-1">
29
+ <SimpleLivePreview id="live-preview" previewPort={previewPort} />
30
+ </div>
155
31
  </div>
156
-
157
- {/* Terminal Tabs Management Script */}
158
- <script type="module" src="/components/TerminalTabs.js"></script>
159
32
  </Layout>
160
33
  );
161
34
  }
package/src/server.tsx CHANGED
@@ -6,9 +6,11 @@ import { Welcome } from "./pages/Welcome.js";
6
6
  import { Code } from "./pages/Code.js";
7
7
  import { DevServerManager } from "./services/dev-server.js";
8
8
  import { TerminalService } from "./services/terminal.js";
9
+ import { WebSocketTerminalService } from "./services/websocket.js";
9
10
  import * as path from 'node:path';
10
11
  import process from "node:process";
11
12
  import { fileURLToPath } from 'node:url';
13
+ import type { Server } from 'http';
12
14
 
13
15
  export interface TreesapConfig {
14
16
  port?: number;
@@ -144,15 +146,90 @@ export async function startServer(config: TreesapConfig & { autoStartDev?: boole
144
146
  return c.json({ logs });
145
147
  });
146
148
 
147
- // List active terminal sessions
149
+ // List active terminal sessions with WebSocket client info
148
150
  app.get("/api/terminal/sessions", (c: Context) => {
149
151
  const sessions = TerminalService.getAllSessions();
152
+ const wsActiveSessions = WebSocketTerminalService.getActiveSessions();
153
+
154
+ return c.json({
155
+ sessions: sessions.map(session => {
156
+ const wsInfo = wsActiveSessions.find(ws => ws.sessionId === session.id);
157
+ return {
158
+ id: session.id,
159
+ createdAt: session.createdAt,
160
+ lastActivity: session.lastActivity,
161
+ connectedClients: wsInfo ? wsInfo.clientCount : 0
162
+ };
163
+ }),
164
+ totalConnectedClients: WebSocketTerminalService.getConnectedClients()
165
+ });
166
+ });
167
+
168
+ // Get specific terminal session status
169
+ app.get("/api/terminal/sessions/:sessionId/status", (c: Context) => {
170
+ const sessionId = c.req.param('sessionId');
171
+ const session = TerminalService.getSession(sessionId);
172
+
173
+ if (!session) {
174
+ return c.json({ error: "Session not found" }, 404);
175
+ }
176
+
177
+ const clients = WebSocketTerminalService.getSessionClients(sessionId);
178
+
179
+ return c.json({
180
+ id: session.id,
181
+ createdAt: session.createdAt,
182
+ lastActivity: session.lastActivity,
183
+ connectedClients: clients.length,
184
+ clientIds: clients
185
+ });
186
+ });
187
+
188
+ // Send command to terminal via API
189
+ app.post("/api/terminal/sessions/:sessionId/command", async (c: Context) => {
190
+ const sessionId = c.req.param('sessionId');
191
+ const body = await c.req.json();
192
+ const { command } = body;
193
+
194
+ if (!command) {
195
+ return c.json({ error: "Command is required" }, 400);
196
+ }
197
+
198
+ // Get or create terminal session
199
+ let session = TerminalService.getSession(sessionId);
200
+ if (!session) {
201
+ session = TerminalService.createSession(sessionId);
202
+ }
203
+
204
+ const success = WebSocketTerminalService.sendCommandToSession(sessionId, command);
205
+
206
+ if (success) {
207
+ return c.json({
208
+ success: true,
209
+ message: `Command sent to session ${sessionId}`,
210
+ connectedClients: WebSocketTerminalService.getSessionClients(sessionId).length
211
+ });
212
+ } else {
213
+ return c.json({ error: "Failed to send command to terminal" }, 500);
214
+ }
215
+ });
216
+
217
+ // Get recent output from terminal session (for API clients)
218
+ app.get("/api/terminal/sessions/:sessionId/output", async (c: Context) => {
219
+ const sessionId = c.req.param('sessionId');
220
+ const session = TerminalService.getSession(sessionId);
221
+
222
+ if (!session) {
223
+ return c.json({ error: "Session not found" }, 404);
224
+ }
225
+
226
+ // Note: This is a basic implementation. For production, you'd want to
227
+ // store recent output history in the TerminalService
150
228
  return c.json({
151
- sessions: sessions.map(session => ({
152
- id: session.id,
153
- createdAt: session.createdAt,
154
- lastActivity: session.lastActivity
155
- }))
229
+ sessionId,
230
+ message: "Output streaming available via WebSocket connection",
231
+ connectedClients: WebSocketTerminalService.getSessionClients(sessionId).length,
232
+ websocketUrl: `ws://${c.req.header('host')}/terminal/ws`
156
233
  });
157
234
  });
158
235
 
@@ -371,11 +448,23 @@ export async function startServer(config: TreesapConfig & { autoStartDev?: boole
371
448
 
372
449
  const { serve } = await import('@hono/node-server');
373
450
 
451
+ // Start the server and initialize WebSocket
452
+ const server = serve({
453
+ fetch: app.fetch,
454
+ port,
455
+ }) as Server;
456
+
457
+ // Initialize WebSocket service
458
+ WebSocketTerminalService.initialize(server);
459
+
374
460
  // Setup global graceful shutdown for all subprocess managers
375
461
  const setupGlobalShutdown = () => {
376
462
  const cleanup = async () => {
377
463
  console.log('\n🛑 Shutting down server and all subprocesses...');
378
464
 
465
+ // Clean up WebSocket connections
466
+ WebSocketTerminalService.cleanup();
467
+
379
468
  // Stop Claude Code process if running
380
469
  if (claudeCodeManager) {
381
470
  try {
@@ -425,10 +514,6 @@ export async function startServer(config: TreesapConfig & { autoStartDev?: boole
425
514
  // Initialize terminal service cleanup
426
515
  TerminalService.setupGlobalCleanup();
427
516
 
428
- serve({
429
- fetch: app.fetch,
430
- port,
431
- });
432
-
433
- console.log(`\n🌳 Treesap server running at http://localhost:${port}\n`);
517
+ console.log(`\n🌳 Treesap server running at http://localhost:${port}`);
518
+ console.log(`🔌 WebSocket terminal server available at ws://localhost:${port}/terminal/ws\n`);
434
519
  }
@@ -1,6 +1,9 @@
1
1
  import { spawn, ChildProcess } from 'child_process';
2
2
  import { EventEmitter } from 'events';
3
3
  import * as pty from 'node-pty';
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+ import * as os from 'node:os';
4
7
 
5
8
  export interface TerminalSession {
6
9
  id: string;
@@ -8,13 +11,29 @@ export interface TerminalSession {
8
11
  eventEmitter: EventEmitter;
9
12
  createdAt: Date;
10
13
  lastActivity: Date;
14
+ cwd?: string;
15
+ env?: Record<string, string>;
16
+ cols?: number;
17
+ rows?: number;
18
+ }
19
+
20
+ export interface PersistedSessionData {
21
+ id: string;
22
+ createdAt: string;
23
+ lastActivity: string;
24
+ cwd: string;
25
+ env: Record<string, string>;
26
+ cols: number;
27
+ rows: number;
11
28
  }
12
29
 
13
30
  export class TerminalService {
14
31
  private static sessions = new Map<string, TerminalSession>();
15
32
  private static readonly SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
33
+ private static readonly PERSISTENCE_DIR = path.join(os.tmpdir(), '.treesap-terminals');
34
+ private static readonly SESSIONS_FILE = path.join(this.PERSISTENCE_DIR, 'sessions.json');
16
35
 
17
- static createSession(sessionId: string): TerminalSession {
36
+ static createSession(sessionId: string, options?: { cwd?: string; cols?: number; rows?: number }): TerminalSession {
18
37
  // Clean up any existing session with the same ID
19
38
  this.destroySession(sessionId);
20
39
 
@@ -22,13 +41,26 @@ export class TerminalService {
22
41
  // Increase max listeners to handle multiple terminal tabs and connections
23
42
  eventEmitter.setMaxListeners(20);
24
43
 
44
+ // Use provided options or defaults
45
+ const cwd = options?.cwd || process.cwd();
46
+ const cols = options?.cols || 80;
47
+ const rows = options?.rows || 24;
48
+ const env: Record<string, string> = {};
49
+
50
+ // Filter out undefined values from process.env
51
+ for (const [key, value] of Object.entries(process.env)) {
52
+ if (value !== undefined) {
53
+ env[key] = value;
54
+ }
55
+ }
56
+
25
57
  // Create a PTY process for proper terminal behavior
26
58
  const ptyProcess = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : process.env.SHELL || '/bin/bash', [], {
27
59
  name: 'xterm-256color',
28
- cols: 80,
29
- rows: 24,
30
- cwd: process.cwd(),
31
- env: process.env
60
+ cols,
61
+ rows,
62
+ cwd,
63
+ env
32
64
  });
33
65
 
34
66
  const session: TerminalSession = {
@@ -36,7 +68,11 @@ export class TerminalService {
36
68
  process: ptyProcess,
37
69
  eventEmitter,
38
70
  createdAt: new Date(),
39
- lastActivity: new Date()
71
+ lastActivity: new Date(),
72
+ cwd,
73
+ env,
74
+ cols,
75
+ rows
40
76
  };
41
77
 
42
78
  // Handle process output
@@ -60,6 +96,9 @@ export class TerminalService {
60
96
 
61
97
  // Set up session cleanup
62
98
  this.scheduleSessionCleanup(sessionId);
99
+
100
+ // Persist session data
101
+ this.persistSessionData(session);
63
102
 
64
103
  return session;
65
104
  }
@@ -100,6 +139,9 @@ export class TerminalService {
100
139
  // Remove from sessions map
101
140
  this.sessions.delete(sessionId);
102
141
 
142
+ // Remove from persistent storage
143
+ this.removePersistedSession(sessionId);
144
+
103
145
  return true;
104
146
  } catch (error) {
105
147
  console.error(`Error destroying session ${sessionId}:`, error);
@@ -144,6 +186,9 @@ export class TerminalService {
144
186
  }
145
187
 
146
188
  static setupGlobalCleanup(): void {
189
+ // Load persisted sessions on startup
190
+ this.loadPersistedSessions();
191
+
147
192
  // Cleanup all sessions on process exit
148
193
  const cleanup = () => {
149
194
  console.log('Cleaning up all terminal sessions...');
@@ -164,4 +209,117 @@ export class TerminalService {
164
209
  }
165
210
  }, 5 * 60 * 1000); // Every 5 minutes
166
211
  }
212
+
213
+ // Persistence methods
214
+ private static ensurePersistenceDir(): void {
215
+ try {
216
+ if (!fs.existsSync(this.PERSISTENCE_DIR)) {
217
+ fs.mkdirSync(this.PERSISTENCE_DIR, { recursive: true });
218
+ }
219
+ } catch (error) {
220
+ console.error('Error creating persistence directory:', error);
221
+ }
222
+ }
223
+
224
+ private static persistSessionData(session: TerminalSession): void {
225
+ try {
226
+ this.ensurePersistenceDir();
227
+
228
+ const sessionData: PersistedSessionData = {
229
+ id: session.id,
230
+ createdAt: session.createdAt.toISOString(),
231
+ lastActivity: session.lastActivity.toISOString(),
232
+ cwd: session.cwd || process.cwd(),
233
+ env: session.env || (() => {
234
+ const env: Record<string, string> = {};
235
+ for (const [key, value] of Object.entries(process.env)) {
236
+ if (value !== undefined) {
237
+ env[key] = value;
238
+ }
239
+ }
240
+ return env;
241
+ })(),
242
+ cols: session.cols || 80,
243
+ rows: session.rows || 24
244
+ };
245
+
246
+ let existingData: PersistedSessionData[] = [];
247
+ if (fs.existsSync(this.SESSIONS_FILE)) {
248
+ const content = fs.readFileSync(this.SESSIONS_FILE, 'utf8');
249
+ if (content.trim()) {
250
+ existingData = JSON.parse(content);
251
+ }
252
+ }
253
+
254
+ // Remove existing session data if present
255
+ existingData = existingData.filter(s => s.id !== session.id);
256
+
257
+ // Add new session data
258
+ existingData.push(sessionData);
259
+
260
+ fs.writeFileSync(this.SESSIONS_FILE, JSON.stringify(existingData, null, 2));
261
+ } catch (error) {
262
+ console.error('Error persisting session data:', error);
263
+ }
264
+ }
265
+
266
+ private static removePersistedSession(sessionId: string): void {
267
+ try {
268
+ if (!fs.existsSync(this.SESSIONS_FILE)) return;
269
+
270
+ const content = fs.readFileSync(this.SESSIONS_FILE, 'utf8');
271
+ if (!content.trim()) return;
272
+
273
+ let existingData: PersistedSessionData[] = JSON.parse(content);
274
+ existingData = existingData.filter(s => s.id !== sessionId);
275
+
276
+ fs.writeFileSync(this.SESSIONS_FILE, JSON.stringify(existingData, null, 2));
277
+ } catch (error) {
278
+ console.error('Error removing persisted session:', error);
279
+ }
280
+ }
281
+
282
+ private static loadPersistedSessions(): void {
283
+ try {
284
+ if (!fs.existsSync(this.SESSIONS_FILE)) return;
285
+
286
+ const content = fs.readFileSync(this.SESSIONS_FILE, 'utf8');
287
+ if (!content.trim()) return;
288
+
289
+ const persistedSessions: PersistedSessionData[] = JSON.parse(content);
290
+
291
+ console.log(`Found ${persistedSessions.length} persisted terminal session(s)`);
292
+
293
+ for (const sessionData of persistedSessions) {
294
+ // Check if session is not too old
295
+ const lastActivity = new Date(sessionData.lastActivity);
296
+ const timeSinceLastActivity = Date.now() - lastActivity.getTime();
297
+
298
+ if (timeSinceLastActivity < this.SESSION_TIMEOUT) {
299
+ console.log(`Restoring terminal session: ${sessionData.id}`);
300
+
301
+ // Create new session with the persisted options
302
+ this.createSession(sessionData.id, {
303
+ cwd: sessionData.cwd,
304
+ cols: sessionData.cols,
305
+ rows: sessionData.rows
306
+ });
307
+ } else {
308
+ console.log(`Skipping expired session: ${sessionData.id}`);
309
+ this.removePersistedSession(sessionData.id);
310
+ }
311
+ }
312
+ } catch (error) {
313
+ console.error('Error loading persisted sessions:', error);
314
+ }
315
+ }
316
+
317
+ // Update session activity and persist
318
+ static updateSessionActivity(sessionId: string): void {
319
+ const session = this.getSession(sessionId);
320
+ if (session) {
321
+ session.lastActivity = new Date();
322
+ this.persistSessionData(session);
323
+ }
324
+ }
167
325
  }