@axhub/genie 0.2.9 → 0.2.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 (96) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-CYCCsgwf.js +264 -0
  3. package/dist/assets/{ReviewApp-C9K--AQE.js → ReviewApp-0srHIXwb.js} +1 -1
  4. package/dist/assets/{_basePickBy-DR_8uFCo.js → _basePickBy-DVVb07UV.js} +1 -1
  5. package/dist/assets/{_baseUniq-D0njlQ_7.js → _baseUniq-BtbziL5G.js} +1 -1
  6. package/dist/assets/{arc-CKlr_Rec.js → arc-BsCC8yBD.js} +1 -1
  7. package/dist/assets/{architectureDiagram-2XIMDMQ5-BmO_uLUH.js → architectureDiagram-2XIMDMQ5-woFp6eNI.js} +1 -1
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-DhAeO-56.js → blockDiagram-WCTKOSBZ-ya8VAc2k.js} +1 -1
  9. package/dist/assets/{c4Diagram-IC4MRINW-C67kFoXx.js → c4Diagram-IC4MRINW-CY1dZmIZ.js} +1 -1
  10. package/dist/assets/channel-BMhScXFe.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB-mLLagvJi.js → chunk-4BX2VUAB-CR1lAd74.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-Lx-hOjlM.js → chunk-55IACEB6-CP98WcFC.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-Bt-XmVUV.js → chunk-FMBD7UC4-D9c7ijAB.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-Cya6gaDV.js → chunk-JSJVCQXG-DQAGYOn-.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-Bd7Ig6tF.js → chunk-KX2RTZJC-BbTXiDq7.js} +1 -1
  16. package/dist/assets/{chunk-NQ4KR5QH-5UAE0Vg-.js → chunk-NQ4KR5QH-BI6AX0dr.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-BAxZ8m7w.js → chunk-QZHKN3VN-DB3V2Ifo.js} +1 -1
  18. package/dist/assets/{chunk-WL4C6EOR-DjDPvUUP.js → chunk-WL4C6EOR-DhzTthv6.js} +1 -1
  19. package/dist/assets/classDiagram-VBA2DB6C-CMIxlWcT.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-CMIxlWcT.js +1 -0
  21. package/dist/assets/clone-BPqOt4r3.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-D-60XrkJ.js → cose-bilkent-S5V4N54A-BQ09ZE2j.js} +1 -1
  23. package/dist/assets/{dagre-KLK3FWXG-bqu3ZS4K.js → dagre-KLK3FWXG-Dc2ueD_R.js} +1 -1
  24. package/dist/assets/{diagram-E7M64L7V-BueeqoYm.js → diagram-E7M64L7V-DP-LsQoL.js} +1 -1
  25. package/dist/assets/{diagram-IFDJBPK2-D4fDv2E7.js → diagram-IFDJBPK2-Cg6r42cB.js} +1 -1
  26. package/dist/assets/{diagram-P4PSJMXO-WqipY3fN.js → diagram-P4PSJMXO-aHsfoUZE.js} +1 -1
  27. package/dist/assets/{erDiagram-INFDFZHY-D0oVnO-x.js → erDiagram-INFDFZHY-qBXJ4aAz.js} +1 -1
  28. package/dist/assets/{flowDiagram-PKNHOUZH-DzbGyxrr.js → flowDiagram-PKNHOUZH-D_13emJM.js} +1 -1
  29. package/dist/assets/{ganttDiagram-A5KZAMGK-BwhbbgCP.js → ganttDiagram-A5KZAMGK-BvIcOLwz.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js → gitGraphDiagram-K3NZZRJ6-ad0vvNcU.js} +1 -1
  31. package/dist/assets/{graph-DzKos-N0.js → graph-CeJCMjan.js} +1 -1
  32. package/dist/assets/{highlighted-body-TPN3WLV5-CKDMgz3X.js → highlighted-body-TPN3WLV5-B_novwSz.js} +1 -1
  33. package/dist/assets/index-C514cLyb.js +2 -0
  34. package/dist/assets/index-h1DBl_g3.css +1 -0
  35. package/dist/assets/{infoDiagram-LFFYTUFH-BFicZbTf.js → infoDiagram-LFFYTUFH-lOxAqb3m.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-PHBUUO56-CtihxDxl.js → ishikawaDiagram-PHBUUO56-DIr-51gj.js} +1 -1
  37. package/dist/assets/{journeyDiagram-4ABVD52K-Du00J8_d.js → journeyDiagram-4ABVD52K-CYcIW0ZU.js} +1 -1
  38. package/dist/assets/{kanban-definition-K7BYSVSG-BJi9S0iQ.js → kanban-definition-K7BYSVSG-C1ZK616a.js} +1 -1
  39. package/dist/assets/{layout-B80Sityu.js → layout-CI2RM-v6.js} +1 -1
  40. package/dist/assets/{linear-sRQLOf5H.js → linear-DE7bISck.js} +1 -1
  41. package/dist/assets/{mermaid-O7DHMXV3-CBuVs4eJ.js → mermaid-O7DHMXV3-XxAJo8EK.js} +6 -6
  42. package/dist/assets/{mindmap-definition-YRQLILUH-C5IL_xi-.js → mindmap-definition-YRQLILUH-Dz6EFjmn.js} +1 -1
  43. package/dist/assets/{pieDiagram-SKSYHLDU-CeTwlJ8z.js → pieDiagram-SKSYHLDU-DPpEzUed.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-337W2JSQ-COfUcLWt.js → quadrantDiagram-337W2JSQ-xdoXNet7.js} +1 -1
  45. package/dist/assets/{requirementDiagram-Z7DCOOCP-DSb-CJ5B.js → requirementDiagram-Z7DCOOCP-DUq8H3CL.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-WA2Y5GQK-8jtuVb45.js → sankeyDiagram-WA2Y5GQK-CmqEUxRu.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-2WXFIKYE-C2VpkMwA.js → sequenceDiagram-2WXFIKYE-DhtXRNiH.js} +1 -1
  48. package/dist/assets/{stateDiagram-RAJIS63D-fmwMqxxc.js → stateDiagram-RAJIS63D-Dj0HOlbN.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-FVOUBMTO-C9utf5gv.js +1 -0
  50. package/dist/assets/{timeline-definition-YZTLITO2-Dx1hP5lg.js → timeline-definition-YZTLITO2-DUuJzZB5.js} +1 -1
  51. package/dist/assets/{treemap-KZPCXAKY-CkLOdYCZ.js → treemap-KZPCXAKY-DpYBQ0qr.js} +1 -1
  52. package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
  53. package/dist/assets/{vennDiagram-LZ73GAT5-D6KWcnln.js → vennDiagram-LZ73GAT5-DpePUyOd.js} +1 -1
  54. package/dist/assets/{xychartDiagram-JWTSCODW-6fh6qmzN.js → xychartDiagram-JWTSCODW-Cfp1I4_U.js} +1 -1
  55. package/dist/index.html +3 -3
  56. package/package.json +6 -5
  57. package/server/acp-runtime/client.js +120 -14
  58. package/server/acp-runtime/index.js +54 -0
  59. package/server/acp-runtime/registry.js +2 -2
  60. package/server/acp-runtime/session-store.js +75 -1
  61. package/server/cli.js +32 -8
  62. package/server/database/db.js +20 -0
  63. package/server/external-agent/ws.js +477 -24
  64. package/server/index.js +78 -146
  65. package/server/lan-access/core.js +79 -0
  66. package/server/lan-access/state.js +102 -0
  67. package/server/middleware/auth.js +57 -14
  68. package/server/projects.js +423 -535
  69. package/server/routes/auth.js +24 -4
  70. package/server/routes/cli-auth.js +21 -25
  71. package/server/routes/codex.js +84 -298
  72. package/server/routes/commands.js +322 -407
  73. package/server/routes/lan-access.js +231 -0
  74. package/server/routes/projects.js +154 -158
  75. package/server/routes/session-core.js +13 -7
  76. package/server/routes/settings.js +113 -99
  77. package/server/session-core/eventStore.js +15 -2
  78. package/server/session-core/providerAdapters.js +28 -28
  79. package/server/session-core/sessionListMerge.js +47 -0
  80. package/shared/conversationEvents.js +96 -1
  81. package/shared/modelConstants.js +79 -99
  82. package/dist/assets/App-GBcTeeUS.js +0 -460
  83. package/dist/assets/channel-V3MBjKys.js +0 -1
  84. package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +0 -1
  85. package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +0 -1
  86. package/dist/assets/clone-BbMGfZwt.js +0 -1
  87. package/dist/assets/index-DiQlHzGj.js +0 -2
  88. package/dist/assets/index-Drat2nB9.css +0 -1
  89. package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +0 -1
  90. package/dist/assets/vendor-codemirror-BxPY6emf.js +0 -39
  91. package/server/routes/git.js +0 -1110
  92. package/server/routes/mcp-utils.js +0 -48
  93. package/server/routes/mcp.js +0 -536
  94. package/server/routes/taskmaster.js +0 -1963
  95. package/server/utils/mcp-detector.js +0 -198
  96. package/server/utils/taskmaster-websocket.js +0 -129
@@ -54,6 +54,8 @@ import crypto from 'crypto';
54
54
  import { fileURLToPath } from 'url';
55
55
  import { parseCodexTokenCountInfo } from './utils/codexTokenUsage.js';
56
56
  import { CODEX_MODELS } from '../shared/modelConstants.js';
57
+ import { listAcpSessions } from './acp-runtime/session-store.js';
58
+ import { mergeSessionLists } from './session-core/sessionListMerge.js';
57
59
 
58
60
  const __filename = fileURLToPath(import.meta.url);
59
61
  const __dirname = path.dirname(__filename);
@@ -64,6 +66,10 @@ const KNOWN_CODEX_MODELS = new Set(
64
66
 
65
67
  const PROJECT_CONFIG_PERMISSION_ERROR_CODES = new Set(['EACCES', 'EPERM', 'EROFS']);
66
68
 
69
+ function trimText(value) {
70
+ return typeof value === 'string' ? value.trim() : '';
71
+ }
72
+
67
73
  function getPrimaryProjectConfigPath() {
68
74
  return path.join(os.homedir(), '.claude', 'project-config.json');
69
75
  }
@@ -174,6 +180,40 @@ function extractVisibleUserMessage(value) {
174
180
  return text;
175
181
  }
176
182
 
183
+ function extractCodexMessageText(content) {
184
+ if (!Array.isArray(content)) {
185
+ return typeof content === 'string' ? content : '';
186
+ }
187
+
188
+ return content
189
+ .map((item) => {
190
+ if (item?.type === 'input_text' || item?.type === 'output_text' || item?.type === 'text') {
191
+ return item.text;
192
+ }
193
+ return '';
194
+ })
195
+ .filter(Boolean)
196
+ .join('\n');
197
+ }
198
+
199
+ function shouldIncludeCodexTranscriptMessage(entry) {
200
+ const payload = entry?.payload;
201
+ if (entry?.type !== 'response_item' || payload?.type !== 'message') {
202
+ return false;
203
+ }
204
+
205
+ const role = String(payload.role || '').trim().toLowerCase();
206
+ if (role !== 'user' && role !== 'assistant') {
207
+ return false;
208
+ }
209
+
210
+ if (role === 'assistant' && String(payload.phase || '').trim().toLowerCase() === 'commentary') {
211
+ return false;
212
+ }
213
+
214
+ return true;
215
+ }
216
+
177
217
  function isInjectedContextContent(value) {
178
218
  const text = String(value || '').trim();
179
219
  if (!text) {
@@ -194,134 +234,6 @@ function isInjectedContextContent(value) {
194
234
  || /<subagent_notification(?:\s|>)/i.test(text);
195
235
  }
196
236
 
197
- // Import TaskMaster detection functions
198
- async function detectTaskMasterFolder(projectPath) {
199
- try {
200
- const taskMasterPath = path.join(projectPath, '.taskmaster');
201
-
202
- // Check if .taskmaster directory exists
203
- try {
204
- const stats = await fs.stat(taskMasterPath);
205
- if (!stats.isDirectory()) {
206
- return {
207
- hasTaskmaster: false,
208
- reason: '.taskmaster exists but is not a directory'
209
- };
210
- }
211
- } catch (error) {
212
- if (error.code === 'ENOENT') {
213
- return {
214
- hasTaskmaster: false,
215
- reason: '.taskmaster directory not found'
216
- };
217
- }
218
- throw error;
219
- }
220
-
221
- // Check for key TaskMaster files
222
- const keyFiles = [
223
- 'tasks/tasks.json',
224
- 'config.json'
225
- ];
226
-
227
- const fileStatus = {};
228
- let hasEssentialFiles = true;
229
-
230
- for (const file of keyFiles) {
231
- const filePath = path.join(taskMasterPath, file);
232
- try {
233
- await fs.access(filePath);
234
- fileStatus[file] = true;
235
- } catch (error) {
236
- fileStatus[file] = false;
237
- if (file === 'tasks/tasks.json') {
238
- hasEssentialFiles = false;
239
- }
240
- }
241
- }
242
-
243
- // Parse tasks.json if it exists for metadata
244
- let taskMetadata = null;
245
- if (fileStatus['tasks/tasks.json']) {
246
- try {
247
- const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
248
- const tasksContent = await fs.readFile(tasksPath, 'utf8');
249
- const tasksData = JSON.parse(tasksContent);
250
-
251
- // Handle both tagged and legacy formats
252
- let tasks = [];
253
- if (tasksData.tasks) {
254
- // Legacy format
255
- tasks = tasksData.tasks;
256
- } else {
257
- // Tagged format - get tasks from all tags
258
- Object.values(tasksData).forEach(tagData => {
259
- if (tagData.tasks) {
260
- tasks = tasks.concat(tagData.tasks);
261
- }
262
- });
263
- }
264
-
265
- // Calculate task statistics
266
- const stats = tasks.reduce((acc, task) => {
267
- acc.total++;
268
- acc[task.status] = (acc[task.status] || 0) + 1;
269
-
270
- // Count subtasks
271
- if (task.subtasks) {
272
- task.subtasks.forEach(subtask => {
273
- acc.subtotalTasks++;
274
- acc.subtasks = acc.subtasks || {};
275
- acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
276
- });
277
- }
278
-
279
- return acc;
280
- }, {
281
- total: 0,
282
- subtotalTasks: 0,
283
- pending: 0,
284
- 'in-progress': 0,
285
- done: 0,
286
- review: 0,
287
- deferred: 0,
288
- cancelled: 0,
289
- subtasks: {}
290
- });
291
-
292
- taskMetadata = {
293
- taskCount: stats.total,
294
- subtaskCount: stats.subtotalTasks,
295
- completed: stats.done || 0,
296
- pending: stats.pending || 0,
297
- inProgress: stats['in-progress'] || 0,
298
- review: stats.review || 0,
299
- completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
300
- lastModified: (await fs.stat(tasksPath)).mtime.toISOString()
301
- };
302
- } catch (parseError) {
303
- console.warn('Failed to parse tasks.json:', parseError.message);
304
- taskMetadata = { error: 'Failed to parse tasks.json' };
305
- }
306
- }
307
-
308
- return {
309
- hasTaskmaster: true,
310
- hasEssentialFiles,
311
- files: fileStatus,
312
- metadata: taskMetadata,
313
- path: taskMasterPath
314
- };
315
-
316
- } catch (error) {
317
- console.error('Error detecting TaskMaster folder:', error);
318
- return {
319
- hasTaskmaster: false,
320
- reason: `Error checking directory: ${error.message}`
321
- };
322
- }
323
- }
324
-
325
237
  // Cache for extracted project directories
326
238
  const projectDirectoryCache = new Map();
327
239
  const PROJECT_LIST_CACHE_TTL_MS = 15000;
@@ -601,151 +513,118 @@ async function saveProjectConfig(config) {
601
513
  }
602
514
  }
603
515
 
604
- // Generate better display name from path
605
- async function generateDisplayName(projectName, actualProjectDir = null) {
606
- // Use actual project directory if provided, otherwise decode from project name
607
- let projectPath = actualProjectDir || projectName.replace(/-/g, '/');
608
-
609
- // Try to read package.json from the project path
516
+ function decodeProjectNameFallback(projectName) {
517
+ return String(projectName || '').replace(/-/g, '/');
518
+ }
519
+
520
+ async function readPackageDisplayName(projectPath) {
610
521
  try {
611
- const packageJsonPath = path.join(projectPath, 'package.json');
612
- const packageData = await fs.readFile(packageJsonPath, 'utf8');
613
- const packageJson = JSON.parse(packageData);
614
-
615
- // Return the name from package.json if it exists
616
- if (packageJson.name) {
617
- return packageJson.name;
522
+ const raw = await fs.readFile(path.join(projectPath, 'package.json'), 'utf8');
523
+ const parsed = JSON.parse(raw);
524
+ return trimText(parsed?.name) || null;
525
+ } catch {
526
+ return null;
527
+ }
528
+ }
529
+
530
+ function lastPathSegment(projectPath) {
531
+ const normalized = path.normalize(projectPath);
532
+ const segment = path.basename(normalized);
533
+ return segment || normalized;
534
+ }
535
+
536
+ async function generateDisplayName(projectName, actualProjectDir = null) {
537
+ const preferredPath = actualProjectDir || decodeProjectNameFallback(projectName);
538
+ return (await readPackageDisplayName(preferredPath)) || lastPathSegment(preferredPath);
539
+ }
540
+
541
+ async function summarizeProjectDirectoryHistory(projectDirectoryPath) {
542
+ const cwdCounts = new Map();
543
+ let latestSeen = { cwd: null, timestamp: 0 };
544
+ const files = (await fs.readdir(projectDirectoryPath))
545
+ .filter((fileName) => fileName.endsWith('.jsonl') && !fileName.startsWith('agent-'));
546
+
547
+ for (const fileName of files) {
548
+ const stream = fsSync.createReadStream(path.join(projectDirectoryPath, fileName));
549
+ const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
550
+
551
+ for await (const line of lines) {
552
+ if (!line.trim()) {
553
+ continue;
554
+ }
555
+
556
+ try {
557
+ const entry = JSON.parse(line);
558
+ const cwd = trimText(entry?.cwd);
559
+ if (!cwd) {
560
+ continue;
561
+ }
562
+
563
+ cwdCounts.set(cwd, (cwdCounts.get(cwd) || 0) + 1);
564
+ const timestamp = Date.parse(entry?.timestamp || '') || 0;
565
+ if (timestamp >= latestSeen.timestamp) {
566
+ latestSeen = { cwd, timestamp };
567
+ }
568
+ } catch {
569
+ }
618
570
  }
619
- } catch (error) {
620
- // Fall back to path-based naming if package.json doesn't exist or can't be read
621
571
  }
622
-
623
- // If it starts with /, it's an absolute path
624
- if (projectPath.startsWith('/')) {
625
- const parts = projectPath.split('/').filter(Boolean);
626
- // Return only the last folder name
627
- return parts[parts.length - 1] || projectPath;
572
+
573
+ return { cwdCounts, latestSeen };
574
+ }
575
+
576
+ function chooseProjectDirectory(projectName, cwdCounts, latestSeen) {
577
+ if (cwdCounts.size === 0) {
578
+ return decodeProjectNameFallback(projectName);
628
579
  }
629
-
630
- return projectPath;
580
+
581
+ if (cwdCounts.size === 1) {
582
+ return Array.from(cwdCounts.keys())[0];
583
+ }
584
+
585
+ let topPath = null;
586
+ let topCount = -1;
587
+ for (const [cwd, count] of cwdCounts.entries()) {
588
+ if (count > topCount) {
589
+ topPath = cwd;
590
+ topCount = count;
591
+ }
592
+ }
593
+
594
+ if (latestSeen.cwd && (cwdCounts.get(latestSeen.cwd) || 0) >= Math.max(1, Math.floor(topCount / 4))) {
595
+ return latestSeen.cwd;
596
+ }
597
+
598
+ return topPath || decodeProjectNameFallback(projectName);
631
599
  }
632
600
 
633
- // Extract the actual project directory from JSONL sessions (with caching)
634
601
  async function extractProjectDirectory(projectName) {
635
- // Check cache first
636
602
  if (projectDirectoryCache.has(projectName)) {
637
603
  return projectDirectoryCache.get(projectName);
638
604
  }
639
605
 
640
- // Check project config for originalPath (manually added projects via UI or platform)
641
- // This handles projects with dashes in their directory names correctly
642
606
  const config = await loadProjectConfig();
643
- if (config[projectName]?.originalPath) {
644
- const originalPath = config[projectName].originalPath;
645
- projectDirectoryCache.set(projectName, originalPath);
646
- return originalPath;
607
+ const configuredPath = trimText(config?.[projectName]?.originalPath);
608
+ if (configuredPath) {
609
+ projectDirectoryCache.set(projectName, configuredPath);
610
+ return configuredPath;
647
611
  }
648
612
 
649
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
650
- const cwdCounts = new Map();
651
- let latestTimestamp = 0;
652
- let latestCwd = null;
653
- let extractedPath;
654
-
613
+ const projectDirectoryPath = path.join(os.homedir(), '.claude', 'projects', projectName);
614
+
655
615
  try {
656
- // Check if the project directory exists
657
- await fs.access(projectDir);
658
-
659
- const files = await fs.readdir(projectDir);
660
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
661
-
662
- if (jsonlFiles.length === 0) {
663
- // Fall back to decoded project name if no sessions
664
- extractedPath = projectName.replace(/-/g, '/');
665
- } else {
666
- // Process all JSONL files to collect cwd values
667
- for (const file of jsonlFiles) {
668
- const jsonlFile = path.join(projectDir, file);
669
- const fileStream = fsSync.createReadStream(jsonlFile);
670
- const rl = readline.createInterface({
671
- input: fileStream,
672
- crlfDelay: Infinity
673
- });
674
-
675
- for await (const line of rl) {
676
- if (line.trim()) {
677
- try {
678
- const entry = JSON.parse(line);
679
-
680
- if (entry.cwd) {
681
- // Count occurrences of each cwd
682
- cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) || 0) + 1);
683
-
684
- // Track the most recent cwd
685
- const timestamp = new Date(entry.timestamp || 0).getTime();
686
- if (timestamp > latestTimestamp) {
687
- latestTimestamp = timestamp;
688
- latestCwd = entry.cwd;
689
- }
690
- }
691
- } catch (parseError) {
692
- // Skip malformed lines
693
- }
694
- }
695
- }
696
- }
697
-
698
- // Determine the best cwd to use
699
- if (cwdCounts.size === 0) {
700
- // No cwd found, fall back to decoded project name
701
- extractedPath = projectName.replace(/-/g, '/');
702
- } else if (cwdCounts.size === 1) {
703
- // Only one cwd, use it
704
- extractedPath = Array.from(cwdCounts.keys())[0];
705
- } else {
706
- // Multiple cwd values - prefer the most recent one if it has reasonable usage
707
- const mostRecentCount = cwdCounts.get(latestCwd) || 0;
708
- const maxCount = Math.max(...cwdCounts.values());
709
-
710
- // Use most recent if it has at least 25% of the max count
711
- if (mostRecentCount >= maxCount * 0.25) {
712
- extractedPath = latestCwd;
713
- } else {
714
- // Otherwise use the most frequently used cwd
715
- for (const [cwd, count] of cwdCounts.entries()) {
716
- if (count === maxCount) {
717
- extractedPath = cwd;
718
- break;
719
- }
720
- }
721
- }
722
-
723
- // Fallback (shouldn't reach here)
724
- if (!extractedPath) {
725
- extractedPath = latestCwd || projectName.replace(/-/g, '/');
726
- }
727
- }
728
- }
729
-
730
- // Cache the result
731
- projectDirectoryCache.set(projectName, extractedPath);
732
-
733
- return extractedPath;
734
-
616
+ const { cwdCounts, latestSeen } = await summarizeProjectDirectoryHistory(projectDirectoryPath);
617
+ const resolvedPath = chooseProjectDirectory(projectName, cwdCounts, latestSeen);
618
+ projectDirectoryCache.set(projectName, resolvedPath);
619
+ return resolvedPath;
735
620
  } catch (error) {
736
- // If the directory doesn't exist, just use the decoded project name
737
- if (error.code === 'ENOENT') {
738
- extractedPath = projectName.replace(/-/g, '/');
739
- } else {
621
+ if (error.code !== 'ENOENT') {
740
622
  console.error(`Error extracting project directory for ${projectName}:`, error);
741
- // Fall back to decoded project name for other errors
742
- extractedPath = projectName.replace(/-/g, '/');
743
623
  }
744
-
745
- // Cache the fallback result too
746
- projectDirectoryCache.set(projectName, extractedPath);
747
-
748
- return extractedPath;
624
+
625
+ const fallbackPath = decodeProjectNameFallback(projectName);
626
+ projectDirectoryCache.set(projectName, fallbackPath);
627
+ return fallbackPath;
749
628
  }
750
629
  }
751
630
 
@@ -940,55 +819,70 @@ async function buildProjectFromDefinition(definition, providerLookups = null) {
940
819
  };
941
820
 
942
821
  try {
822
+ let claudeAcpSessions = [];
823
+ let codexAcpSessions = [];
824
+ let opencodeAcpSessions = [];
825
+ let geminiAcpSessions = [];
826
+
943
827
  if (providerLookups) {
944
- project.codexSessions = providerLookups.codexSessionsByProjectPath?.get(normalizedProjectPath) || [];
945
- project.opencodeSessions = providerLookups.opencodeSessionsByProjectPath?.get(normalizedProjectPath) || [];
946
- project.geminiSessions = providerLookups.geminiSessionsByProjectPath?.get(normalizedProjectPath) || [];
828
+ claudeAcpSessions = providerLookups.claudeAcpSessionsByProjectPath?.get(normalizedProjectPath) || [];
829
+ codexAcpSessions = providerLookups.codexAcpSessionsByProjectPath?.get(normalizedProjectPath) || [];
830
+ opencodeAcpSessions = providerLookups.opencodeAcpSessionsByProjectPath?.get(normalizedProjectPath) || [];
831
+ geminiAcpSessions = providerLookups.geminiAcpSessionsByProjectPath?.get(normalizedProjectPath) || [];
832
+
833
+ project.codexSessions = mergeSessionLists(
834
+ providerLookups.codexSessionsByProjectPath?.get(normalizedProjectPath) || [],
835
+ codexAcpSessions,
836
+ { fallbackProvider: 'codex' }
837
+ );
838
+ project.opencodeSessions = mergeSessionLists(
839
+ providerLookups.opencodeSessionsByProjectPath?.get(normalizedProjectPath) || [],
840
+ opencodeAcpSessions,
841
+ { fallbackProvider: 'opencode' }
842
+ );
843
+ project.geminiSessions = mergeSessionLists(
844
+ providerLookups.geminiSessionsByProjectPath?.get(normalizedProjectPath) || [],
845
+ geminiAcpSessions,
846
+ { fallbackProvider: 'gemini' }
847
+ );
947
848
  } else {
948
- const [codexSessions, opencodeSessions, geminiSessions] = await Promise.all([
849
+ const [codexSessions, opencodeSessions, geminiSessions, acpSessions] = await Promise.all([
949
850
  getCodexSessions(definition.fullPath, { limit: 5 }),
950
851
  getOpencodeSessions(definition.fullPath, { limit: 5 }),
951
- getGeminiSessions(definition.fullPath, { limit: 5 })
852
+ getGeminiSessions(definition.fullPath, { limit: 5 }),
853
+ listAcpSessions({ projectPath: definition.fullPath })
952
854
  ]);
953
- project.codexSessions = codexSessions;
954
- project.opencodeSessions = opencodeSessions;
955
- project.geminiSessions = geminiSessions;
855
+ claudeAcpSessions = acpSessions.filter((session) => session.provider === 'claude');
856
+ codexAcpSessions = acpSessions.filter((session) => session.provider === 'codex');
857
+ opencodeAcpSessions = acpSessions.filter((session) => session.provider === 'opencode');
858
+ geminiAcpSessions = acpSessions.filter((session) => session.provider === 'gemini');
859
+ project.codexSessions = mergeSessionLists(codexSessions, codexAcpSessions, { fallbackProvider: 'codex' });
860
+ project.opencodeSessions = mergeSessionLists(opencodeSessions, opencodeAcpSessions, { fallbackProvider: 'opencode' });
861
+ project.geminiSessions = mergeSessionLists(geminiSessions, geminiAcpSessions, { fallbackProvider: 'gemini' });
956
862
  }
957
863
 
958
864
  if (!definition.isManuallyAdded) {
959
865
  const sessionResult = await getSessions(definition.name, 5, 0);
960
- project.sessions = sessionResult.sessions || [];
866
+ project.sessions = mergeSessionLists(
867
+ (sessionResult.sessions || []).map((session) => ({
868
+ ...session,
869
+ provider: 'claude',
870
+ source: session?.source || 'legacy'
871
+ })),
872
+ claudeAcpSessions,
873
+ { fallbackProvider: 'claude' }
874
+ );
961
875
  project.sessionMeta = {
962
876
  hasMore: sessionResult.hasMore,
963
877
  total: sessionResult.total
964
878
  };
879
+ } else {
880
+ project.sessions = mergeSessionLists([], claudeAcpSessions, { fallbackProvider: 'claude' });
965
881
  }
966
882
  } catch (error) {
967
883
  console.warn(`Could not load session details for project ${definition.name}:`, error.message);
968
884
  }
969
885
 
970
- try {
971
- const taskMasterResult = await detectTaskMasterFolder(definition.fullPath);
972
- const taskMasterStatus = definition.isManuallyAdded
973
- ? (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'taskmaster-only' : 'not-configured')
974
- : (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'configured' : 'not-configured');
975
-
976
- project.taskmaster = {
977
- status: taskMasterStatus,
978
- hasTaskmaster: taskMasterResult.hasTaskmaster,
979
- hasEssentialFiles: taskMasterResult.hasEssentialFiles,
980
- metadata: taskMasterResult.metadata
981
- };
982
- } catch (error) {
983
- console.warn(`TaskMaster detection failed for project ${definition.name}:`, error.message);
984
- project.taskmaster = {
985
- status: 'error',
986
- hasTaskmaster: false,
987
- hasEssentialFiles: false,
988
- error: error.message
989
- };
990
- }
991
-
992
886
  return project;
993
887
  }
994
888
 
@@ -1013,17 +907,25 @@ async function getProjects(progressCallback = null) {
1013
907
  .filter(Boolean)
1014
908
  ));
1015
909
 
1016
- const [codexSessionsByProjectPath, geminiSessionsByProjectPath, opencodeSessionsByProjectPath] = await Promise.all([
910
+ const [codexSessionsByProjectPath, geminiSessionsByProjectPath, opencodeSessionsByProjectPath, claudeAcpSessionsByProjectPath, codexAcpSessionsByProjectPath, geminiAcpSessionsByProjectPath, opencodeAcpSessionsByProjectPath] = await Promise.all([
1017
911
  buildCodexSessionsLookup(uniqueProjectPaths, { limit: 5 }),
1018
912
  buildGeminiSessionsLookup(uniqueProjectPaths, { limit: 5 }),
1019
- buildOpencodeSessionsLookup(uniqueProjectPaths, { limit: 5 })
913
+ buildOpencodeSessionsLookup(uniqueProjectPaths, { limit: 5 }),
914
+ buildAcpProviderSessionsLookup('claude', uniqueProjectPaths, { limit: 5 }),
915
+ buildAcpProviderSessionsLookup('codex', uniqueProjectPaths, { limit: 5 }),
916
+ buildAcpProviderSessionsLookup('gemini', uniqueProjectPaths, { limit: 5 }),
917
+ buildAcpProviderSessionsLookup('opencode', uniqueProjectPaths, { limit: 5 })
1020
918
  ]);
1021
919
 
1022
920
  for (const definition of projectDefinitions) {
1023
921
  projects.push(await buildProjectFromDefinition(definition, {
922
+ claudeAcpSessionsByProjectPath,
1024
923
  codexSessionsByProjectPath,
924
+ codexAcpSessionsByProjectPath,
925
+ geminiSessionsByProjectPath,
926
+ geminiAcpSessionsByProjectPath,
1025
927
  opencodeSessionsByProjectPath,
1026
- geminiSessionsByProjectPath
928
+ opencodeAcpSessionsByProjectPath
1027
929
  }));
1028
930
  }
1029
931
 
@@ -1039,131 +941,149 @@ async function getProjects(progressCallback = null) {
1039
941
  return projects;
1040
942
  }
1041
943
 
944
+ function createClaudeSessionRecord(sessionId) {
945
+ return {
946
+ id: sessionId,
947
+ summary: 'New Session',
948
+ messageCount: 0,
949
+ lastActivity: new Date(0),
950
+ cwd: '',
951
+ lastUserMessage: null,
952
+ lastAssistantMessage: null,
953
+ rootMessageId: null
954
+ };
955
+ }
956
+
957
+ function truncateSummary(text, maxLength = 50) {
958
+ const normalized = trimText(text);
959
+ if (!normalized) {
960
+ return 'New Session';
961
+ }
962
+
963
+ return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
964
+ }
965
+
966
+ function extractClaudeTextContent(content) {
967
+ if (typeof content === 'string') {
968
+ return content;
969
+ }
970
+
971
+ if (Array.isArray(content)) {
972
+ return content
973
+ .filter((part) => part?.type === 'text' && typeof part?.text === 'string')
974
+ .map((part) => part.text)
975
+ .join('\n')
976
+ .trim();
977
+ }
978
+
979
+ return '';
980
+ }
981
+
982
+ function isIgnoredClaudePrompt(text) {
983
+ return text.startsWith('<command-name>')
984
+ || text.startsWith('<command-message>')
985
+ || text.startsWith('<command-args>')
986
+ || text.startsWith('<local-command-stdout>')
987
+ || text.startsWith('<system-reminder>')
988
+ || text.startsWith('Caveat:')
989
+ || text.startsWith('This session is being continued from a previous')
990
+ || text.startsWith('Invalid API key')
991
+ || text.includes('{"subtasks":')
992
+ || text.includes('CRITICAL: You MUST respond with ONLY a JSON')
993
+ || text === 'Warmup';
994
+ }
995
+
996
+ function mergeClaudeSession(target, incoming) {
997
+ if (incoming.messageCount > target.messageCount) {
998
+ target.messageCount = incoming.messageCount;
999
+ }
1000
+
1001
+ if (incoming.summary !== 'New Session' && (target.summary === 'New Session' || incoming.lastActivity > target.lastActivity)) {
1002
+ target.summary = incoming.summary;
1003
+ }
1004
+
1005
+ if (incoming.lastActivity > target.lastActivity) {
1006
+ target.lastActivity = incoming.lastActivity;
1007
+ target.lastUserMessage = incoming.lastUserMessage || target.lastUserMessage;
1008
+ target.lastAssistantMessage = incoming.lastAssistantMessage || target.lastAssistantMessage;
1009
+ }
1010
+
1011
+ if (!target.cwd && incoming.cwd) {
1012
+ target.cwd = incoming.cwd;
1013
+ }
1014
+
1015
+ if (!target.rootMessageId && incoming.rootMessageId) {
1016
+ target.rootMessageId = incoming.rootMessageId;
1017
+ }
1018
+ }
1019
+
1042
1020
  async function getSessions(projectName, limit = 5, offset = 0) {
1043
1021
  const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1044
1022
 
1045
1023
  try {
1046
- const files = await fs.readdir(projectDir);
1047
- // agent-*.jsonl files contain session start data at this point. This needs to be revisited
1048
- // periodically to make sure only accurate data is there and no new functionality is added there
1049
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
1050
-
1051
- if (jsonlFiles.length === 0) {
1052
- return { sessions: [], hasMore: false, total: 0 };
1024
+ const files = (await fs.readdir(projectDir))
1025
+ .filter((fileName) => fileName.endsWith('.jsonl') && !fileName.startsWith('agent-'));
1026
+
1027
+ if (files.length === 0) {
1028
+ return { sessions: [], hasMore: false, total: 0, offset, limit };
1053
1029
  }
1054
-
1055
- // Sort files by modification time (newest first)
1056
- const filesWithStats = await Promise.all(
1057
- jsonlFiles.map(async (file) => {
1058
- const filePath = path.join(projectDir, file);
1059
- const stats = await fs.stat(filePath);
1060
- return { file, mtime: stats.mtime };
1061
- })
1062
- );
1063
- filesWithStats.sort((a, b) => b.mtime - a.mtime);
1064
-
1065
- const allSessions = new Map();
1066
- const allEntries = [];
1067
- const uuidToSessionMap = new Map();
1068
-
1069
- // Collect all sessions and entries from all files
1070
- for (const { file } of filesWithStats) {
1071
- const jsonlFile = path.join(projectDir, file);
1072
- const result = await parseJsonlSessions(jsonlFile);
1073
-
1074
- result.sessions.forEach(session => {
1075
- if (!allSessions.has(session.id)) {
1076
- allSessions.set(session.id, session);
1030
+
1031
+ const filesByRecency = await Promise.all(files.map(async (fileName) => {
1032
+ const filePath = path.join(projectDir, fileName);
1033
+ const stat = await fs.stat(filePath);
1034
+ return { filePath, modifiedAt: stat.mtime.getTime() };
1035
+ }));
1036
+ filesByRecency.sort((left, right) => right.modifiedAt - left.modifiedAt);
1037
+
1038
+ const sessionsById = new Map();
1039
+
1040
+ for (const { filePath } of filesByRecency) {
1041
+ const parsed = await parseJsonlSessions(filePath);
1042
+ for (const session of parsed.sessions) {
1043
+ const existing = sessionsById.get(session.id);
1044
+ if (!existing) {
1045
+ sessionsById.set(session.id, session);
1046
+ continue;
1077
1047
  }
1078
- });
1079
-
1080
- allEntries.push(...result.entries);
1081
-
1082
- // Early exit optimization for large projects
1083
- if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
1084
- break;
1048
+ mergeClaudeSession(existing, session);
1085
1049
  }
1086
1050
  }
1087
-
1088
- // Build UUID-to-session mapping for timeline detection
1089
- allEntries.forEach(entry => {
1090
- if (entry.uuid && entry.sessionId) {
1091
- uuidToSessionMap.set(entry.uuid, entry.sessionId);
1092
- }
1093
- });
1094
-
1095
- // Group sessions by first user message ID
1096
- const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
1097
- const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId
1098
-
1099
- // Find the first user message for each session
1100
- allEntries.forEach(entry => {
1101
- if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) {
1102
- // This is a first user message in a session (parentUuid is null)
1103
- const firstUserMsgId = entry.uuid;
1104
-
1105
- if (!sessionToFirstUserMsgId.has(entry.sessionId)) {
1106
- sessionToFirstUserMsgId.set(entry.sessionId, firstUserMsgId);
1107
-
1108
- const session = allSessions.get(entry.sessionId);
1109
- if (session) {
1110
- if (!sessionGroups.has(firstUserMsgId)) {
1111
- sessionGroups.set(firstUserMsgId, {
1112
- latestSession: session,
1113
- allSessions: [session]
1114
- });
1115
- } else {
1116
- const group = sessionGroups.get(firstUserMsgId);
1117
- group.allSessions.push(session);
1118
1051
 
1119
- // Update latest session if this one is more recent
1120
- if (new Date(session.lastActivity) > new Date(group.latestSession.lastActivity)) {
1121
- group.latestSession = session;
1122
- }
1123
- }
1124
- }
1125
- }
1126
- }
1127
- });
1052
+ const groupedSessions = new Map();
1128
1053
 
1129
- // Collect all sessions that don't belong to any group (standalone sessions)
1130
- const groupedSessionIds = new Set();
1131
- sessionGroups.forEach(group => {
1132
- group.allSessions.forEach(session => groupedSessionIds.add(session.id));
1133
- });
1054
+ for (const session of sessionsById.values()) {
1055
+ const groupKey = session.rootMessageId || `session:${session.id}`;
1056
+ const currentGroup = groupedSessions.get(groupKey) || [];
1057
+ currentGroup.push(session);
1058
+ groupedSessions.set(groupKey, currentGroup);
1059
+ }
1060
+
1061
+ const visibleSessions = Array.from(groupedSessions.values()).map((group) => {
1062
+ const ordered = [...group].sort((left, right) => right.lastActivity - left.lastActivity);
1063
+ const primary = { ...ordered[0] };
1134
1064
 
1135
- const standaloneSessionsArray = Array.from(allSessions.values())
1136
- .filter(session => !groupedSessionIds.has(session.id));
1137
-
1138
- // Combine grouped sessions (only show latest from each group) + standalone sessions
1139
- const latestFromGroups = Array.from(sessionGroups.values()).map(group => {
1140
- const session = { ...group.latestSession };
1141
- // Add metadata about grouping
1142
- if (group.allSessions.length > 1) {
1143
- session.isGrouped = true;
1144
- session.groupSize = group.allSessions.length;
1145
- session.groupSessions = group.allSessions.map(s => s.id);
1065
+ if (ordered.length > 1) {
1066
+ primary.isGrouped = true;
1067
+ primary.groupSize = ordered.length;
1068
+ primary.groupSessions = ordered.map((session) => session.id);
1146
1069
  }
1147
- return session;
1148
- });
1149
- const visibleSessions = [...latestFromGroups, ...standaloneSessionsArray]
1150
- .filter(session => !session.summary.startsWith('{ "'))
1151
- .sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
1070
+
1071
+ return primary;
1072
+ }).sort((left, right) => right.lastActivity - left.lastActivity);
1152
1073
 
1153
1074
  const total = visibleSessions.length;
1154
- const paginatedSessions = visibleSessions.slice(offset, offset + limit);
1155
- const hasMore = offset + limit < total;
1156
-
1075
+ const sessions = visibleSessions.slice(offset, offset + limit);
1076
+
1157
1077
  return {
1158
- sessions: paginatedSessions,
1159
- hasMore,
1078
+ sessions,
1079
+ hasMore: offset + limit < total,
1160
1080
  total,
1161
1081
  offset,
1162
1082
  limit
1163
1083
  };
1164
1084
  } catch (error) {
1165
1085
  console.error(`Error reading sessions for project ${projectName}:`, error);
1166
- return { sessions: [], hasMore: false, total: 0 };
1086
+ return { sessions: [], hasMore: false, total: 0, offset, limit };
1167
1087
  }
1168
1088
  }
1169
1089
 
@@ -1207,159 +1127,117 @@ async function getClaudeSessionMetadata(sessionId) {
1207
1127
  async function parseJsonlSessions(filePath) {
1208
1128
  const sessions = new Map();
1209
1129
  const entries = [];
1210
- const pendingSummaries = new Map(); // leafUuid -> summary for entries without sessionId
1130
+ const pendingSummaries = new Map();
1211
1131
 
1212
1132
  try {
1213
- const fileStream = fsSync.createReadStream(filePath);
1214
- const rl = readline.createInterface({
1215
- input: fileStream,
1216
- crlfDelay: Infinity
1217
- });
1218
-
1219
- for await (const line of rl) {
1220
- if (line.trim()) {
1221
- try {
1222
- const entry = JSON.parse(line);
1223
- entries.push(entry);
1224
-
1225
- // Handle summary entries that don't have sessionId yet
1226
- if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
1227
- pendingSummaries.set(entry.leafUuid, entry.summary);
1228
- }
1229
-
1230
- if (entry.sessionId) {
1231
- if (!sessions.has(entry.sessionId)) {
1232
- sessions.set(entry.sessionId, {
1233
- id: entry.sessionId,
1234
- summary: 'New Session',
1235
- messageCount: 0,
1236
- lastActivity: new Date(),
1237
- cwd: entry.cwd || '',
1238
- lastUserMessage: null,
1239
- lastAssistantMessage: null
1240
- });
1241
- }
1133
+ const stream = fsSync.createReadStream(filePath);
1134
+ const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
1242
1135
 
1243
- const session = sessions.get(entry.sessionId);
1136
+ for await (const line of lines) {
1137
+ if (!line.trim()) {
1138
+ continue;
1139
+ }
1244
1140
 
1245
- // Apply pending summary if this entry has a parentUuid that matches a pending summary
1246
- if (session.summary === 'New Session' && entry.parentUuid && pendingSummaries.has(entry.parentUuid)) {
1247
- session.summary = pendingSummaries.get(entry.parentUuid);
1248
- }
1141
+ try {
1142
+ const entry = JSON.parse(line);
1143
+ entries.push(entry);
1249
1144
 
1250
- // Update summary from summary entries with sessionId
1251
- if (entry.type === 'summary' && entry.summary) {
1252
- session.summary = entry.summary;
1253
- }
1145
+ if (entry.type === 'summary' && trimText(entry.summary) && !entry.sessionId && trimText(entry.leafUuid)) {
1146
+ pendingSummaries.set(entry.leafUuid, trimText(entry.summary));
1147
+ }
1254
1148
 
1255
- // Track last user and assistant messages (skip system messages)
1256
- if (entry.message?.role === 'user' && entry.message?.content) {
1257
- const content = entry.message.content;
1149
+ const sessionId = trimText(entry.sessionId);
1150
+ if (!sessionId) {
1151
+ continue;
1152
+ }
1258
1153
 
1259
- // Extract text from array format if needed
1260
- let textContent = content;
1261
- if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
1262
- textContent = content[0].text;
1263
- }
1154
+ const session = sessions.get(sessionId) || createClaudeSessionRecord(sessionId);
1155
+ sessions.set(sessionId, session);
1264
1156
 
1265
- const isSystemMessage = typeof textContent === 'string' && (
1266
- textContent.startsWith('<command-name>') ||
1267
- textContent.startsWith('<command-message>') ||
1268
- textContent.startsWith('<command-args>') ||
1269
- textContent.startsWith('<local-command-stdout>') ||
1270
- textContent.startsWith('<system-reminder>') ||
1271
- textContent.startsWith('Caveat:') ||
1272
- textContent.startsWith('This session is being continued from a previous') ||
1273
- textContent.startsWith('Invalid API key') ||
1274
- textContent.includes('{"subtasks":') || // Filter Task Master prompts
1275
- textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') || // Filter Task Master system prompts
1276
- textContent === 'Warmup' // Explicitly filter out "Warmup"
1277
- );
1278
-
1279
- if (typeof textContent === 'string' && textContent.length > 0 && !isSystemMessage) {
1280
- session.lastUserMessage = textContent;
1281
- }
1282
- } else if (entry.message?.role === 'assistant' && entry.message?.content) {
1283
- // Skip API error messages using the isApiErrorMessage flag
1284
- if (entry.isApiErrorMessage === true) {
1285
- // Skip this message entirely
1286
- } else {
1287
- // Track last assistant text message
1288
- let assistantText = null;
1289
-
1290
- if (Array.isArray(entry.message.content)) {
1291
- for (const part of entry.message.content) {
1292
- if (part.type === 'text' && part.text) {
1293
- assistantText = part.text;
1294
- }
1295
- }
1296
- } else if (typeof entry.message.content === 'string') {
1297
- assistantText = entry.message.content;
1298
- }
1157
+ if (!session.cwd && trimText(entry.cwd)) {
1158
+ session.cwd = trimText(entry.cwd);
1159
+ }
1299
1160
 
1300
- // Additional filter for assistant messages with system content
1301
- const isSystemAssistantMessage = typeof assistantText === 'string' && (
1302
- assistantText.startsWith('Invalid API key') ||
1303
- assistantText.includes('{"subtasks":') ||
1304
- assistantText.includes('CRITICAL: You MUST respond with ONLY a JSON')
1305
- );
1161
+ if (trimText(entry.timestamp)) {
1162
+ session.lastActivity = new Date(entry.timestamp);
1163
+ }
1306
1164
 
1307
- if (assistantText && !isSystemAssistantMessage) {
1308
- session.lastAssistantMessage = assistantText;
1309
- }
1310
- }
1311
- }
1165
+ if (entry.type === 'summary' && trimText(entry.summary)) {
1166
+ session.summary = trimText(entry.summary);
1167
+ } else if (session.summary === 'New Session' && trimText(entry.parentUuid) && pendingSummaries.has(entry.parentUuid)) {
1168
+ session.summary = pendingSummaries.get(entry.parentUuid);
1169
+ }
1312
1170
 
1313
- session.messageCount++;
1171
+ if (!session.rootMessageId && entry.type === 'user' && entry.parentUuid === null && trimText(entry.uuid)) {
1172
+ session.rootMessageId = trimText(entry.uuid);
1173
+ }
1314
1174
 
1315
- if (entry.timestamp) {
1316
- session.lastActivity = new Date(entry.timestamp);
1317
- }
1175
+ if (entry.message?.role === 'user') {
1176
+ const text = extractClaudeTextContent(entry.message.content);
1177
+ if (text && !isIgnoredClaudePrompt(text)) {
1178
+ session.lastUserMessage = text;
1179
+ }
1180
+ } else if (entry.message?.role === 'assistant' && entry.isApiErrorMessage !== true) {
1181
+ const text = extractClaudeTextContent(entry.message.content);
1182
+ if (text && !isIgnoredClaudePrompt(text)) {
1183
+ session.lastAssistantMessage = text;
1318
1184
  }
1319
- } catch (parseError) {
1320
- // Skip malformed lines silently
1321
1185
  }
1322
- }
1323
- }
1324
1186
 
1325
- // After processing all entries, set final summary based on last message if no summary exists
1326
- for (const session of sessions.values()) {
1327
- if (session.summary === 'New Session') {
1328
- // Prefer last user message, fall back to last assistant message
1329
- const lastMessage = session.lastUserMessage || session.lastAssistantMessage;
1330
- if (lastMessage) {
1331
- session.summary = lastMessage.length > 50 ? lastMessage.substring(0, 50) + '...' : lastMessage;
1332
- }
1187
+ session.messageCount += 1;
1188
+ } catch {
1333
1189
  }
1334
1190
  }
1335
1191
 
1336
- // Filter out sessions that contain JSON responses (Task Master errors)
1337
- const allSessions = Array.from(sessions.values());
1338
- const filteredSessions = allSessions.filter(session => {
1339
- const shouldFilter = session.summary.startsWith('{ "');
1340
- if (shouldFilter) {
1341
- }
1342
- // Log a sample of summaries to debug
1343
- if (Math.random() < 0.01) { // Log 1% of sessions
1344
- }
1345
- return !shouldFilter;
1346
- });
1192
+ const normalizedSessions = Array.from(sessions.values())
1193
+ .map((session) => {
1194
+ if (session.summary === 'New Session') {
1195
+ session.summary = truncateSummary(session.lastUserMessage || session.lastAssistantMessage || 'New Session');
1196
+ } else {
1197
+ session.summary = truncateSummary(session.summary, 80);
1198
+ }
1347
1199
 
1200
+ return session;
1201
+ })
1202
+ .filter((session) => !session.summary.startsWith('{ "'));
1348
1203
 
1349
1204
  return {
1350
- sessions: filteredSessions,
1351
- entries: entries
1205
+ sessions: normalizedSessions,
1206
+ entries
1352
1207
  };
1353
-
1354
1208
  } catch (error) {
1355
1209
  console.error('Error reading JSONL file:', error);
1356
1210
  return { sessions: [], entries: [] };
1357
1211
  }
1358
1212
  }
1359
1213
 
1214
+ async function resolveClaudeProjectDir(projectName, sessionId = null) {
1215
+ const defaultProjectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1216
+
1217
+ try {
1218
+ await fs.access(defaultProjectDir);
1219
+ return defaultProjectDir;
1220
+ } catch (error) {
1221
+ if (error?.code !== 'ENOENT') {
1222
+ throw error;
1223
+ }
1224
+ }
1225
+
1226
+ if (!sessionId) {
1227
+ return defaultProjectDir;
1228
+ }
1229
+
1230
+ const sessionMetadata = await getClaudeSessionMetadata(sessionId);
1231
+ if (sessionMetadata?.filePath) {
1232
+ return path.dirname(sessionMetadata.filePath);
1233
+ }
1234
+
1235
+ return defaultProjectDir;
1236
+ }
1237
+
1360
1238
  // Get messages for a specific session with pagination support
1361
1239
  async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
1362
- const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1240
+ const projectDir = await resolveClaudeProjectDir(projectName, sessionId);
1363
1241
 
1364
1242
  try {
1365
1243
  const files = await fs.readdir(projectDir);
@@ -2279,6 +2157,24 @@ async function buildCodexSessionsLookup(projectPaths, options = {}) {
2279
2157
  });
2280
2158
  }
2281
2159
 
2160
+ async function buildAcpProviderSessionsLookup(provider, projectPaths, options = {}) {
2161
+ const { limit = 5 } = options;
2162
+ const normalizedProjectPaths = Array.from(new Set(
2163
+ (projectPaths || []).map(normalizeComparableProjectPath).filter(Boolean)
2164
+ ));
2165
+
2166
+ if (normalizedProjectPaths.length === 0) {
2167
+ return new Map();
2168
+ }
2169
+
2170
+ const sessions = await listAcpSessions({ provider });
2171
+ return groupSessionsByNormalizedProjectPath(
2172
+ normalizedProjectPaths,
2173
+ sessions.map((session) => ({ ...session, provider, source: 'acp' })),
2174
+ limit
2175
+ );
2176
+ }
2177
+
2282
2178
  async function buildOpencodeSessionsLookup(projectPaths, options = {}) {
2283
2179
  const { limit = 5 } = options;
2284
2180
  const normalizedProjectPaths = Array.from(new Set(
@@ -2501,6 +2397,15 @@ async function parseCodexSessionFile(filePath) {
2501
2397
  }
2502
2398
  }
2503
2399
 
2400
+ if (shouldIncludeCodexTranscriptMessage(entry) && String(entry.payload.role).trim().toLowerCase() === 'user') {
2401
+ const visibleUserMessage = extractVisibleUserMessage(extractCodexMessageText(entry.payload.content));
2402
+ if (!visibleUserMessage) {
2403
+ continue;
2404
+ }
2405
+ messageCount++;
2406
+ lastUserMessage = visibleUserMessage;
2407
+ }
2408
+
2504
2409
  if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') {
2505
2410
  messageCount++;
2506
2411
  }
@@ -2554,23 +2459,6 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2554
2459
  crlfDelay: Infinity
2555
2460
  });
2556
2461
 
2557
- // Helper to extract text from Codex content array
2558
- const extractText = (content) => {
2559
- if (!Array.isArray(content)) return content;
2560
- return content
2561
- .map(item => {
2562
- if (item.type === 'input_text' || item.type === 'output_text') {
2563
- return item.text;
2564
- }
2565
- if (item.type === 'text') {
2566
- return item.text;
2567
- }
2568
- return '';
2569
- })
2570
- .filter(Boolean)
2571
- .join('\n');
2572
- };
2573
-
2574
2462
  const appendMessage = (message) => {
2575
2463
  total += 1;
2576
2464
  appendBoundedMessage(messages, message, maxBufferedMessages);
@@ -2587,10 +2475,10 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2587
2475
  }
2588
2476
 
2589
2477
  // Extract messages from response_item
2590
- if (entry.type === 'response_item' && entry.payload?.type === 'message') {
2478
+ if (shouldIncludeCodexTranscriptMessage(entry)) {
2591
2479
  const content = entry.payload.content;
2592
2480
  const role = entry.payload.role || 'assistant';
2593
- const textContent = extractText(content);
2481
+ const textContent = extractCodexMessageText(content);
2594
2482
  const visibleTextContent = role === 'user'
2595
2483
  ? extractVisibleUserMessage(textContent)
2596
2484
  : textContent;