@axhub/genie 0.2.11 → 0.2.12

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-Clb2COtW.js +274 -0
  3. package/dist/assets/ImagePlaygroundPage-DqhMSbM8.js +106 -0
  4. package/dist/assets/ImagePlaygroundPage-MEn3NN80.css +1 -0
  5. package/dist/assets/ReviewApp-CDcLYe-u.js +1 -0
  6. package/dist/assets/{_basePickBy-BDnj7-0Z.js → _basePickBy-jUZsM51q.js} +1 -1
  7. package/dist/assets/{_baseUniq-Bl0JKOyl.js → _baseUniq-BXglE6_v.js} +1 -1
  8. package/dist/assets/{arc-DY-4Kev3.js → arc-D-oFCFBv.js} +1 -1
  9. package/dist/assets/{architectureDiagram-2XIMDMQ5-qw7crNVd.js → architectureDiagram-2XIMDMQ5-DC8bAnQt.js} +1 -1
  10. package/dist/assets/{blockDiagram-WCTKOSBZ-B9xg7ep3.js → blockDiagram-WCTKOSBZ-C4semIRc.js} +1 -1
  11. package/dist/assets/{c4Diagram-IC4MRINW-H9xp3ytb.js → c4Diagram-IC4MRINW-FHj1QO3y.js} +1 -1
  12. package/dist/assets/channel-BF4woPXX.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-B3EVDUxI.js → chunk-4BX2VUAB-D-LjsQ_s.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-CGv945ef.js → chunk-55IACEB6-DI3j_d7A.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-uAT4CKWM.js → chunk-FMBD7UC4-BEVnaLFN.js} +1 -1
  16. package/dist/assets/{chunk-JSJVCQXG-Cbvlpkf7.js → chunk-JSJVCQXG-CSxpcErk.js} +1 -1
  17. package/dist/assets/{chunk-KX2RTZJC-CcqIuGat.js → chunk-KX2RTZJC-BbuhDN4h.js} +1 -1
  18. package/dist/assets/{chunk-NQ4KR5QH-CgrcsRuX.js → chunk-NQ4KR5QH-C3x61XQa.js} +1 -1
  19. package/dist/assets/{chunk-QZHKN3VN-Cx0APOoV.js → chunk-QZHKN3VN-DxWOFtPh.js} +1 -1
  20. package/dist/assets/{chunk-WL4C6EOR-BbZirvBk.js → chunk-WL4C6EOR-Bt2OauD2.js} +1 -1
  21. package/dist/assets/classDiagram-VBA2DB6C-D2kHlnQ7.js +1 -0
  22. package/dist/assets/classDiagram-v2-RAHNMMFH-D2kHlnQ7.js +1 -0
  23. package/dist/assets/clone-CqBvwCJW.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-CrvmGFLD.js → cose-bilkent-S5V4N54A-Dexadrue.js} +1 -1
  25. package/dist/assets/{dagre-KLK3FWXG-C-W6VPjS.js → dagre-KLK3FWXG-F9U4X2xC.js} +1 -1
  26. package/dist/assets/{diagram-E7M64L7V-IP2q3bL0.js → diagram-E7M64L7V-B3V17aH3.js} +1 -1
  27. package/dist/assets/{diagram-IFDJBPK2-CQaL-XyV.js → diagram-IFDJBPK2-CdHAmLL1.js} +1 -1
  28. package/dist/assets/{diagram-P4PSJMXO-BxBLThfv.js → diagram-P4PSJMXO-CrTNfk8K.js} +1 -1
  29. package/dist/assets/{erDiagram-INFDFZHY-Dyl7bJTt.js → erDiagram-INFDFZHY-vDh9SWK9.js} +1 -1
  30. package/dist/assets/{flowDiagram-PKNHOUZH-B7NFMgFK.js → flowDiagram-PKNHOUZH-DpltMg7L.js} +1 -1
  31. package/dist/assets/{ganttDiagram-A5KZAMGK-hReWSDu2.js → ganttDiagram-A5KZAMGK-COTk2xur.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-gVgcr0ST.js → gitGraphDiagram-K3NZZRJ6-BNV7bvvj.js} +1 -1
  33. package/dist/assets/{graph-DNDiJhTn.js → graph-Dkeg9oys.js} +1 -1
  34. package/dist/assets/{highlighted-body-TPN3WLV5-DclLmTou.js → highlighted-body-TPN3WLV5-DaiQEBwR.js} +1 -1
  35. package/dist/assets/index-DgGmiqsP.css +1 -0
  36. package/dist/assets/index-DvA901Vs.js +2 -0
  37. package/dist/assets/{infoDiagram-LFFYTUFH-CqQOOzDA.js → infoDiagram-LFFYTUFH-CZioW3Gt.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-PHBUUO56-CZ0iLiHg.js → ishikawaDiagram-PHBUUO56-BbqR3i1B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-4ABVD52K-DdfYKfNh.js → journeyDiagram-4ABVD52K-wfb-WHzl.js} +1 -1
  40. package/dist/assets/{kanban-definition-K7BYSVSG-C5Vf32u6.js → kanban-definition-K7BYSVSG-B3c4y3VN.js} +1 -1
  41. package/dist/assets/{layout-rvTEu2KS.js → layout-Xr9Z2VGF.js} +1 -1
  42. package/dist/assets/{linear-CD9SiYze.js → linear-JBmzAJtl.js} +1 -1
  43. package/dist/assets/{mermaid-O7DHMXV3-OZ8qWWwa.js → mermaid-O7DHMXV3-fDuyNLKe.js} +230 -222
  44. package/dist/assets/{mindmap-definition-YRQLILUH-CQxrLNVc.js → mindmap-definition-YRQLILUH-B5NTN_jD.js} +1 -1
  45. package/dist/assets/{pieDiagram-SKSYHLDU-XgAUByWg.js → pieDiagram-SKSYHLDU-CuO98GVu.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-337W2JSQ-CH16ls7G.js → quadrantDiagram-337W2JSQ-LL3f4vLf.js} +1 -1
  47. package/dist/assets/{requirementDiagram-Z7DCOOCP-B_kQO06L.js → requirementDiagram-Z7DCOOCP-Di-2O6LH.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-WA2Y5GQK-ofe78CyS.js → sankeyDiagram-WA2Y5GQK-9lHqrXqR.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-2WXFIKYE-Ckbxwny6.js → sequenceDiagram-2WXFIKYE-BQu-SoGr.js} +1 -1
  50. package/dist/assets/{stateDiagram-RAJIS63D-DNtzCk14.js → stateDiagram-RAJIS63D-BUxvd2BC.js} +1 -1
  51. package/dist/assets/stateDiagram-v2-FVOUBMTO-CDVexTiR.js +1 -0
  52. package/dist/assets/{timeline-definition-YZTLITO2-zT6CklKt.js → timeline-definition-YZTLITO2-oP47UEU6.js} +1 -1
  53. package/dist/assets/{treemap-KZPCXAKY-y0U2c3xG.js → treemap-KZPCXAKY-BRjDo2aE.js} +1 -1
  54. package/dist/assets/{vendor-codemirror-CMHSJ_9p.js → vendor-codemirror-BiCeS-y4.js} +1 -1
  55. package/dist/assets/{vendor-react-xmA_f8ig.js → vendor-react-DVlYPmi3.js} +1 -1
  56. package/dist/assets/{vennDiagram-LZ73GAT5-xKj3SjYG.js → vennDiagram-LZ73GAT5-DrRqcDqo.js} +1 -1
  57. package/dist/assets/{xychartDiagram-JWTSCODW-Da_qyEoX.js → xychartDiagram-JWTSCODW-DUXrymAi.js} +1 -1
  58. package/dist/index.html +4 -4
  59. package/package.json +25 -6
  60. package/scripts/refresh-acp-default-capabilities.mjs +160 -0
  61. package/server/acp-runtime/client.js +1137 -181
  62. package/server/acp-runtime/command-overrides.js +48 -0
  63. package/server/acp-runtime/index.js +576 -16
  64. package/server/acp-runtime/registry.js +6 -4
  65. package/server/acp-runtime/session-store.js +235 -92
  66. package/server/database/db.js +12 -3
  67. package/server/external-agent/ws.js +212 -11
  68. package/server/index.js +145 -52
  69. package/server/projects-watcher-config.js +4 -0
  70. package/server/projects.js +466 -125
  71. package/server/routes/cc-connect.js +5 -4
  72. package/server/routes/codex.js +24 -0
  73. package/server/routes/commands.js +144 -1
  74. package/server/routes/runs.js +641 -0
  75. package/server/routes/session-core.js +357 -109
  76. package/server/session-core/eventStore.js +0 -121
  77. package/server/session-core/providerAdapters.js +644 -163
  78. package/server/session-core/providerDiscovery.js +66 -38
  79. package/server/session-core/runRegistry.js +244 -0
  80. package/server/session-core/runtimeState.js +75 -3
  81. package/server/session-core/runtimeWriter.js +132 -10
  82. package/server/utils/codexImagePlayground.js +479 -0
  83. package/server/utils/localTerminal.js +56 -0
  84. package/server/utils/shellCommand.js +70 -0
  85. package/shared/acpCapabilities.js +393 -0
  86. package/shared/acpDefaultCapabilities.generated.json +141 -0
  87. package/shared/conversationEvents.js +425 -121
  88. package/dist/assets/App-VH1wNUHs.js +0 -259
  89. package/dist/assets/ReviewApp-D_9EN4TM.js +0 -1
  90. package/dist/assets/channel-CyNUnRfc.js +0 -1
  91. package/dist/assets/classDiagram-VBA2DB6C-DxBtyz2A.js +0 -1
  92. package/dist/assets/classDiagram-v2-RAHNMMFH-DxBtyz2A.js +0 -1
  93. package/dist/assets/clone-C341l3d0.js +0 -1
  94. package/dist/assets/index-DBkz_W_P.css +0 -1
  95. package/dist/assets/index-DdRyoXKh.js +0 -2
  96. package/dist/assets/stateDiagram-v2-FVOUBMTO-B3VPhiE1.js +0 -1
