@siteboon/claude-code-ui 1.8.12 → 1.9.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
@@ -27,25 +27,26 @@ try {
27
27
  console.log('PORT from env:', process.env.PORT);
28
28
 
29
29
  import express from 'express';
30
- import { WebSocketServer } from 'ws';
30
+ import { WebSocketServer, WebSocket } from 'ws';
31
+ import os from 'os';
31
32
  import http from 'http';
32
33
  import cors from 'cors';
33
34
  import { promises as fsPromises } from 'fs';
34
35
  import { spawn } from 'child_process';
35
- import os from 'os';
36
36
  import pty from 'node-pty';
37
37
  import fetch from 'node-fetch';
38
38
  import mime from 'mime-types';
39
39
 
40
40
  import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
41
- import { spawnClaude, abortClaudeSession } from './claude-cli.js';
42
- import { spawnCursor, abortCursorSession } from './cursor-cli.js';
41
+ import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions } from './claude-sdk.js';
42
+ import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
43
43
  import gitRoutes from './routes/git.js';
44
44
  import authRoutes from './routes/auth.js';
45
45
  import mcpRoutes from './routes/mcp.js';
46
46
  import cursorRoutes from './routes/cursor.js';
47
47
  import taskmasterRoutes from './routes/taskmaster.js';
48
48
  import mcpUtilsRoutes from './routes/mcp-utils.js';
49
+ import commandsRoutes from './routes/commands.js';
49
50
  import { initializeDatabase } from './database/db.js';
50
51
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
51
52
 
@@ -107,7 +108,7 @@ async function setupProjectsWatcher() {
107
108
  });
108
109
 
109
110
  connectedClients.forEach(client => {
110
- if (client.readyState === client.OPEN) {
111
+ if (client.readyState === WebSocket.OPEN) {
111
112
  client.send(updateMessage);
112
113
  }
113
114
  });
@@ -192,8 +193,24 @@ app.use('/api/taskmaster', authenticateToken, taskmasterRoutes);
192
193
  // MCP utilities
193
194
  app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
194
195
 
196
+ // Commands API Routes (protected)
197
+ app.use('/api/commands', authenticateToken, commandsRoutes);
198
+
195
199
  // Static files served after API routes
196
- app.use(express.static(path.join(__dirname, '../dist')));
200
+ // Add cache control: HTML files should not be cached, but assets can be cached
201
+ app.use(express.static(path.join(__dirname, '../dist'), {
202
+ setHeaders: (res, filePath) => {
203
+ if (filePath.endsWith('.html')) {
204
+ // Prevent HTML caching to avoid service worker issues after builds
205
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
206
+ res.setHeader('Pragma', 'no-cache');
207
+ res.setHeader('Expires', '0');
208
+ } else if (filePath.match(/\.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico)$/)) {
209
+ // Cache static assets for 1 year (they have hashed names)
210
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
211
+ }
212
+ }
213
+ }));
197
214
 
198
215
  // API Routes (protected)
