@epiphytic/claudecodeui 1.0.1 โ†’ 1.2.0

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/server/index.js CHANGED
@@ -75,6 +75,17 @@ import {
75
75
  getActiveClaudeSDKSessions,
76
76
  resolveToolApproval,
77
77
  } from "./claude-sdk.js";
78
+ import { sessionLock } from "./session-lock.js";
79
+ import {
80
+ checkTmuxAvailable,
81
+ createTmuxSession,
82
+ attachToTmuxSession,
83
+ killSession as killTmuxSession,
84
+ generateTmuxSessionName,
85
+ sessionExists as tmuxSessionExists,
86
+ resizeSession as resizeTmuxSession,
87
+ } from "./tmux-manager.js";
88
+ import { detectExternalClaude } from "./external-session-detector.js";
78
89
  import {
79
90
  spawnCursor,
80
91
  abortCursorSession,
@@ -100,7 +111,10 @@ import projectsRoutes from "./routes/projects.js";
100
111
  import cliAuthRoutes from "./routes/cli-auth.js";
101
112
  import userRoutes from "./routes/user.js";
102
113
  import codexRoutes from "./routes/codex.js";
103
- import { initializeDatabase } from "./database/db.js";
114
+ import sessionsRoutes from "./routes/sessions.js";
115
+ import { updateSessionsCache } from "./sessions-cache.js";
116
+ import { updateProjectsCache } from "./projects-cache.js";
117
+ import { initializeDatabase, tmuxSessionsDb } from "./database/db.js";
104
118
  import {
105
119
  validateApiKey,
106
120
  authenticateToken,
@@ -116,6 +130,37 @@ const connectedClients = new Set();
116
130
  let orchestrator = null;
117
131
  let orchestratorStatusHooks = null;
118
132
 
133
+ /**
134
+ * Clean up stale tmux session entries from the database
135
+ * Called when projects are rescanned to remove entries for tmux sessions that no longer exist
136
+ */
137
+ function cleanupStaleTmuxSessions() {
138
+ try {
139
+ const allSessions = tmuxSessionsDb.getAllTmuxSessions();
140
+ const staleIds = [];
141
+
142
+ for (const session of allSessions) {
143
+ // Check if the tmux session still exists
144
+ if (!tmuxSessionExists(session.tmux_session_name)) {
145
+ console.log(
146
+ `๐Ÿงน Cleaning up stale tmux entry: ${session.tmux_session_name}`,
147
+ );
148
+ staleIds.push(session.id);
149
+ }
150
+ }
151
+
152
+ if (staleIds.length > 0) {
153
+ const deleted = tmuxSessionsDb.deleteByIds(staleIds);
154
+ console.log(`๐Ÿงน Cleaned up ${deleted} stale tmux session entries`);
155
+ }
156
+ } catch (error) {
157
+ console.error(
158
+ "[ERROR] Error cleaning up stale tmux sessions:",
159
+ error.message,
160
+ );
161
+ }
162
+ }
163
+
119
164
  // Setup file system watcher for Claude projects folder using chokidar
120
165
  async function setupProjectsWatcher() {
121
166
  const chokidar = (await import("chokidar")).default;
@@ -159,6 +204,13 @@ async function setupProjectsWatcher() {
159
204
  // Get updated projects list
160
205
  const updatedProjects = await getProjects();
161
206
 
207
+ // Update sessions and projects caches with new projects data
208
+ updateSessionsCache(updatedProjects);
209
+ updateProjectsCache(updatedProjects);
210
+
211
+ // Clean up stale tmux session entries from database
212
+ cleanupStaleTmuxSessions();
213
+
162
214
  // Notify all connected clients about the project changes
163
215
  const updateMessage = JSON.stringify({
164
216
  type: "projects_updated",
@@ -198,9 +250,89 @@ async function setupProjectsWatcher() {
198
250
  const app = express();
199
251
  const server = http.createServer(app);
200
252
 
253
+ /**
254
+ * PTY Sessions Map - Enhanced for multi-client support
255
+ *
256
+ * Each entry: {
257
+ * pty: PTYProcess, // node-pty or tmux attached process
258
+ * clients: Set<WebSocket>, // Connected WebSocket clients
259
+ * buffer: string[], // Output buffer (max 5000 items)
260
+ * mode: 'shell' | 'chat', // Current session mode
261
+ * lockedBy: string | null, // Chat lock holder (clientId)
262
+ * tmuxSessionName: string|null, // tmux session name if using tmux
263
+ * projectPath: string,
264
+ * sessionId: string,
265
+ * timeoutId: NodeJS.Timeout|null,
266
+ * createdAt: number,
267
+ * lastActivity: number,
268
+ * }
269
+ */
201
270
  const ptySessionsMap = new Map();
202
271
  const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
203
272
 
273
+ // Check if tmux is available on startup
274
+ const tmuxStatus = checkTmuxAvailable();
275
+ if (tmuxStatus.available) {
276
+ console.log(`[INFO] tmux available: ${tmuxStatus.version}`);
277
+ } else {
278
+ console.log(`[WARN] tmux not available: ${tmuxStatus.error}`);
279
+ console.log("[INFO] Multi-client shell sharing will be limited");
280
+ }
281
+
282
+ /**
283
+ * Broadcast a message to all connected clients for a session
284
+ * @param {string} sessionKey - The session key
285
+ * @param {object} message - The message object to send
286
+ */
287
+ function broadcastToSession(sessionKey, message) {
288
+ const session = ptySessionsMap.get(sessionKey);
289
+ if (!session || !session.clients) return;
290
+
291
+ const messageStr =
292
+ typeof message === "string" ? message : JSON.stringify(message);
293
+
294
+ for (const client of session.clients) {
295
+ if (client.readyState === WebSocket.OPEN) {
296
+ try {
297
+ client.send(messageStr);
298
+ } catch (err) {
299
+ console.error("[WARN] Failed to send to client:", err.message);
300
+ }
301
+ }
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Broadcast session state update to all clients
307
+ * @param {string} sessionKey - The session key
308
+ */
309
+ function broadcastSessionState(sessionKey) {
310
+ const session = ptySessionsMap.get(sessionKey);
311
+ if (!session) return;
312
+
313
+ broadcastToSession(sessionKey, {
314
+ type: "session-state-update",
315
+ sessionKey,
316
+ state: {
317
+ mode: session.mode || "shell",
318
+ clientCount: session.clients ? session.clients.size : 0,
319
+ lockedBy: session.lockedBy,
320
+ tmuxSessionName: session.tmuxSessionName,
321
+ },
322
+ });
323
+ }
324
+
325
+ /**
326
+ * Get the session key for a project/session combination
327
+ * @param {string} projectPath
328
+ * @param {string} sessionId
329
+ * @param {string} commandSuffix - Optional command suffix for unique commands
330
+ * @returns {string}
331
+ */
332
+ function getPtySessionKey(projectPath, sessionId, commandSuffix = "") {
333
+ return `${projectPath}_${sessionId || "default"}${commandSuffix}`;
334
+ }
335
+
204
336
  // Single WebSocket server that handles both paths
205
337
  const wss = new WebSocketServer({
206
338
  server,
@@ -309,16 +441,42 @@ app.use("/api/user", authenticateToken, userRoutes);
309
441
  // Codex API Routes (protected)
310
442
  app.use("/api/codex", authenticateToken, codexRoutes);
311
443
 
444
+ // Sessions API Routes (protected)
445
+ app.use("/api/sessions", authenticateToken, sessionsRoutes);
446
+
312
447
  // Agent API Routes (uses API key authentication)
313
448
  app.use("/api/agent", agentRoutes);
314
449
 
315
- // Serve public files (like api-docs.html)
316
- app.use(express.static(path.join(__dirname, "../public")));
450
+ // Serve public files (like api-docs.html, icons)
451
+ // Enable ETag generation for conditional requests (304 support)
452
+ app.use(
453
+ express.static(path.join(__dirname, "../public"), {
454
+ etag: true,
455
+ lastModified: true,
456
+ setHeaders: (res, filePath) => {
457
+ // Cache icons and other static assets
458
+ if (filePath.match(/\.(svg|png|jpg|jpeg|gif|ico|woff2?|ttf|eot)$/)) {
459
+ // Cache for 1 week, allow revalidation
460
+ res.setHeader(
461
+ "Cache-Control",
462
+ "public, max-age=604800, must-revalidate",
463
+ );
464
+ } else if (filePath.endsWith(".html")) {
465
+ // HTML files should not be cached
466
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
467
+ res.setHeader("Pragma", "no-cache");
468
+ res.setHeader("Expires", "0");
469
+ }
470
+ },
471
+ }),
472
+ );
317
473
 
318
474
  // Static files served after API routes
319
475
  // Add cache control: HTML files should not be cached, but assets can be cached
320
476
  app.use(
321
477
  express.static(path.join(__dirname, "../dist"), {
478
+ etag: true,
479
+ lastModified: true,
322
480
  setHeaders: (res, filePath) => {
323
481
  if (filePath.endsWith(".html")) {
324
482
  // Prevent HTML caching to avoid service worker issues after builds
@@ -431,7 +589,7 @@ app.get(
431
589
  },
432
590
  );
433
591
 
434
- // Get messages for a specific session
592
+ // Get messages for a specific session with ETag caching support
435
593
  app.get(
436
594
  "/api/projects/:projectName/sessions/:sessionId/messages",
437
595
  authenticateToken,
@@ -451,6 +609,27 @@ app.get(
451
609
  parsedOffset,
452
610
  );
453
611
 
612
+ // Generate ETag based on message count and last timestamp
613
+ const messages = Array.isArray(result) ? result : result.messages || [];
614
+ const total = Array.isArray(result) ? messages.length : result.total || 0;
615
+ const lastTimestamp =
616
+ messages.length > 0
617
+ ? messages[messages.length - 1]?.timestamp || ""
618
+ : "";
619
+ const currentETag = `"${sessionId}-${total}-${Buffer.from(lastTimestamp).toString("base64").slice(0, 16)}"`;
620
+
621
+ // Check If-None-Match header for conditional request
622
+ const clientETag = req.headers["if-none-match"];
623
+ if (clientETag && clientETag === currentETag) {
624
+ return res.status(304).end();
625
+ }
626
+
627
+ // Set caching headers
628
+ res.set({
629
+ "Cache-Control": "private, max-age=5",
630
+ ETag: currentETag,
631
+ });
632
+
454
633
  // Handle both old and new response formats
455
634
  if (Array.isArray(result)) {
456
635
  // Backward compatibility: no pagination parameters were provided
@@ -518,6 +697,75 @@ app.delete(
518
697
  },
519
698
  );
520
699
 
700
+ // Terminate shell session endpoint (for conflict resolution from chat interface)
701
+ app.post("/api/shell/terminate", authenticateToken, async (req, res) => {
702
+ try {
703
+ const { projectPath, sessionId } = req.body;
704
+
705
+ if (!projectPath) {
706
+ return res.status(400).json({ error: "projectPath is required" });
707
+ }
708
+
709
+ // Build session key (same format as in handleShellConnection)
710
+ const sessionKey = sessionId
711
+ ? `${projectPath}:${sessionId}`
712
+ : `${projectPath}:plain-shell`;
713
+
714
+ console.log("๐Ÿ”ช API: Force closing shell session:", sessionKey);
715
+
716
+ const session = ptySessionsMap.get(sessionKey);
717
+
718
+ if (!session) {
719
+ // Session not found - might already be closed
720
+ return res.json({
721
+ success: true,
722
+ message: "Session not found or already closed",
723
+ });
724
+ }
725
+
726
+ // Notify all shell clients
727
+ const closeMsg = JSON.stringify({
728
+ type: "output",
729
+ data: `\r\n\x1b[31m[Session terminated by another client]\x1b[0m\r\n`,
730
+ });
731
+
732
+ for (const client of session.clients) {
733
+ if (client.readyState === WebSocket.OPEN) {
734
+ try {
735
+ client.send(closeMsg);
736
+ // Also send a session-closed message so clients know to disconnect
737
+ client.send(JSON.stringify({ type: "session-closed" }));
738
+ } catch {
739
+ // Ignore send errors
740
+ }
741
+ }
742
+ }
743
+
744
+ // Kill the PTY process
745
+ if (session.pty && session.pty.kill) {
746
+ session.pty.kill();
747
+ }
748
+
749
+ // Kill tmux session if exists
750
+ if (session.tmuxSessionName) {
751
+ killTmuxSession(session.tmuxSessionName);
752
+ }
753
+
754
+ // Clear any pending timeout
755
+ if (session.timeoutId) {
756
+ clearTimeout(session.timeoutId);
757
+ }
758
+
759
+ ptySessionsMap.delete(sessionKey);
760
+
761
+ console.log("โœ… Shell session terminated:", sessionKey);
762
+ res.json({ success: true, sessionKey });
763
+ } catch (error) {
764
+ console.error("[API] Error terminating shell session:", error);
765
+ res.status(500).json({ error: error.message });
766
+ }
767
+ });
768
+
521
769
  // Create project endpoint
522
770
  app.post("/api/projects/create", authenticateToken, async (req, res) => {
523
771
  try {
@@ -899,11 +1147,112 @@ async function handleChatMessage(ws, writer, messageData) {
899
1147
  const sessionIdForTracking =
900
1148
  data.options?.sessionId || data.sessionId || `session-${Date.now()}`;
901
1149
 
1150
+ // Handle proactive external session check (before user submits a prompt)
1151
+ if (data.type === "check-external-session") {
1152
+ const projectPath = data.projectPath;
1153
+ if (projectPath) {
1154
+ const externalCheck = detectExternalClaude(projectPath);
1155
+ writer.send({
1156
+ type: "external-session-check-result",
1157
+ projectPath,
1158
+ hasExternalSession: externalCheck.hasExternalSession,
1159
+ details: externalCheck.hasExternalSession
1160
+ ? {
1161
+ processIds: externalCheck.processes.map((p) => p.pid),
1162
+ commands: externalCheck.processes.map((p) => p.command),
1163
+ tmuxSessions: externalCheck.tmuxSessions.map(
1164
+ (s) => s.sessionName,
1165
+ ),
1166
+ lockFile: externalCheck.lockFile.exists
1167
+ ? externalCheck.lockFile.lockFile
1168
+ : null,
1169
+ }
1170
+ : null,
1171
+ });
1172
+ }
1173
+ return;
1174
+ }
1175
+
902
1176
  if (data.type === "claude-command") {
903
1177
  console.log("[DEBUG] User message:", data.command || "[Continue/Resume]");
904
1178
  console.log("๐Ÿ“ Project:", data.options?.projectPath || "Unknown");
905
1179
  console.log("๐Ÿ”„ Session:", data.options?.sessionId ? "Resume" : "New");
906
1180
 
1181
+ const projectPath = data.options?.projectPath;
1182
+ const sessionId = data.options?.sessionId || sessionIdForTracking;
1183
+ const ptySessionKey = getPtySessionKey(projectPath, sessionId);
1184
+
1185
+ // Check for active shell session with clients
1186
+ const shellSession = ptySessionsMap.get(ptySessionKey);
1187
+ if (
1188
+ shellSession &&
1189
+ shellSession.clients &&
1190
+ shellSession.clients.size > 0
1191
+ ) {
1192
+ // Shell is active with connected clients
1193
+ writer.send({
1194
+ type: "session-conflict",
1195
+ sessionKey: ptySessionKey,
1196
+ conflictType: "shell-active",
1197
+ message: "A shell session is active with connected clients",
1198
+ clientCount: shellSession.clients.size,
1199
+ options: ["close-shell", "fork-session", "cancel"],
1200
+ });
1201
+ return;
1202
+ }
1203
+
1204
+ // Check for external Claude sessions
1205
+ if (projectPath) {
1206
+ const externalCheck = detectExternalClaude(projectPath);
1207
+ if (externalCheck.hasExternalSession) {
1208
+ writer.send({
1209
+ type: "external-session-detected",
1210
+ projectPath,
1211
+ details: {
1212
+ processIds: externalCheck.processes.map((p) => p.pid),
1213
+ tmuxSessions: externalCheck.tmuxSessions.map(
1214
+ (s) => s.sessionName,
1215
+ ),
1216
+ lockFile: externalCheck.lockFile.exists
1217
+ ? externalCheck.lockFile.lockFile
1218
+ : null,
1219
+ },
1220
+ });
1221
+ // Continue with warning - don't block the chat
1222
+ }
1223
+ }
1224
+
1225
+ // Acquire chat lock
1226
+ const clientId = `chat-${Date.now()}`;
1227
+ const lockResult = sessionLock.acquireLock(
1228
+ ptySessionKey,
1229
+ clientId,
1230
+ "chat",
1231
+ {
1232
+ sessionId,
1233
+ startedAt: Date.now(),
1234
+ },
1235
+ );
1236
+
1237
+ if (!lockResult.success) {
1238
+ writer.send({
1239
+ type: "session-conflict",
1240
+ sessionKey: ptySessionKey,
1241
+ conflictType: "chat-locked",
1242
+ message: lockResult.reason,
1243
+ holder: lockResult.holder,
1244
+ options: ["wait", "cancel"],
1245
+ });
1246
+ return;
1247
+ }
1248
+
1249
+ // Update shell session mode if it exists
1250
+ if (shellSession) {
1251
+ shellSession.mode = "chat";
1252
+ shellSession.lockedBy = clientId;
1253
+ broadcastSessionState(ptySessionKey);
1254
+ }
1255
+
907
1256
  // Track busy status for orchestrator
908
1257
  if (orchestratorStatusHooks) {
909
1258
  orchestratorStatusHooks.onQueryStart(sessionIdForTracking);
@@ -913,6 +1262,16 @@ async function handleChatMessage(ws, writer, messageData) {
913
1262
  // Use Claude Agents SDK
914
1263
  await queryClaudeSDK(data.command, data.options, writer);
915
1264
  } finally {
1265
+ // Release chat lock
1266
+ sessionLock.releaseLock(ptySessionKey, clientId);
1267
+
1268
+ // Reset shell session mode
1269
+ if (shellSession) {
1270
+ shellSession.mode = "shell";
1271
+ shellSession.lockedBy = null;
1272
+ broadcastSessionState(ptySessionKey);
1273
+ }
1274
+
916
1275
  // Mark as no longer busy
917
1276
  if (orchestratorStatusHooks) {
918
1277
  orchestratorStatusHooks.onQueryEnd(sessionIdForTracking);
@@ -1054,12 +1413,14 @@ async function handleChatMessage(ws, writer, messageData) {
1054
1413
  }
1055
1414
  }
1056
1415
 
1057
- // Handle shell WebSocket connections
1416
+ // Handle shell WebSocket connections with multi-client support
1058
1417
  function handleShellConnection(ws) {
1059
1418
  console.log("๐Ÿš Shell client connected");
1419
+
1420
+ // Generate unique client ID for this connection
1421
+ const clientId = `shell-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1060
1422
  let shellProcess = null;
1061
1423
  let ptySessionKey = null;
1062
- let outputBuffer = [];
1063
1424
 
1064
1425
  ws.on("message", async (message) => {
1065
1426
  try {
@@ -1089,7 +1450,7 @@ function handleShellConnection(ws) {
1089
1450
  isPlainShell && initialCommand
1090
1451
  ? `_cmd_${Buffer.from(initialCommand).toString("base64").slice(0, 16)}`
1091
1452
  : "";
1092
- ptySessionKey = `${projectPath}_${sessionId || "default"}${commandSuffix}`;
1453
+ ptySessionKey = getPtySessionKey(projectPath, sessionId, commandSuffix);
1093
1454
 
1094
1455
  // Kill any existing login session before starting fresh
1095
1456
  if (isLoginCommand) {
@@ -1101,6 +1462,10 @@ function handleShellConnection(ws) {
1101
1462
  );
1102
1463
  if (oldSession.timeoutId) clearTimeout(oldSession.timeoutId);
1103
1464
  if (oldSession.pty && oldSession.pty.kill) oldSession.pty.kill();
1465
+ // Kill tmux session if exists
1466
+ if (oldSession.tmuxSessionName) {
1467
+ killTmuxSession(oldSession.tmuxSessionName);
1468
+ }
1104
1469
  ptySessionsMap.delete(ptySessionKey);
1105
1470
  }
1106
1471
  }
@@ -1108,25 +1473,133 @@ function handleShellConnection(ws) {
1108
1473
  const existingSession = isLoginCommand
1109
1474
  ? null
1110
1475
  : ptySessionsMap.get(ptySessionKey);
1476
+
1477
+ // Check for saved tmux session in database (for server restarts)
1478
+ if (!existingSession && !isLoginCommand && !isPlainShell) {
1479
+ const savedTmuxName = tmuxSessionsDb.getTmuxSession(
1480
+ projectPath,
1481
+ sessionId,
1482
+ );
1483
+ if (savedTmuxName && tmuxSessionExists(savedTmuxName)) {
1484
+ console.log(
1485
+ "โ™ป๏ธ Found existing tmux session from database:",
1486
+ savedTmuxName,
1487
+ );
1488
+
1489
+ // Reconnect to the existing tmux session
1490
+ const termCols = data.cols || 80;
1491
+ const termRows = data.rows || 24;
1492
+
1493
+ const attachResult = attachToTmuxSession(savedTmuxName, pty, {
1494
+ cols: termCols,
1495
+ rows: termRows,
1496
+ });
1497
+
1498
+ if (attachResult && attachResult.pty) {
1499
+ shellProcess = attachResult.pty;
1500
+
1501
+ // Update last used timestamp
1502
+ tmuxSessionsDb.touchTmuxSession(projectPath, sessionId);
1503
+
1504
+ // Create session with multi-client support
1505
+ const clients = new Set([ws]);
1506
+ ptySessionsMap.set(ptySessionKey, {
1507
+ pty: shellProcess,
1508
+ clients,
1509
+ buffer: [],
1510
+ mode: "shell",
1511
+ lockedBy: null,
1512
+ tmuxSessionName: savedTmuxName,
1513
+ timeoutId: null,
1514
+ projectPath,
1515
+ sessionId,
1516
+ createdAt: Date.now(),
1517
+ lastActivity: Date.now(),
1518
+ });
1519
+
1520
+ // Handle data output - broadcast to all clients
1521
+ shellProcess.onData((outputData) => {
1522
+ const session = ptySessionsMap.get(ptySessionKey);
1523
+ if (!session) return;
1524
+ session.lastActivity = Date.now();
1525
+
1526
+ // Add to buffer
1527
+ if (session.buffer.length < 5000) {
1528
+ session.buffer.push(outputData);
1529
+ } else {
1530
+ session.buffer.shift();
1531
+ session.buffer.push(outputData);
1532
+ }
1533
+
1534
+ // Broadcast to all clients
1535
+ broadcastToSession(ptySessionKey, {
1536
+ type: "output",
1537
+ data: outputData,
1538
+ });
1539
+ });
1540
+
1541
+ shellProcess.onExit(({ exitCode }) => {
1542
+ console.log(`๐Ÿ”ด Tmux process exited with code ${exitCode}`);
1543
+ const session = ptySessionsMap.get(ptySessionKey);
1544
+ if (session) {
1545
+ broadcastToSession(ptySessionKey, {
1546
+ type: "output",
1547
+ data: `\r\n\x1b[33mSession ended with exit code ${exitCode}\x1b[0m\r\n`,
1548
+ });
1549
+ ptySessionsMap.delete(ptySessionKey);
1550
+ }
1551
+ });
1552
+
1553
+ ws.send(
1554
+ JSON.stringify({
1555
+ type: "output",
1556
+ data: `\x1b[36m[Reconnected to existing tmux session]\x1b[0m\r\n`,
1557
+ }),
1558
+ );
1559
+
1560
+ broadcastSessionState(ptySessionKey);
1561
+ return;
1562
+ } else {
1563
+ // Failed to attach, will create new session below
1564
+ console.log(
1565
+ "โš ๏ธ Failed to attach to saved tmux session, creating new one",
1566
+ );
1567
+ // Clean up stale database entry
1568
+ tmuxSessionsDb.deleteTmuxSession(projectPath, sessionId);
1569
+ }
1570
+ }
1571
+ }
1572
+
1111
1573
  if (existingSession) {
1574
+ // Multi-client: Add this client to the existing session
1112
1575
  console.log(
1113
- "โ™ป๏ธ Reconnecting to existing PTY session:",
1576
+ "โ™ป๏ธ Adding client to existing PTY session:",
1114
1577
  ptySessionKey,
1115
1578
  );
1116
1579
  shellProcess = existingSession.pty;
1117
1580
 
1118
- clearTimeout(existingSession.timeoutId);
1581
+ // Clear timeout since we have an active client
1582
+ if (existingSession.timeoutId) {
1583
+ clearTimeout(existingSession.timeoutId);
1584
+ existingSession.timeoutId = null;
1585
+ }
1586
+
1587
+ // Add client to the clients Set
1588
+ existingSession.clients.add(ws);
1589
+ existingSession.lastActivity = Date.now();
1119
1590
 
1591
+ // Send reconnect message to this client only
1120
1592
  ws.send(
1121
1593
  JSON.stringify({
1122
1594
  type: "output",
1123
- data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`,
1595
+ data: `\x1b[36m[Connected to session - ${existingSession.clients.size} client(s) connected]\x1b[0m\r\n`,
1124
1596
  }),
1125
1597
  );
1126
1598
 
1599
+ // Send buffered output to this new client
1127
1600
  if (existingSession.buffer && existingSession.buffer.length > 0) {
1128
1601
  console.log(
1129
- `๐Ÿ“œ Sending ${existingSession.buffer.length} buffered messages`,
1602
+ `๐Ÿ“œ Sending ${existingSession.buffer.length} buffered messages to new client`,
1130
1603
  );
1131
1604
  existingSession.buffer.forEach((bufferedData) => {
1132
1605
  ws.send(
@@ -1138,7 +1611,22 @@ function handleShellConnection(ws) {
1138
1611
  });
1139
1612
  }
1140
1613
 
1141
- existingSession.ws = ws;
1614
+ // Send session state to this client
1615
+ ws.send(
1616
+ JSON.stringify({
1617
+ type: "session-state-update",
1618
+ sessionKey: ptySessionKey,
1619
+ state: {
1620
+ mode: existingSession.mode || "shell",
1621
+ clientCount: existingSession.clients.size,
1622
+ lockedBy: existingSession.lockedBy,
1623
+ tmuxSessionName: existingSession.tmuxSessionName,
1624
+ },
1625
+ }),
1626
+ );
1627
+
1628
+ // Broadcast updated client count to all clients
1629
+ broadcastSessionState(ptySessionKey);
1142
1630
 
1143
1631
  return;
1144
1632
  }
@@ -1221,114 +1709,201 @@ function handleShellConnection(ws) {
1221
1709
 
1222
1710
  console.log("๐Ÿ”ง Executing shell command:", shellCommand);
1223
1711
 
1224
- // Use appropriate shell based on platform
1225
- const shell = os.platform() === "win32" ? "powershell.exe" : "bash";
1226
- const shellArgs =
1227
- os.platform() === "win32"
1228
- ? ["-Command", shellCommand]
1229
- : ["-c", shellCommand];
1230
-
1231
1712
  // Use terminal dimensions from client if provided, otherwise use defaults
1232
1713
  const termCols = data.cols || 80;
1233
1714
  const termRows = data.rows || 24;
1234
1715
  console.log("๐Ÿ“ Using terminal dimensions:", termCols, "x", termRows);
1235
1716
 
1236
- shellProcess = pty.spawn(shell, shellArgs, {
1237
- name: "xterm-256color",
1238
- cols: termCols,
1239
- rows: termRows,
1240
- cwd: os.homedir(),
1241
- env: {
1242
- ...process.env,
1243
- TERM: "xterm-256color",
1244
- COLORTERM: "truecolor",
1245
- FORCE_COLOR: "3",
1246
- // Override browser opening commands to echo URL for detection
1247
- BROWSER:
1248
- os.platform() === "win32"
1249
- ? 'echo "OPEN_URL:"'
1250
- : 'echo "OPEN_URL:"',
1251
- },
1252
- });
1717
+ // Track tmux session name if we use tmux
1718
+ let tmuxSessionName = null;
1719
+
1720
+ // For non-plain-shell sessions on Unix, use tmux for session persistence
1721
+ const useTmux =
1722
+ !isPlainShell &&
1723
+ os.platform() !== "win32" &&
1724
+ checkTmuxAvailable().available;
1725
+
1726
+ if (useTmux) {
1727
+ // Create tmux session
1728
+ const tmuxResult = createTmuxSession(
1729
+ projectPath,
1730
+ sessionId || "shell",
1731
+ {
1732
+ cols: termCols,
1733
+ rows: termRows,
1734
+ },
1735
+ );
1736
+
1737
+ if (tmuxResult.success) {
1738
+ tmuxSessionName = tmuxResult.tmuxSessionName;
1739
+ console.log("๐Ÿ“บ Created/found tmux session:", tmuxSessionName);
1740
+
1741
+ // Save to database for persistence across server restarts
1742
+ tmuxSessionsDb.saveTmuxSession(
1743
+ projectPath,
1744
+ sessionId,
1745
+ tmuxSessionName,
1746
+ );
1747
+
1748
+ // Send the shell command to tmux
1749
+ const { spawnSync } = await import("child_process");
1750
+ spawnSync(
1751
+ "tmux",
1752
+ ["send-keys", "-t", tmuxSessionName, shellCommand, "Enter"],
1753
+ {
1754
+ encoding: "utf8",
1755
+ },
1756
+ );
1757
+
1758
+ // Attach to tmux session
1759
+ const attachResult = attachToTmuxSession(tmuxSessionName, pty, {
1760
+ cols: termCols,
1761
+ rows: termRows,
1762
+ });
1763
+
1764
+ if (attachResult && attachResult.pty) {
1765
+ shellProcess = attachResult.pty;
1766
+ } else {
1767
+ // Fallback to direct PTY if tmux attach fails
1768
+ console.log(
1769
+ "โš ๏ธ Tmux attach failed, falling back to direct PTY",
1770
+ );
1771
+ tmuxSessionName = null;
1772
+ }
1773
+ }
1774
+ }
1775
+
1776
+ // Fallback to direct PTY spawn (for plain shell, Windows, or tmux failure)
1777
+ if (!shellProcess) {
1778
+ const shell = os.platform() === "win32" ? "powershell.exe" : "bash";
1779
+ const shellArgs =
1780
+ os.platform() === "win32"
1781
+ ? ["-Command", shellCommand]
1782
+ : ["-c", shellCommand];
1783
+
1784
+ shellProcess = pty.spawn(shell, shellArgs, {
1785
+ name: "xterm-256color",
1786
+ cols: termCols,
1787
+ rows: termRows,
1788
+ cwd: os.homedir(),
1789
+ env: {
1790
+ ...process.env,
1791
+ TERM: "xterm-256color",
1792
+ COLORTERM: "truecolor",
1793
+ FORCE_COLOR: "3",
1794
+ // Override browser opening commands to echo URL for detection
1795
+ BROWSER:
1796
+ os.platform() === "win32"
1797
+ ? 'echo "OPEN_URL:"'
1798
+ : 'echo "OPEN_URL:"',
1799
+ },
1800
+ });
1801
+ }
1253
1802
 
1254
1803
  console.log(
1255
1804
  "๐ŸŸข Shell process started with PTY, PID:",
1256
1805
  shellProcess.pid,
1806
+ tmuxSessionName ? `(tmux: ${tmuxSessionName})` : "(direct)",
1257
1807
  );
1258
1808
 
1809
+ // Create session with multi-client support
1810
+ const clients = new Set([ws]);
1259
1811
  ptySessionsMap.set(ptySessionKey, {
1260
1812
  pty: shellProcess,
1261
- ws: ws,
1813
+ clients,
1262
1814
  buffer: [],
1815
+ mode: "shell",
1816
+ lockedBy: null,
1817
+ tmuxSessionName,
1263
1818
  timeoutId: null,
1264
1819
  projectPath,
1265
1820
  sessionId,
1821
+ createdAt: Date.now(),
1822
+ lastActivity: Date.now(),
1266
1823
  });
1267
1824
 
1268
- // Handle data output
1269
- shellProcess.onData((data) => {
1825
+ // Handle data output - broadcast to all clients
1826
+ shellProcess.onData((outputData) => {
1270
1827
  const session = ptySessionsMap.get(ptySessionKey);
1271
1828
  if (!session) return;
1272
1829
 
1830
+ session.lastActivity = Date.now();
1831
+
1832
+ // Add to buffer (circular buffer, max 5000 items)
1273
1833
  if (session.buffer.length < 5000) {
1274
- session.buffer.push(data);
1834
+ session.buffer.push(outputData);
1275
1835
  } else {
1276
1836
  session.buffer.shift();
1277
- session.buffer.push(data);
1837
+ session.buffer.push(outputData);
1278
1838
  }
1279
1839
 
1280
- if (session.ws && session.ws.readyState === WebSocket.OPEN) {
1281
- let outputData = data;
1282
-
1283
- // Check for various URL opening patterns
1284
- const patterns = [
1285
- // Direct browser opening commands
1286
- /(?:xdg-open|open|start)\s+(https?:\/\/[^\s\x1b\x07]+)/g,
1287
- // BROWSER environment variable override
1288
- /OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
1289
- // Git and other tools opening URLs
1290
- /Opening\s+(https?:\/\/[^\s\x1b\x07]+)/gi,
1291
- // General URL patterns that might be opened
1292
- /Visit:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
1293
- /View at:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
1294
- /Browse to:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
1295
- ];
1296
-
1297
- patterns.forEach((pattern) => {
1298
- let match;
1299
- while ((match = pattern.exec(data)) !== null) {
1300
- const url = match[1];
1301
- console.log("[DEBUG] Detected URL for opening:", url);
1302
-
1303
- // Send URL opening message to client
1304
- session.ws.send(
1305
- JSON.stringify({
1306
- type: "url_open",
1307
- url: url,
1308
- }),
1840
+ // Process output for URL detection
1841
+ let processedData = outputData;
1842
+
1843
+ // Check for various URL opening patterns
1844
+ const patterns = [
1845
+ // Direct browser opening commands
1846
+ /(?:xdg-open|open|start)\s+(https?:\/\/[^\s\x1b\x07]+)/g,
1847
+ // BROWSER environment variable override
1848
+ /OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
1849
+ // Git and other tools opening URLs
1850
+ /Opening\s+(https?:\/\/[^\s\x1b\x07]+)/gi,
1851
+ // General URL patterns that might be opened
1852
+ /Visit:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
1853
+ /View at:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
1854
+ /Browse to:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
1855
+ ];
1856
+
1857
+ const detectedUrls = [];
1858
+ patterns.forEach((pattern) => {
1859
+ let match;
1860
+ while ((match = pattern.exec(outputData)) !== null) {
1861
+ const url = match[1];
1862
+ console.log("[DEBUG] Detected URL for opening:", url);
1863
+ detectedUrls.push(url);
1864
+
1865
+ // Replace the OPEN_URL pattern with a user-friendly message
1866
+ if (pattern.source.includes("OPEN_URL")) {
1867
+ processedData = processedData.replace(
1868
+ match[0],
1869
+ `[INFO] Opening in browser: ${url}`,
1309
1870
  );
1871
+ }
1872
+ }
1873
+ });
1310
1874
 
1311
- // Replace the OPEN_URL pattern with a user-friendly message
1312
- if (pattern.source.includes("OPEN_URL")) {
1313
- outputData = outputData.replace(
1314
- match[0],
1315
- `[INFO] Opening in browser: ${url}`,
1875
+ // Broadcast output to all clients
1876
+ for (const client of session.clients) {
1877
+ if (client.readyState === WebSocket.OPEN) {
1878
+ try {
1879
+ // Send URL opening message
1880
+ for (const url of detectedUrls) {
1881
+ client.send(
1882
+ JSON.stringify({
1883
+ type: "url_open",
1884
+ url,
1885
+ }),
1316
1886
  );
1317
1887
  }
1318
- }
1319
- });
1320
1888
 
1321
- // Send regular output
1322
- session.ws.send(
1323
- JSON.stringify({
1324
- type: "output",
1325
- data: outputData,
1326
- }),
1327
- );
1889
+ // Send regular output
1890
+ client.send(
1891
+ JSON.stringify({
1892
+ type: "output",
1893
+ data: processedData,
1894
+ }),
1895
+ );
1896
+ } catch (err) {
1897
+ console.error(
1898
+ "[WARN] Failed to send to client:",
1899
+ err.message,
1900
+ );
1901
+ }
1902
+ }
1328
1903
  }
1329
1904
  });
1330
1905
 
1331
- // Handle process exit
1906
+ // Handle process exit - broadcast to all clients
1332
1907
  shellProcess.onExit((exitCode) => {
1333
1908
  console.log(
1334
1909
  "๐Ÿ”š Shell process exited with code:",
@@ -1337,21 +1912,34 @@ function handleShellConnection(ws) {
1337
1912
  exitCode.signal,
1338
1913
  );
1339
1914
  const session = ptySessionsMap.get(ptySessionKey);
1340
- if (
1341
- session &&
1342
- session.ws &&
1343
- session.ws.readyState === WebSocket.OPEN
1344
- ) {
1345
- session.ws.send(
1346
- JSON.stringify({
1347
- type: "output",
1348
- data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ""}\x1b[0m\r\n`,
1349
- }),
1350
- );
1351
- }
1352
- if (session && session.timeoutId) {
1353
- clearTimeout(session.timeoutId);
1915
+
1916
+ if (session) {
1917
+ // Broadcast exit to all clients
1918
+ const exitMsg = JSON.stringify({
1919
+ type: "output",
1920
+ data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ""}\x1b[0m\r\n`,
1921
+ });
1922
+
1923
+ for (const client of session.clients) {
1924
+ if (client.readyState === WebSocket.OPEN) {
1925
+ try {
1926
+ client.send(exitMsg);
1927
+ } catch {
1928
+ // Ignore send errors during exit
1929
+ }
1930
+ }
1931
+ }
1932
+
1933
+ if (session.timeoutId) {
1934
+ clearTimeout(session.timeoutId);
1935
+ }
1936
+
1937
+ // Kill tmux session if it exists
1938
+ if (session.tmuxSessionName) {
1939
+ killTmuxSession(session.tmuxSessionName);
1940
+ }
1354
1941
  }
1942
+
1355
1943
  ptySessionsMap.delete(ptySessionKey);
1356
1944
  shellProcess = null;
1357
1945
  });
@@ -1366,9 +1954,25 @@ function handleShellConnection(ws) {
1366
1954
  }
1367
1955
  } else if (data.type === "input") {
1368
1956
  // Send input to shell process
1957
+ const session = ptySessionsMap.get(ptySessionKey);
1958
+
1959
+ // Check if session is locked by chat
1960
+ if (session && session.mode === "chat") {
1961
+ ws.send(
1962
+ JSON.stringify({
1963
+ type: "output",
1964
+ data: `\r\n\x1b[33m[Chat in progress - shell input disabled]\x1b[0m\r\n`,
1965
+ }),
1966
+ );
1967
+ return;
1968
+ }
1969
+
1369
1970
  if (shellProcess && shellProcess.write) {
1370
1971
  try {
1371
1972
  shellProcess.write(data.data);
1973
+ if (session) {
1974
+ session.lastActivity = Date.now();
1975
+ }
1372
1976
  } catch (error) {
1373
1977
  console.error("Error writing to shell:", error);
1374
1978
  }
@@ -1380,7 +1984,121 @@ function handleShellConnection(ws) {
1380
1984
  if (shellProcess && shellProcess.resize) {
1381
1985
  console.log("Terminal resize requested:", data.cols, "x", data.rows);
1382
1986
  shellProcess.resize(data.cols, data.rows);
1987
+
1988
+ // Also resize tmux session if using tmux
1989
+ const session = ptySessionsMap.get(ptySessionKey);
1990
+ if (session && session.tmuxSessionName) {
1991
+ resizeTmuxSession(session.tmuxSessionName, data.cols, data.rows);
1992
+ }
1383
1993
  }
1994
+ } else if (data.type === "check-session-mode") {
1995
+ // Return current session mode
1996
+ const session = ptySessionsMap.get(data.sessionKey || ptySessionKey);
1997
+ ws.send(
1998
+ JSON.stringify({
1999
+ type: "session-state-update",
2000
+ sessionKey: data.sessionKey || ptySessionKey,
2001
+ state: session
2002
+ ? {
2003
+ mode: session.mode || "shell",
2004
+ clientCount: session.clients ? session.clients.size : 0,
2005
+ lockedBy: session.lockedBy,
2006
+ tmuxSessionName: session.tmuxSessionName,
2007
+ }
2008
+ : null,
2009
+ }),
2010
+ );
2011
+ } else if (data.type === "terminate") {
2012
+ // Handle client request to terminate their session
2013
+ const session = ptySessionsMap.get(ptySessionKey);
2014
+ if (session) {
2015
+ console.log(
2016
+ "๐Ÿ”ช Client requested session termination:",
2017
+ ptySessionKey,
2018
+ );
2019
+
2020
+ // Notify all shell clients
2021
+ const closeMsg = JSON.stringify({
2022
+ type: "output",
2023
+ data: `\r\n\x1b[33m[Session terminated]\x1b[0m\r\n`,
2024
+ });
2025
+
2026
+ for (const client of session.clients) {
2027
+ if (client.readyState === WebSocket.OPEN) {
2028
+ try {
2029
+ client.send(closeMsg);
2030
+ client.send(JSON.stringify({ type: "session-closed" }));
2031
+ } catch {
2032
+ // Ignore
2033
+ }
2034
+ }
2035
+ }
2036
+
2037
+ // Kill the PTY process
2038
+ if (session.pty && session.pty.kill) {
2039
+ session.pty.kill();
2040
+ }
2041
+
2042
+ // Kill tmux session if exists
2043
+ if (session.tmuxSessionName) {
2044
+ killTmuxSession(session.tmuxSessionName);
2045
+ }
2046
+
2047
+ // Clear any pending timeout
2048
+ if (session.timeoutId) {
2049
+ clearTimeout(session.timeoutId);
2050
+ }
2051
+
2052
+ ptySessionsMap.delete(ptySessionKey);
2053
+ console.log("โœ… Shell session terminated by client:", ptySessionKey);
2054
+ }
2055
+ } else if (data.type === "resolve-conflict") {
2056
+ // Handle conflict resolution
2057
+ const targetSessionKey = data.sessionKey || ptySessionKey;
2058
+ const session = ptySessionsMap.get(targetSessionKey);
2059
+
2060
+ if (data.action === "close-shell" && session) {
2061
+ // Kill the shell session
2062
+ console.log("๐Ÿ”ช Force closing shell session:", targetSessionKey);
2063
+
2064
+ // Notify all shell clients
2065
+ const closeMsg = JSON.stringify({
2066
+ type: "output",
2067
+ data: `\r\n\x1b[31m[Session closed by another client]\x1b[0m\r\n`,
2068
+ });
2069
+
2070
+ for (const client of session.clients) {
2071
+ if (client.readyState === WebSocket.OPEN) {
2072
+ try {
2073
+ client.send(closeMsg);
2074
+ } catch {
2075
+ // Ignore
2076
+ }
2077
+ }
2078
+ }
2079
+
2080
+ // Kill the PTY process
2081
+ if (session.pty && session.pty.kill) {
2082
+ session.pty.kill();
2083
+ }
2084
+
2085
+ // Kill tmux session if exists
2086
+ if (session.tmuxSessionName) {
2087
+ killTmuxSession(session.tmuxSessionName);
2088
+ }
2089
+
2090
+ ptySessionsMap.delete(targetSessionKey);
2091
+
2092
+ // Confirm to the requesting client
2093
+ ws.send(
2094
+ JSON.stringify({
2095
+ type: "conflict-resolved",
2096
+ action: "close-shell",
2097
+ sessionKey: targetSessionKey,
2098
+ }),
2099
+ );
2100
+ }
2101
+ // Note: 'fork-session' is handled by the frontend creating a new session
1384
2102
  }
1385
2103
  } catch (error) {
1386
2104
  console.error("[ERROR] Shell WebSocket error:", error.message);
@@ -1396,27 +2114,42 @@ function handleShellConnection(ws) {
1396
2114
  });
1397
2115
 
1398
2116
  ws.on("close", () => {
1399
- console.log("๐Ÿ”Œ Shell client disconnected");
2117
+ console.log("๐Ÿ”Œ Shell client disconnected:", clientId);
1400
2118
 
1401
2119
  if (ptySessionKey) {
1402
2120
  const session = ptySessionsMap.get(ptySessionKey);
1403
- if (session) {
2121
+ if (session && session.clients) {
2122
+ // Remove this client from the session
2123
+ session.clients.delete(ws);
2124
+
1404
2125
  console.log(
1405
- "โณ PTY session kept alive, will timeout in 30 minutes:",
1406
- ptySessionKey,
2126
+ `๐Ÿ“Š Session ${ptySessionKey}: ${session.clients.size} client(s) remaining`,
1407
2127
  );
1408
- session.ws = null;
1409
2128
 
1410
- session.timeoutId = setTimeout(() => {
2129
+ // Broadcast updated client count to remaining clients
2130
+ if (session.clients.size > 0) {
2131
+ broadcastSessionState(ptySessionKey);
2132
+ } else {
2133
+ // No clients left, start timeout
1411
2134
  console.log(
1412
- "โฐ PTY session timeout, killing process:",
2135
+ "โณ PTY session kept alive, will timeout in 30 minutes:",
1413
2136
  ptySessionKey,
1414
2137
  );
1415
- if (session.pty && session.pty.kill) {
1416
- session.pty.kill();
1417
- }
1418
- ptySessionsMap.delete(ptySessionKey);
1419
- }, PTY_SESSION_TIMEOUT);
2138
+
2139
+ session.timeoutId = setTimeout(() => {
2140
+ console.log(
2141
+ "โฐ PTY session timeout, killing process:",
2142
+ ptySessionKey,
2143
+ );
2144
+ if (session.pty && session.pty.kill) {
2145
+ session.pty.kill();
2146
+ }
2147
+ if (session.tmuxSessionName) {
2148
+ killTmuxSession(session.tmuxSessionName);
2149
+ }
2150
+ ptySessionsMap.delete(ptySessionKey);
2151
+ }, PTY_SESSION_TIMEOUT);
2152
+ }
1420
2153
  }
1421
2154
  }
1422
2155
  });
@@ -1703,6 +2436,9 @@ app.get(
1703
2436
  try {
1704
2437
  const { projectName, sessionId } = req.params;
1705
2438
  const { provider = "claude" } = req.query;
2439
+ console.log(
2440
+ `[TOKEN-USAGE] Request for project: ${projectName}, session: ${sessionId}, provider: ${provider}`,
2441
+ );
1706
2442
  const homeDir = os.homedir();
1707
2443
 
1708
2444
  // Allow only safe characters in sessionId
@@ -1821,8 +2557,8 @@ app.get(
1821
2557
 
1822
2558
  // Construct the JSONL file path
1823
2559
  // Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
1824
- // The encoding replaces /, spaces, ~, and _ with -
1825
- const encodedPath = projectPath.replace(/[\\/:\s~_]/g, "-");
2560
+ // The encoding replaces /, spaces, ~, _, and . with -
2561
+ const encodedPath = projectPath.replace(/[\\/:\s~_.]/g, "-");
1826
2562
  const projectDir = path.join(homeDir, ".claude", "projects", encodedPath);
1827
2563
 
1828
2564
  const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
@@ -1842,9 +2578,22 @@ app.get(
1842
2578
  fileContent = await fsPromises.readFile(jsonlPath, "utf8");
1843
2579
  } catch (error) {
1844
2580
  if (error.code === "ENOENT") {
1845
- return res
1846
- .status(404)
1847
- .json({ error: "Session file not found", path: jsonlPath });
2581
+ // Session file doesn't exist yet (new session with no messages)
2582
+ // Return zero token usage instead of 404
2583
+ const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
2584
+ const contextWindow = Number.isFinite(parsedContextWindow)
2585
+ ? parsedContextWindow
2586
+ : 160000;
2587
+ return res.json({
2588
+ used: 0,
2589
+ total: contextWindow,
2590
+ breakdown: {
2591
+ input: 0,
2592
+ cacheCreation: 0,
2593
+ cacheRead: 0,
2594
+ },
2595
+ newSession: true,
2596
+ });
1848
2597
  }
1849
2598
  throw error; // Re-throw other errors to be caught by outer try-catch
1850
2599
  }
@@ -2110,6 +2859,26 @@ async function startServer() {
2110
2859
 
2111
2860
  // Start watching the projects folder for changes
2112
2861
  await setupProjectsWatcher();
2862
+
2863
+ // Initialize sessions and projects caches with initial project data
2864
+ try {
2865
+ const initialProjects = await getProjects();
2866
+ updateSessionsCache(initialProjects);
2867
+ updateProjectsCache(initialProjects);
2868
+ console.log(
2869
+ `${c.ok("[OK]")} Sessions cache initialized with ${initialProjects.length} projects`,
2870
+ );
2871
+ console.log(
2872
+ `${c.ok("[OK]")} Projects cache initialized with ${initialProjects.length} projects`,
2873
+ );
2874
+
2875
+ // Clean up stale tmux session entries on startup
2876
+ cleanupStaleTmuxSessions();
2877
+ } catch (cacheError) {
2878
+ console.warn(
2879
+ `${c.warn("[WARN]")} Failed to initialize caches: ${cacheError.message}`,
2880
+ );
2881
+ }
2113
2882
  });
2114
2883
  } catch (error) {
2115
2884
  console.error("[ERROR] Failed to start server:", error);