@@ -51,10 +51,11 @@ import os from 'os';
51
51
  import path from 'path';
52
52
  import readline from 'readline';
53
53
  import crypto from 'crypto';
54
+ import { spawn } from 'child_process';
54
55
  import { fileURLToPath } from 'url';
55
56
  import { parseCodexTokenCountInfo } from './utils/codexTokenUsage.js';
56
57
  import { CODEX_MODELS } from '../shared/modelConstants.js';
57
- import { listAcpSessions } from './acp-runtime/session-store.js';
58
+ import { deleteAcpSessionRecord, listAcpSessions } from './acp-runtime/session-store.js';
58
59
  import { mergeSessionLists } from './session-core/sessionListMerge.js';
59
60
 
60
61
  const __filename = fileURLToPath(import.meta.url);
@@ -244,6 +245,7 @@ const projectListCache = {
244
245
  expiresAt: 0
245
246
  };
246
247
  const codexSessionFileCache = new Map();
248
+ let codexAppServerClientOverride = null;
247
249
  const providerSessionLookupCache = {
248
250
  codex: { key: null, data: null, promise: null, expiresAt: 0 },
249
251
  gemini: { key: null, data: null, promise: null, expiresAt: 0 },
@@ -332,6 +334,309 @@ function cacheCodexSessionFilePath(sessionId, filePath) {
332
334
  codexSessionFileCache.set(sessionId, filePath);
333
335
  }
334
336
 
337
+ class CodexAppServerClient {
338
+ constructor({ command = process.env.CODEX_APP_SERVER_COMMAND || 'codex' } = {}) {
339
+ this.command = command;
340
+ this.process = null;
341
+ this.readline = null;
342
+ this.nextId = 1;
343
+ this.pending = new Map();
344
+ this.initialized = false;
345
+ this.initializePromise = null;
346
+ this.stopped = false;
347
+ }
348
+
349
+ start() {
350
+ if (this.process) {
351
+ return;
352
+ }
353
+
354
+ this.stopped = false;
355
+ this.process = spawn(this.command, ['app-server', '--listen', 'stdio://'], {
356
+ stdio: ['pipe', 'pipe', 'pipe']
357
+ });
358
+
359
+ this.readline = readline.createInterface({
360
+ input: this.process.stdout,
361
+ crlfDelay: Infinity
362
+ });
363
+
364
+ this.readline.on('line', (line) => this.handleLine(line));
365
+ this.process.stderr.on('data', (buffer) => {
366
+ const text = String(buffer || '').trim().split('\n')[0]?.slice(0, 500);
367
+ if (text) {
368
+ console.warn(`[codex app-server] ${text}`);
369
+ }
370
+ });
371
+
372
+ const rejectPending = (error) => {
373
+ for (const pending of this.pending.values()) {
374
+ pending.reject(error);
375
+ }
376
+ this.pending.clear();
377
+ this.process = null;
378
+ this.readline = null;
379
+ this.initialized = false;
380
+ this.initializePromise = null;
381
+ };
382
+
383
+ this.process.on('error', (error) => rejectPending(error));
384
+ this.process.on('exit', (code, signal) => {
385
+ if (this.stopped) {
386
+ rejectPending(new Error('codex app-server stopped'));
387
+ return;
388
+ }
389
+ rejectPending(new Error(`codex app-server exited unexpectedly: ${code ?? signal ?? 'unknown'}`));
390
+ });
391
+ }
392
+
393
+ handleLine(line) {
394
+ if (!line.trim()) {
395
+ return;
396
+ }
397
+
398
+ let message;
399
+ try {
400
+ message = JSON.parse(line);
401
+ } catch (_) {
402
+ return;
403
+ }
404
+
405
+ if (typeof message.id === 'number' && this.pending.has(message.id)) {
406
+ const pending = this.pending.get(message.id);
407
+ this.pending.delete(message.id);
408
+ if (message.error) {
409
+ pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
410
+ } else {
411
+ pending.resolve(message.result);
412
+ }
413
+ return;
414
+ }
415
+
416
+ if (typeof message.id === 'number' && typeof message.method === 'string') {
417
+ this.send({
418
+ jsonrpc: '2.0',
419
+ id: message.id,
420
+ result: {}
421
+ });
422
+ }
423
+ }
424
+
425
+ send(payload) {
426
+ this.start();
427
+ if (!this.process?.stdin?.writable) {
428
+ throw new Error('codex app-server is not writable');
429
+ }
430
+ this.process.stdin.write(`${JSON.stringify(payload)}\n`);
431
+ }
432
+
433
+ call(method, params) {
434
+ this.start();
435
+ const id = this.nextId++;
436
+ return new Promise((resolve, reject) => {
437
+ this.pending.set(id, { resolve, reject });
438
+ this.send({
439
+ jsonrpc: '2.0',
440
+ id,
441
+ method,
442
+ params
443
+ });
444
+ });
445
+ }
446
+
447
+ async ensureInitialized() {
448
+ if (this.initialized) {
449
+ return;
450
+ }
451
+ if (this.initializePromise) {
452
+ await this.initializePromise;
453
+ return;
454
+ }
455
+
456
+ this.initializePromise = this.call('initialize', {
457
+ clientInfo: {
458
+ name: 'axhub-genie',
459
+ version: '0.2.11'
460
+ },
461
+ capabilities: {
462
+ experimentalApi: true
463
+ }
464
+ }).then(() => {
465
+ this.send({
466
+ jsonrpc: '2.0',
467
+ method: 'initialized'
468
+ });
469
+ this.initialized = true;
470
+ }).finally(() => {
471
+ this.initializePromise = null;
472
+ });
473
+
474
+ await this.initializePromise;
475
+ }
476
+
477
+ async listThreads({ cwd, limit = 5, cursor = null } = {}) {
478
+ await this.ensureInitialized();
479
+ return this.call('thread/list', {
480
+ archived: false,
481
+ sortKey: 'updated_at',
482
+ modelProviders: [],
483
+ cwd,
484
+ limit,
485
+ cursor: cursor || null
486
+ });
487
+ }
488
+
489
+ stop() {
490
+ this.stopped = true;
491
+ this.readline?.close();
492
+ this.process?.kill();
493
+ this.process = null;
494
+ this.readline = null;
495
+ this.pending.clear();
496
+ this.initialized = false;
497
+ this.initializePromise = null;
498
+ }
499
+ }
500
+
501
+ let codexAppServerClient = null;
502
+
503
+ function getCodexAppServerClient() {
504
+ if (codexAppServerClientOverride) {
505
+ return codexAppServerClientOverride;
506
+ }
507
+
508
+ if (!codexAppServerClient) {
509
+ codexAppServerClient = new CodexAppServerClient();
510
+ }
511
+
512
+ return codexAppServerClient;
513
+ }
514
+
515
+ function setCodexAppServerClientForTests(client) {
516
+ if (!client) {
517
+ codexAppServerClient?.stop?.();
518
+ codexAppServerClient = null;
519
+ }
520
+ codexAppServerClientOverride = client;
521
+ }
522
+
523
+ function normalizeCodexAppServerTimestamp(updatedAt) {
524
+ const timestamp = Number(updatedAt);
525
+ if (!Number.isFinite(timestamp) || timestamp <= 0) {
526
+ return new Date(0);
527
+ }
528
+
529
+ return new Date(timestamp * 1000);
530
+ }
531
+
532
+ function mapCodexAppServerThreadToSession(thread) {
533
+ if (!thread || typeof thread !== 'object' || typeof thread.id !== 'string' || !thread.id.trim()) {
534
+ return null;
535
+ }
536
+
537
+ const session = {
538
+ id: thread.id.trim(),
539
+ summary: typeof thread.preview === 'string' && thread.preview.trim() ? thread.preview.trim() : 'Codex Session',
540
+ messageCount: 0,
541
+ lastActivity: normalizeCodexAppServerTimestamp(thread.updatedAt),
542
+ cwd: typeof thread.cwd === 'string' ? thread.cwd : '',
543
+ model: null,
544
+ filePath: typeof thread.path === 'string' && thread.path.trim() ? thread.path.trim() : null,
545
+ provider: 'codex',
546
+ source: 'app-server'
547
+ };
548
+
549
+ if (session.filePath) {
550
+ cacheCodexSessionFilePath(session.id, session.filePath);
551
+ }
552
+
553
+ return session;
554
+ }
555
+
556
+ async function getCodexSessionsFromAppServer(projectPath, options = {}) {
557
+ const { limit = 5 } = options;
558
+ const normalizedProjectPath = normalizeComparableProjectPath(projectPath);
559
+ if (!normalizedProjectPath) {
560
+ return [];
561
+ }
562
+
563
+ const payload = await getCodexAppServerClient().listThreads({
564
+ cwd: projectPath,
565
+ limit,
566
+ cursor: null
567
+ });
568
+ const threads = Array.isArray(payload?.data) ? payload.data : [];
569
+
570
+ return threads
571
+ .map(mapCodexAppServerThreadToSession)
572
+ .filter(Boolean)
573
+ .filter((session) => normalizeComparableProjectPath(session.cwd) === normalizedProjectPath)
574
+ .slice(0, limit > 0 ? limit : undefined);
575
+ }
576
+
577
+ async function findCodexAppServerSessionById(sessionId, options = {}) {
578
+ const normalizedSessionId = trimText(sessionId);
579
+ if (!normalizedSessionId) {
580
+ return null;
581
+ }
582
+
583
+ const { limit = 50 } = options;
584
+ let cursor = null;
585
+
586
+ do {
587
+ const payload = await getCodexAppServerClient().listThreads({
588
+ cwd: undefined,
589
+ limit,
590
+ cursor
591
+ });
592
+ const threads = Array.isArray(payload?.data) ? payload.data : [];
593
+ const matchedThread = threads.find((thread) => trimText(thread?.id) === normalizedSessionId);
594
+
595
+ if (matchedThread) {
596
+ return mapCodexAppServerThreadToSession(matchedThread);
597
+ }
598
+
599
+ cursor = payload?.nextCursor || null;
600
+ } while (cursor);
601
+
602
+ return null;
603
+ }
604
+
605
+ async function getCodexSessionsFromJsonl(projectPath, options = {}) {
606
+ const { limit = 5 } = options;
607
+ const sessions = [];
608
+ const normalizedProjectPath = normalizeComparableProjectPath(projectPath);
609
+ const jsonlFiles = await findFilesRecursively(
610
+ getCodexSessionsDir(),
611
+ (entryName) => entryName.endsWith('.jsonl')
612
+ );
613
+
614
+ for (const filePath of jsonlFiles) {
615
+ try {
616
+ const sessionData = await parseCodexSessionFile(filePath);
617
+
618
+ if (sessionData && normalizeComparableProjectPath(sessionData.cwd) === normalizedProjectPath) {
619
+ cacheCodexSessionFilePath(sessionData.id, filePath);
620
+ sessions.push({
621
+ id: sessionData.id,
622
+ summary: sessionData.summary || 'Codex Session',
623
+ messageCount: sessionData.messageCount || 0,
624
+ lastActivity: sessionData.timestamp ? new Date(sessionData.timestamp) : new Date(),
625
+ cwd: sessionData.cwd,
626
+ model: sessionData.model,
627
+ filePath: filePath,
628
+ provider: 'codex'
629
+ });
630
+ }
631
+ } catch (error) {
632
+ console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
633
+ }
634
+ }
635
+
636
+ sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
637
+ return limit > 0 ? sessions.slice(0, limit) : sessions;
638
+ }
639
+
335
640
  function findCodexSessionFilePathBySessionIdHint(sessionId, filePaths = []) {
336
641
  const normalizedSessionId = String(sessionId || '').trim();
337
642
  if (!normalizedSessionId || !Array.isArray(filePaths) || filePaths.length === 0) {
@@ -799,8 +1104,7 @@ async function getProjectsList(progressCallback = null) {
799
1104
  return cloneProjectList(await projectListCache.promise);
800
1105
  }
801
1106
 
802
- async function buildProjectFromDefinition(definition, providerLookups = null) {
803
- const normalizedProjectPath = normalizeComparableProjectPath(definition.fullPath);
1107
+ async function buildProjectFromDefinition(definition) {
804
1108
  const project = {
805
1109
  name: definition.name,
806
1110
  path: definition.path,
@@ -819,66 +1123,21 @@ async function buildProjectFromDefinition(definition, providerLookups = null) {
819
1123
  };
820
1124
 
821
1125
  try {
822
- let claudeAcpSessions = [];
823
- let codexAcpSessions = [];
824
- let opencodeAcpSessions = [];
825
- let geminiAcpSessions = [];
826
-
827
- if (providerLookups) {
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
- );
848
- } else {
849
- const [codexSessions, opencodeSessions, geminiSessions, acpSessions] = await Promise.all([
850
- getCodexSessions(definition.fullPath, { limit: 5 }),
851
- getOpencodeSessions(definition.fullPath, { limit: 5 }),
852
- getGeminiSessions(definition.fullPath, { limit: 5 }),
853
- listAcpSessions({ projectPath: definition.fullPath })
854
- ]);
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' });
862
- }
863
-
864
- if (!definition.isManuallyAdded) {
865
- const sessionResult = await getSessions(definition.name, 5, 0);
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
- );
875
- project.sessionMeta = {
876
- hasMore: sessionResult.hasMore,
877
- total: sessionResult.total
878
- };
879
- } else {
880
- project.sessions = mergeSessionLists([], claudeAcpSessions, { fallbackProvider: 'claude' });
881
- }
1126
+ const [claudeSessions, codexSessions, opencodeSessions, geminiSessions] = await Promise.all([
1127
+ getLocalProviderSessions({ provider: 'claude', projectName: definition.name, projectPath: definition.fullPath, limit: 50 }),
1128
+ getLocalProviderSessions({ provider: 'codex', projectName: definition.name, projectPath: definition.fullPath, limit: 50 }),
1129
+ getLocalProviderSessions({ provider: 'opencode', projectName: definition.name, projectPath: definition.fullPath, limit: 50 }),
1130
+ getLocalProviderSessions({ provider: 'gemini', projectName: definition.name, projectPath: definition.fullPath, limit: 50 })
1131
+ ]);
1132
+
1133
+ project.sessions = claudeSessions;
1134
+ project.codexSessions = codexSessions;
1135
+ project.opencodeSessions = opencodeSessions;
1136
+ project.geminiSessions = geminiSessions;
1137
+ project.sessionMeta = {
1138
+ hasMore: false,
1139
+ total: claudeSessions.length
1140
+ };
882
1141
  } catch (error) {
883
1142
  console.warn(`Could not load session details for project ${definition.name}:`, error.message);
884
1143
  }
@@ -901,32 +1160,8 @@ async function getProjects(progressCallback = null) {
901
1160
  const projects = [];
902
1161
  const { projectDefinitions, totalProjects } = await collectProjectDefinitions(progressCallback);
903
1162
 
904
- const uniqueProjectPaths = Array.from(new Set(
905
- projectDefinitions
906
- .map((definition) => normalizeComparableProjectPath(definition.fullPath))
907
- .filter(Boolean)
908
- ));
909
-
910
- const [codexSessionsByProjectPath, geminiSessionsByProjectPath, opencodeSessionsByProjectPath, claudeAcpSessionsByProjectPath, codexAcpSessionsByProjectPath, geminiAcpSessionsByProjectPath, opencodeAcpSessionsByProjectPath] = await Promise.all([
911
- buildCodexSessionsLookup(uniqueProjectPaths, { limit: 5 }),
912
- buildGeminiSessionsLookup(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 })
918
- ]);
919
-
920
1163
  for (const definition of projectDefinitions) {
921
- projects.push(await buildProjectFromDefinition(definition, {
922
- claudeAcpSessionsByProjectPath,
923
- codexSessionsByProjectPath,
924
- codexAcpSessionsByProjectPath,
925
- geminiSessionsByProjectPath,
926
- geminiAcpSessionsByProjectPath,
927
- opencodeSessionsByProjectPath,
928
- opencodeAcpSessionsByProjectPath
929
- }));
1164
+ projects.push(await buildProjectFromDefinition(definition));
930
1165
  }
931
1166
 
932
1167
  // Emit completion after all projects (including manual) are processed
@@ -1378,12 +1613,20 @@ async function deleteSession(projectName, sessionId) {
1378
1613
 
1379
1614
  // Write back the filtered content
1380
1615
  await fs.writeFile(jsonlFile, filteredLines.join('\n') + (filteredLines.length > 0 ? '\n' : ''));
1616
+ await deleteAcpSessionRecord('claude', sessionId).catch(() => {});
1381
1617
  return true;
1382
1618
  }
1383
1619
  }
1384
-
1620
+
1621
+ if (await deleteAcpSessionRecord('claude', sessionId).catch(() => false)) {
1622
+ return true;
1623
+ }
1624
+
1385
1625
  throw new Error(`Session ${sessionId} not found in any files`);
1386
1626
  } catch (error) {
1627
+ if (await deleteAcpSessionRecord('claude', sessionId).catch(() => false)) {
1628
+ return true;
1629
+ }
1387
1630
  console.error(`Error deleting session ${sessionId} from project ${projectName}:`, error);
1388
1631
  throw error;
1389
1632
  }
@@ -1533,47 +1776,27 @@ async function addProjectManually(projectPath, displayName = null) {
1533
1776
  }
1534
1777
 
1535
1778
  async function getCodexSessions(projectPath, options = {}) {
1536
- const { limit = 5 } = options;
1537
- try {
1538
- const sessions = [];
1539
- const normalizedProjectPath = normalizeComparableProjectPath(projectPath);
1540
- const jsonlFiles = await findFilesRecursively(
1541
- getCodexSessionsDir(),
1542
- (entryName) => entryName.endsWith('.jsonl')
1543
- );
1544
-
1545
- // Process each file to find sessions matching the project path
1546
- for (const filePath of jsonlFiles) {
1547
- try {
1548
- const sessionData = await parseCodexSessionFile(filePath);
1779
+ const { limit = 5, localOnly = false } = options;
1780
+ if (localOnly) {
1781
+ return getCodexSessionsFromJsonl(projectPath, { limit });
1782
+ }
1549
1783
 
1550
- if (sessionData && normalizeComparableProjectPath(sessionData.cwd) === normalizedProjectPath) {
1551
- cacheCodexSessionFilePath(sessionData.id, filePath);
1552
- sessions.push({
1553
- id: sessionData.id,
1554
- summary: sessionData.summary || 'Codex Session',
1555
- messageCount: sessionData.messageCount || 0,
1556
- lastActivity: sessionData.timestamp ? new Date(sessionData.timestamp) : new Date(),
1557
- cwd: sessionData.cwd,
1558
- model: sessionData.model,
1559
- filePath: filePath,
1560
- provider: 'codex'
1561
- });
1562
- }
1563
- } catch (error) {
1564
- console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
1565
- }
1784
+ try {
1785
+ const appServerSessions = await getCodexSessionsFromAppServer(projectPath, { limit });
1786
+ if (appServerSessions.length > 0) {
1787
+ return appServerSessions;
1566
1788
  }
1567
1789
 
1568
- // Sort sessions by last activity (newest first)
1569
- sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
1570
-
1571
- // Return limited sessions for performance (0 = unlimited for deletion)
1572
- return limit > 0 ? sessions.slice(0, limit) : sessions;
1790
+ return await getCodexSessionsFromJsonl(projectPath, { limit });
1573
1791
 
1574
1792
  } catch (error) {
1575
- console.error('Error fetching Codex sessions:', error);
1576
- return [];
1793
+ console.warn(`Codex app-server thread/list failed, falling back to JSONL scan: ${error.message}`);
1794
+ try {
1795
+ return await getCodexSessionsFromJsonl(projectPath, { limit });
1796
+ } catch (fallbackError) {
1797
+ console.error('Error fetching Codex sessions:', fallbackError);
1798
+ return [];
1799
+ }
1577
1800
  }
1578
1801
  }
1579
1802
 
@@ -1999,10 +2222,14 @@ async function deleteOpencodeSession(sessionId) {
1999
2222
  try {
2000
2223
  const sessionFilePath = await findOpencodeSessionFileById(sessionId);
2001
2224
  if (!sessionFilePath) {
2225
+ if (await deleteAcpSessionRecord('opencode', sessionId).catch(() => false)) {
2226
+ return true;
2227
+ }
2002
2228
  throw new Error(`OpenCode session file not found for session ${sessionId}`);
2003
2229
  }
2004
2230
 
2005
2231
  await fs.unlink(sessionFilePath);
2232
+ await deleteAcpSessionRecord('opencode', sessionId).catch(() => {});
2006
2233
  return true;
2007
2234
  } catch (error) {
2008
2235
  console.error(`Error deleting OpenCode session ${sessionId}:`, error);
@@ -2343,10 +2570,15 @@ async function deleteGeminiSession(sessionId) {
2343
2570
  const sessionFilePath = sessionMetadata?.filePath || null;
2344
2571
 
2345
2572
  if (!sessionFilePath) {
2573
+ if (await deleteAcpSessionRecord('gemini', sessionId).catch(() => false)) {
2574
+ clearProviderSessionLookupCaches();
2575
+ return true;
2576
+ }
2346
2577
  throw new Error(`Gemini session file not found for session ${sessionId}`);
2347
2578
  }
2348
2579
 
2349
2580
  await fs.unlink(sessionFilePath);
2581
+ await deleteAcpSessionRecord('gemini', sessionId).catch(() => {});
2350
2582
  clearProviderSessionLookupCaches();
2351
2583
  return true;
2352
2584
  } catch (error) {
@@ -2687,15 +2919,120 @@ async function getCodexSessionMetadata(sessionId) {
2687
2919
  return metadata;
2688
2920
  }
2689
2921
 
2922
+ function normalizeLocalProviderSession(session, provider, projectPath = null) {
2923
+ if (!session || typeof session !== 'object') {
2924
+ return null;
2925
+ }
2926
+
2927
+ const sessionId = String(session.id || session.sessionId || '').trim();
2928
+ if (!sessionId) {
2929
+ return null;
2930
+ }
2931
+
2932
+ const lastActivity = session.lastActivity || session.updatedAt || session.timestamp || session.createdAt || null;
2933
+ const createdAt = session.createdAt || session.startTime || session.timestamp || lastActivity || null;
2934
+ const cwd = session.cwd || session.projectPath || projectPath || null;
2935
+ const summary = session.summary || session.title || session.name || `${provider} session`;
2936
+
2937
+ return {
2938
+ ...session,
2939
+ id: sessionId,
2940
+ sessionId,
2941
+ provider,
2942
+ __provider: provider,
2943
+ source: session.source || 'native-history',
2944
+ runtime: 'native-history',
2945
+ title: session.title || summary || sessionId,
2946
+ summary: summary || null,
2947
+ projectPath: cwd,
2948
+ cwd,
2949
+ createdAt,
2950
+ updatedAt: session.updatedAt || lastActivity || createdAt,
2951
+ lastActivity: lastActivity || createdAt,
2952
+ model: session.model || null
2953
+ };
2954
+ }
2955
+
2956
+ async function getNativeProviderSessions({ provider, projectName = null, projectPath = null, limit = 50 } = {}) {
2957
+ switch (provider) {
2958
+ case 'claude': {
2959
+ if (!projectName) {
2960
+ return [];
2961
+ }
2962
+ try {
2963
+ await fs.access(path.join(os.homedir(), '.claude', 'projects', projectName));
2964
+ } catch (_) {
2965
+ return [];
2966
+ }
2967
+ const result = await getSessions(projectName, limit > 0 ? limit : Number.MAX_SAFE_INTEGER, 0);
2968
+ const sessions = Array.isArray(result?.sessions) ? result.sessions : [];
2969
+ return sessions.map((session) => normalizeLocalProviderSession(session, 'claude', projectPath)).filter(Boolean);
2970
+ }
2971
+ case 'codex':
2972
+ return (await getCodexSessions(projectPath, { limit, localOnly: true }))
2973
+ .map((session) => normalizeLocalProviderSession(session, 'codex', projectPath))
2974
+ .filter(Boolean);
2975
+ case 'gemini':
2976
+ return (await getGeminiSessions(projectPath, { limit }))
2977
+ .map((session) => normalizeLocalProviderSession(session, 'gemini', projectPath))
2978
+ .filter(Boolean);
2979
+ case 'opencode':
2980
+ return (await getOpencodeSessions(projectPath, { limit }))
2981
+ .map((session) => normalizeLocalProviderSession(session, 'opencode', projectPath))
2982
+ .filter(Boolean);
2983
+ default:
2984
+ return [];
2985
+ }
2986
+ }
2987
+
2988
+ async function getLocalProviderSessions({ provider, projectName = null, projectPath = null, limit = 50 } = {}) {
2989
+ const [nativeSessions, acpSessions] = await Promise.all([
2990
+ getNativeProviderSessions({ provider, projectName, projectPath, limit }),
2991
+ listAcpSessions({ provider, projectPath }).catch(() => [])
2992
+ ]);
2993
+
2994
+ return mergeSessionLists(nativeSessions, acpSessions, { fallbackProvider: provider })
2995
+ .slice(0, limit > 0 ? limit : undefined);
2996
+ }
2997
+
2998
+ async function getLocalProviderSessionMessages({
2999
+ provider,
3000
+ projectName = null,
3001
+ sessionId,
3002
+ limit = null,
3003
+ offset = 0
3004
+ } = {}) {
3005
+ switch (provider) {
3006
+ case 'claude':
3007
+ if (!projectName) {
3008
+ return { messages: [], total: 0, hasMore: false, offset, limit };
3009
+ }
3010
+ return getSessionMessages(projectName, sessionId, limit, offset);
3011
+ case 'codex':
3012
+ return getCodexSessionMessages(sessionId, limit, offset);
3013
+ case 'gemini':
3014
+ return getGeminiSessionMessages(sessionId, limit, offset);
3015
+ case 'opencode':
3016
+ return getOpencodeSessionMessages(sessionId, limit, offset);
3017
+ default:
3018
+ return { messages: [], total: 0, hasMore: false, offset, limit };
3019
+ }
3020
+ }
3021
+
2690
3022
  async function deleteCodexSession(sessionId) {
2691
3023
  try {
2692
3024
  const sessionFilePath = await resolveCodexSessionFile(sessionId);
2693
3025
 
2694
3026
  if (!sessionFilePath) {
3027
+ if (await deleteAcpSessionRecord('codex', sessionId).catch(() => false)) {
3028
+ codexSessionFileCache.delete(sessionId);
3029
+ return true;
3030
+ }
2695
3031
  throw new Error(`Codex session file not found for session ${sessionId}`);
2696
3032
  }
2697
3033
 
2698
3034
  await fs.unlink(sessionFilePath);
3035
+ await deleteAcpSessionRecord('codex', sessionId).catch(() => {});
2699
3036
  codexSessionFileCache.delete(sessionId);
2700
3037
  return true;
2701
3038
  } catch (error) {
@@ -2722,10 +3059,14 @@ export {
2722
3059
  extractProjectDirectory,
2723
3060
  clearProjectDirectoryCache,
2724
3061
  clearProviderSessionLookupCaches,
3062
+ setCodexAppServerClientForTests,
2725
3063
  findCodexSessionFilePathBySessionIdHint,
3064
+ findCodexAppServerSessionById,
2726
3065
  getCodexSessions,
2727
3066
  getCodexSessionMessages,
2728
3067
  getCodexSessionMetadata,
3068
+ getLocalProviderSessions,
3069
+ getLocalProviderSessionMessages,
2729
3070
  getOpencodeSessions,
2730
3071
  getOpencodeSessionMetadata,
2731
3072
  getOpencodeSessionMessages,