199
216
  app.get('/api/config', authenticateToken, (req, res) => {
@@ -370,15 +387,24 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
370
387
 
371
388
  console.log('📄 File read request:', projectName, filePath);
372
389
 
373
- // Using fsPromises from import
374
-
375
- // Security check - ensure the path is safe and absolute
376
- if (!filePath || !path.isAbsolute(filePath)) {
390
+ // Security: ensure the requested path is inside the project root
391
+ if (!filePath) {
377
392
  return res.status(400).json({ error: 'Invalid file path' });
378
393
  }
379
394
 
380
- const content = await fsPromises.readFile(filePath, 'utf8');
381
- res.json({ content, path: filePath });
395
+ const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
396
+ if (!projectRoot) {
397
+ return res.status(404).json({ error: 'Project not found' });
398
+ }
399
+
400
+ const resolved = path.resolve(filePath);
401
+ const normalizedRoot = path.resolve(projectRoot) + path.sep;
402
+ if (!resolved.startsWith(normalizedRoot)) {
403
+ return res.status(403).json({ error: 'Path must be under project root' });
404
+ }
405
+
406
+ const content = await fsPromises.readFile(resolved, 'utf8');
407
+ res.json({ content, path: resolved });
382
408
  } catch (error) {
383
409
  console.error('Error reading file:', error);
384
410
  if (error.code === 'ENOENT') {
@@ -399,27 +425,35 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
399
425
 
400
426
  console.log('🖼️ Binary file serve request:', projectName, filePath);
401
427
 
402
- // Using fs from import
403
- // Using mime from import
404
-
405
- // Security check - ensure the path is safe and absolute
406
- if (!filePath || !path.isAbsolute(filePath)) {
428
+ // Security: ensure the requested path is inside the project root
429
+ if (!filePath) {
407
430
  return res.status(400).json({ error: 'Invalid file path' });
408
431
  }
409
432
 
433
+ const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
434
+ if (!projectRoot) {
435
+ return res.status(404).json({ error: 'Project not found' });
436
+ }
437
+
438
+ const resolved = path.resolve(filePath);
439
+ const normalizedRoot = path.resolve(projectRoot) + path.sep;
440
+ if (!resolved.startsWith(normalizedRoot)) {
441
+ return res.status(403).json({ error: 'Path must be under project root' });
442
+ }
443
+
410
444
  // Check if file exists
411
445
  try {
412
- await fsPromises.access(filePath);
446
+ await fsPromises.access(resolved);
413
447
  } catch (error) {
414
448
  return res.status(404).json({ error: 'File not found' });
415
449
  }
416
450
 
417
451
  // Get file extension and set appropriate content type
418
- const mimeType = mime.lookup(filePath) || 'application/octet-stream';
452
+ const mimeType = mime.lookup(resolved) || 'application/octet-stream';
419
453
  res.setHeader('Content-Type', mimeType);
420
454
 
421
455
  // Stream the file
422
- const fileStream = fs.createReadStream(filePath);
456
+ const fileStream = fs.createReadStream(resolved);
423
457
  fileStream.pipe(res);
424
458
 
425
459
  fileStream.on('error', (error) => {
@@ -445,10 +479,8 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
445
479
 
446
480
  console.log('💾 File save request:', projectName, filePath);
447
481
 
448
- // Using fsPromises from import
449
-
450
- // Security check - ensure the path is safe and absolute
451
- if (!filePath || !path.isAbsolute(filePath)) {
482
+ // Security: ensure the requested path is inside the project root
483
+ if (!filePath) {
452
484
  return res.status(400).json({ error: 'Invalid file path' });
453
485
  }
454
486
 
@@ -456,21 +488,32 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
456
488
  return res.status(400).json({ error: 'Content is required' });
457
489
  }
458
490
 
491
+ const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
492
+ if (!projectRoot) {
493
+ return res.status(404).json({ error: 'Project not found' });
494
+ }
495
+
496
+ const resolved = path.resolve(filePath);
497
+ const normalizedRoot = path.resolve(projectRoot) + path.sep;
498
+ if (!resolved.startsWith(normalizedRoot)) {
499
+ return res.status(403).json({ error: 'Path must be under project root' });
500
+ }
501
+
459
502
  // Create backup of original file
460
503
  try {
461
- const backupPath = filePath + '.backup.' + Date.now();
462
- await fsPromises.copyFile(filePath, backupPath);
504
+ const backupPath = resolved + '.backup.' + Date.now();
505
+ await fsPromises.copyFile(resolved, backupPath);
463
506
  console.log('📋 Created backup:', backupPath);
464
507
  } catch (backupError) {
465
508
  console.warn('Could not create backup:', backupError.message);
466
509
  }
467
510
 
468
511
  // Write the new content
469
- await fsPromises.writeFile(filePath, content, 'utf8');
512
+ await fsPromises.writeFile(resolved, content, 'utf8');
470
513
 
471
514
  res.json({
472
515
  success: true,
473
- path: filePath,
516
+ path: resolved,
474
517
  message: 'File saved successfully'
475
518
  });
476
519
  } catch (error) {
@@ -550,7 +593,9 @@ function handleChatConnection(ws) {
550
593
  console.log('💬 User message:', data.command || '[Continue/Resume]');
551
594
  console.log('📁 Project:', data.options?.projectPath || 'Unknown');
552
595
  console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
553
- await spawnClaude(data.command, data.options, ws);
596
+
597
+ // Use Claude Agents SDK
598
+ await queryClaudeSDK(data.command, data.options, ws);
554
599
  } else if (data.type === 'cursor-command') {
555
600
  console.log('🖱️ Cursor message:', data.command || '[Continue/Resume]');
556
601
  console.log('📁 Project:', data.options?.cwd || 'Unknown');
@@ -568,9 +613,15 @@ function handleChatConnection(ws) {
568
613
  } else if (data.type === 'abort-session') {
569
614
  console.log('🛑 Abort session request:', data.sessionId);
570
615
  const provider = data.provider || 'claude';
571
- const success = provider === 'cursor'
572
- ? abortCursorSession(data.sessionId)
573
- : abortClaudeSession(data.sessionId);
616
+ let success;
617
+
618
+ if (provider === 'cursor') {
619
+ success = abortCursorSession(data.sessionId);
620
+ } else {
621
+ // Use Claude Agents SDK
622
+ success = await abortClaudeSDKSession(data.sessionId);
623
+ }
624
+
574
625
  ws.send(JSON.stringify({
575
626
  type: 'session-aborted',
576
627
  sessionId: data.sessionId,
@@ -586,6 +637,35 @@ function handleChatConnection(ws) {
586
637
  provider: 'cursor',
587
638
  success
588
639
  }));
640
+ } else if (data.type === 'check-session-status') {
641
+ // Check if a specific session is currently processing
642
+ const provider = data.provider || 'claude';
643
+ const sessionId = data.sessionId;
644
+ let isActive;
645
+
646
+ if (provider === 'cursor') {
647
+ isActive = isCursorSessionActive(sessionId);
648
+ } else {
649
+ // Use Claude Agents SDK
650
+ isActive = isClaudeSDKSessionActive(sessionId);
651
+ }
652
+
653
+ ws.send(JSON.stringify({
654
+ type: 'session-status',
655
+ sessionId,
656
+ provider,
657
+ isProcessing: isActive
658
+ }));
659
+ } else if (data.type === 'get-active-sessions') {
660
+ // Get all currently active sessions
661
+ const activeSessions = {
662
+ claude: getActiveClaudeSDKSessions(),
663
+ cursor: getActiveCursorSessions()
664
+ };
665
+ ws.send(JSON.stringify({
666
+ type: 'active-sessions',
667
+ sessions: activeSessions
668
+ }));
589
669
  }
590
670
  } catch (error) {
591
671
  console.error('❌ Chat WebSocket error:', error.message);
@@ -714,7 +794,7 @@ function handleShellConnection(ws) {
714
794
 
715
795
  // Handle data output
716
796
  shellProcess.onData((data) => {
717
- if (ws.readyState === ws.OPEN) {
797
+ if (ws.readyState === WebSocket.OPEN) {
718
798
  let outputData = data;
719
799
 
720
800
  // Check for various URL opening patterns
@@ -761,7 +841,7 @@ function handleShellConnection(ws) {
761
841
  // Handle process exit
762
842
  shellProcess.onExit((exitCode) => {
763
843
  console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
764
- if (ws.readyState === ws.OPEN) {
844
+ if (ws.readyState === WebSocket.OPEN) {
765
845
  ws.send(JSON.stringify({
766
846
  type: 'output',
767
847
  data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
@@ -798,7 +878,7 @@ function handleShellConnection(ws) {
798
878
  }
799
879
  } catch (error) {
800
880
  console.error('❌ Shell WebSocket error:', error.message);
801
- if (ws.readyState === ws.OPEN) {
881
+ if (ws.readyState === WebSocket.OPEN) {
802
882
  ws.send(JSON.stringify({
803
883
  type: 'output',
804
884
  data: `\r\n\x1b[31mError: ${error.message}\x1b[0m\r\n`
@@ -1053,13 +1133,116 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
1053
1133
  }
1054
1134
  });
1055
1135
 
1056
- // Serve React app for all other routes
1136
+ // Get token usage for a specific session
1137
+ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
1138
+ try {
1139
+ const { projectName, sessionId } = req.params;
1140
+ const homeDir = os.homedir();
1141
+
1142
+ // Extract actual project path
1143
+ let projectPath;
1144
+ try {
1145
+ projectPath = await extractProjectDirectory(projectName);
1146
+ } catch (error) {
1147
+ console.error('Error extracting project directory:', error);
1148
+ return res.status(500).json({ error: 'Failed to determine project path' });
1149
+ }
1150
+
1151
+ // Construct the JSONL file path
1152
+ // Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
1153
+ // The encoding replaces /, spaces, ~, and _ with -
1154
+ const encodedPath = projectPath.replace(/[\\/:\s~_]/g, '-');
1155
+ const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
1156
+
1157
+ // Allow only safe characters in sessionId
1158
+ const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
1159
+ if (!safeSessionId) {
1160
+ return res.status(400).json({ error: 'Invalid sessionId' });
1161
+ }
1162
+ const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
1163
+
1164
+ // Constrain to projectDir
1165
+ const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
1166
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
1167
+ return res.status(400).json({ error: 'Invalid path' });
1168
+ }
1169
+
1170
+ // Read and parse the JSONL file
1171
+ let fileContent;
1172
+ try {
1173
+ fileContent = await fsPromises.readFile(jsonlPath, 'utf8');
1174
+ } catch (error) {
1175
+ if (error.code === 'ENOENT') {
1176
+ return res.status(404).json({ error: 'Session file not found', path: jsonlPath });
1177
+ }
1178
+ throw error; // Re-throw other errors to be caught by outer try-catch
1179
+ }
1180
+ const lines = fileContent.trim().split('\n');
1181
+
1182
+ const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
1183
+ const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
1184
+ let inputTokens = 0;
1185
+ let cacheCreationTokens = 0;
1186
+ let cacheReadTokens = 0;
1187
+
1188
+ // Find the latest assistant message with usage data (scan from end)
1189
+ for (let i = lines.length - 1; i >= 0; i--) {
1190
+ try {
1191
+ const entry = JSON.parse(lines[i]);
1192
+
1193
+ // Only count assistant messages which have usage data
1194
+ if (entry.type === 'assistant' && entry.message?.usage) {
1195
+ const usage = entry.message.usage;
1196
+
1197
+ // Use token counts from latest assistant message only
1198
+ inputTokens = usage.input_tokens || 0;
1199
+ cacheCreationTokens = usage.cache_creation_input_tokens || 0;
1200
+ cacheReadTokens = usage.cache_read_input_tokens || 0;
1201
+
1202
+ break; // Stop after finding the latest assistant message
1203
+ }
1204
+ } catch (parseError) {
1205
+ // Skip lines that can't be parsed
1206
+ continue;
1207
+ }
1208
+ }
1209
+
1210
+ // Calculate total context usage (excluding output_tokens, as per ccusage)
1211
+ const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
1212
+
1213
+ res.json({
1214
+ used: totalUsed,
1215
+ total: contextWindow,
1216
+ breakdown: {
1217
+ input: inputTokens,
1218
+ cacheCreation: cacheCreationTokens,
1219
+ cacheRead: cacheReadTokens
1220
+ }
1221
+ });
1222
+ } catch (error) {
1223
+ console.error('Error reading session token usage:', error);
1224
+ res.status(500).json({ error: 'Failed to read session token usage' });
1225
+ }
1226
+ });
1227
+
1228
+ // Serve React app for all other routes (excluding static files)
1057
1229
  app.get('*', (req, res) => {
1230
+ // Skip requests for static assets (files with extensions)
1231
+ if (path.extname(req.path)) {
1232
+ return res.status(404).send('Not found');
1233
+ }
1234
+
1235
+ // Only serve index.html for HTML routes, not for static assets
1236
+ // Static assets should already be handled by express.static middleware above
1058
1237
  if (process.env.NODE_ENV === 'production') {
1238
+ // Set no-cache headers for HTML to prevent service worker issues
1239
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
1240
+ res.setHeader('Pragma', 'no-cache');
1241
+ res.setHeader('Expires', '0');
1059
1242
  res.sendFile(path.join(__dirname, '../dist/index.html'));
1060
1243
  } else {
1061
1244
  // In development, redirect to Vite dev server
1062
- res.redirect(`http://localhost:${process.env.VITE_PORT || 3001}`);
1245
+ res.redirect(`http://localhost:${process.env.VITE_PORT || 5173}`);
1063
1246
  }
1064
1247
  });
1065
1248
 
@@ -1153,11 +1336,14 @@ async function startServer() {
1153
1336
  await initializeDatabase();
1154
1337
  console.log('✅ Database initialization skipped (testing)');
1155
1338
 
1339
+ // Log Claude implementation mode
1340
+ console.log('🚀 Using Claude Agents SDK for Claude integration');
1341
+
1156
1342
  server.listen(PORT, '0.0.0.0', async () => {
1157
1343
  console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
1158
1344
 
1159
1345
  // Start watching the projects folder for changes
1160
- await setupProjectsWatcher();
1346
+ await setupProjectsWatcher();
1161
1347
  });
1162
1348
  } catch (error) {
1163
1349
  console.error('❌ Failed to start server:', error);
@@ -627,8 +627,9 @@ async function getSessions(projectName, limit = 5, offset = 0) {
627
627
  return session;
628
628
  });
629
629
  const visibleSessions = [...latestFromGroups, ...standaloneSessionsArray]
630
+ .filter(session => !session.summary.startsWith('{ "'))
630
631
  .sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
631
-
632
+
632
633
  const total = visibleSessions.length;
633
634
  const paginatedSessions = visibleSessions.slice(offset, offset + limit);
634
635
  const hasMore = offset + limit < total;
@@ -649,20 +650,26 @@ async function getSessions(projectName, limit = 5, offset = 0) {
649
650
  async function parseJsonlSessions(filePath) {
650
651
  const sessions = new Map();
651
652
  const entries = [];
652
-
653
+ const pendingSummaries = new Map(); // leafUuid -> summary for entries without sessionId
654
+
653
655
  try {
654
656
  const fileStream = fsSync.createReadStream(filePath);
655
657
  const rl = readline.createInterface({
656
658
  input: fileStream,
657
659
  crlfDelay: Infinity
658
660
  });
659
-
661
+
660
662
  for await (const line of rl) {
661
663
  if (line.trim()) {
662
664
  try {
663
665
  const entry = JSON.parse(line);
664
666
  entries.push(entry);
665
-
667
+
668
+ // Handle summary entries that don't have sessionId yet
669
+ if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
670
+ pendingSummaries.set(entry.leafUuid, entry.summary);
671
+ }
672
+
666
673
  if (entry.sessionId) {
667
674
  if (!sessions.has(entry.sessionId)) {
668
675
  sessions.set(entry.sessionId, {
@@ -670,24 +677,84 @@ async function parseJsonlSessions(filePath) {
670
677
  summary: 'New Session',
671
678
  messageCount: 0,
672
679
  lastActivity: new Date(),
673
- cwd: entry.cwd || ''
680
+ cwd: entry.cwd || '',
681
+ lastUserMessage: null,
682
+ lastAssistantMessage: null
674
683
  });
675
684
  }
676
-
685
+
677
686
  const session = sessions.get(entry.sessionId);
678
-
679
- // Update summary from summary entries or first user message
687
+
688
+ // Apply pending summary if this entry has a parentUuid that matches a pending summary
689
+ if (session.summary === 'New Session' && entry.parentUuid && pendingSummaries.has(entry.parentUuid)) {
690
+ session.summary = pendingSummaries.get(entry.parentUuid);
691
+ }
692
+
693
+ // Update summary from summary entries with sessionId
680
694
  if (entry.type === 'summary' && entry.summary) {
681
695
  session.summary = entry.summary;
682
- } else if (entry.message?.role === 'user' && entry.message?.content && session.summary === 'New Session') {
696
+ }
697
+
698
+ // Track last user and assistant messages (skip system messages)
699
+ if (entry.message?.role === 'user' && entry.message?.content) {
683
700
  const content = entry.message.content;
684
- if (typeof content === 'string' && content.length > 0 && !content.startsWith('<command-name>')) {
685
- session.summary = content.length > 50 ? content.substring(0, 50) + '...' : content;
701
+
702
+ // Extract text from array format if needed
703
+ let textContent = content;
704
+ if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
705
+ textContent = content[0].text;
706
+ }
707
+
708
+ const isSystemMessage = typeof textContent === 'string' && (
709
+ textContent.startsWith('<command-name>') ||
710
+ textContent.startsWith('<command-message>') ||
711
+ textContent.startsWith('<command-args>') ||
712
+ textContent.startsWith('<local-command-stdout>') ||
713
+ textContent.startsWith('<system-reminder>') ||
714
+ textContent.startsWith('Caveat:') ||
715
+ textContent.startsWith('This session is being continued from a previous') ||
716
+ textContent.startsWith('Invalid API key') ||
717
+ textContent.includes('{"subtasks":') || // Filter Task Master prompts
718
+ textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') || // Filter Task Master system prompts
719
+ textContent === 'Warmup' // Explicitly filter out "Warmup"
720
+ );
721
+
722
+ if (typeof textContent === 'string' && textContent.length > 0 && !isSystemMessage) {
723
+ session.lastUserMessage = textContent;
724
+ }
725
+ } else if (entry.message?.role === 'assistant' && entry.message?.content) {
726
+ // Skip API error messages using the isApiErrorMessage flag
727
+ if (entry.isApiErrorMessage === true) {
728
+ // Skip this message entirely
729
+ } else {
730
+ // Track last assistant text message
731
+ let assistantText = null;
732
+
733
+ if (Array.isArray(entry.message.content)) {
734
+ for (const part of entry.message.content) {
735
+ if (part.type === 'text' && part.text) {
736
+ assistantText = part.text;
737
+ }
738
+ }
739
+ } else if (typeof entry.message.content === 'string') {
740
+ assistantText = entry.message.content;
741
+ }
742
+
743
+ // Additional filter for assistant messages with system content
744
+ const isSystemAssistantMessage = typeof assistantText === 'string' && (
745
+ assistantText.startsWith('Invalid API key') ||
746
+ assistantText.includes('{"subtasks":') ||
747
+ assistantText.includes('CRITICAL: You MUST respond with ONLY a JSON')
748
+ );
749
+
750
+ if (assistantText && !isSystemAssistantMessage) {
751
+ session.lastAssistantMessage = assistantText;
752
+ }
686
753
  }
687
754
  }
688
-
755
+
689
756
  session.messageCount++;
690
-
757
+
691
758
  if (entry.timestamp) {
692
759
  session.lastActivity = new Date(entry.timestamp);
693
760
  }
@@ -697,12 +764,36 @@ async function parseJsonlSessions(filePath) {
697
764
  }
698
765
  }
699
766
  }
700
-
767
+
768
+ // After processing all entries, set final summary based on last message if no summary exists
769
+ for (const session of sessions.values()) {
770
+ if (session.summary === 'New Session') {
771
+ // Prefer last user message, fall back to last assistant message
772
+ const lastMessage = session.lastUserMessage || session.lastAssistantMessage;
773
+ if (lastMessage) {
774
+ session.summary = lastMessage.length > 50 ? lastMessage.substring(0, 50) + '...' : lastMessage;
775
+ }
776
+ }
777
+ }
778
+
779
+ // Filter out sessions that contain JSON responses (Task Master errors)
780
+ const allSessions = Array.from(sessions.values());
781
+ const filteredSessions = allSessions.filter(session => {
782
+ const shouldFilter = session.summary.startsWith('{ "');
783
+ if (shouldFilter) {
784
+ }
785
+ // Log a sample of summaries to debug
786
+ if (Math.random() < 0.01) { // Log 1% of sessions
787
+ }
788
+ return !shouldFilter;
789
+ });
790
+
791
+
701
792
  return {
702
- sessions: Array.from(sessions.values()),
793
+ sessions: filteredSessions,
703
794
  entries: entries
704
795
  };
705
-
796
+
706
797
  } catch (error) {
707
798
  console.error('Error reading JSONL file:', error);
708
799
  return { sessions: [], entries: [] };
@@ -1060,4 +1151,4 @@ export {
1060
1151
  saveProjectConfig,
1061
1152
  extractProjectDirectory,
1062
1153
  clearProjectDirectoryCache
1063
- };
1154
+ };