@mrpatronz/nexusflow 2.21.0 → 2.22.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/dist/gui/assets/AgentsPage-CwgqlrPd.js +3 -0
- package/dist/gui/assets/DashboardPage-BdBo1iDm.js +1 -0
- package/dist/gui/assets/{DocumentPreview-BlUlSRK0.js → DocumentPreview-BF79hZoZ.js} +2 -2
- package/dist/gui/assets/{GettingStartedPage-C-2t-udq.js → GettingStartedPage-BhPWS6qy.js} +1 -1
- package/dist/gui/assets/{MonacoDiffAdapter-C1gYxNl1.js → MonacoDiffAdapter-DgGLd1S5.js} +1 -1
- package/dist/gui/assets/{ProjectsPage-Bll4qUCc.js → ProjectsPage-DfSB-Qul.js} +1 -1
- package/dist/gui/assets/{ScaffoldRepoInline-BYGKL4qv.js → ScaffoldRepoInline-y0YoOFK6.js} +1 -1
- package/dist/gui/assets/SettingsPage-BHly-6Wc.js +1 -0
- package/dist/gui/assets/{SkillsPage-CLGLrRq3.js → SkillsPage-BA2-hZk7.js} +3 -3
- package/dist/gui/assets/StartWorkPage-BIhkPxp6.js +1 -0
- package/dist/gui/assets/{StrategiesPage-nNBMOxvu.js → StrategiesPage-DCPhSuM4.js} +2 -2
- package/dist/gui/assets/WorkroomsPage-BKSxMbg3.js +4 -0
- package/dist/gui/assets/WorkspacesPage-DfTNHupN.js +17 -0
- package/dist/gui/assets/{alert-dialog-DnwZtA87.js → alert-dialog-D0AKLuV3.js} +1 -1
- package/dist/gui/assets/{index-R0fvXNEi.js → index-CvEbFSmU.js} +35 -35
- package/dist/gui/assets/index-XMSrNppd.css +2 -0
- package/dist/gui/assets/{card-BMggHY3n.js → label-D5LXpdA9.js} +1 -1
- package/dist/gui/assets/queries-BSNhQxoS.js +3 -0
- package/dist/gui/assets/{tabs-CIkN5ISA.js → tabs-DRynuaQV.js} +1 -1
- package/dist/gui/index.html +3 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +72 -37
- package/dist/server.js.map +1 -1
- package/dist/terminal/routes.d.ts.map +1 -1
- package/dist/terminal/routes.js +2 -1
- package/dist/terminal/routes.js.map +1 -1
- package/dist/terminal/targets.d.ts +2 -28
- package/dist/terminal/targets.d.ts.map +1 -1
- package/dist/terminal/targets.js +12 -27
- package/dist/terminal/targets.js.map +1 -1
- package/dist/types.d.ts +6 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/cli-harnesses.d.ts +56 -0
- package/dist/utils/cli-harnesses.d.ts.map +1 -0
- package/dist/utils/cli-harnesses.js +20 -0
- package/dist/utils/cli-harnesses.js.map +1 -0
- package/dist/utils/pi-sessions.d.ts +5 -0
- package/dist/utils/pi-sessions.d.ts.map +1 -0
- package/dist/utils/pi-sessions.js +150 -0
- package/dist/utils/pi-sessions.js.map +1 -0
- package/dist/utils/session-finder.d.ts +10 -6
- package/dist/utils/session-finder.d.ts.map +1 -1
- package/dist/utils/session-finder.js +369 -340
- package/dist/utils/session-finder.js.map +1 -1
- package/dist/utils/session-readers.d.ts +21 -0
- package/dist/utils/session-readers.d.ts.map +1 -0
- package/dist/utils/session-readers.js +14 -0
- package/dist/utils/session-readers.js.map +1 -0
- package/dist/utils/terminal-launch.d.ts +1 -0
- package/dist/utils/terminal-launch.d.ts.map +1 -1
- package/dist/utils/terminal-launch.js +11 -32
- package/dist/utils/terminal-launch.js.map +1 -1
- package/package.json +1 -1
- package/dist/gui/assets/AgentsPage-Dhd1ZqwZ.js +0 -3
- package/dist/gui/assets/DashboardPage-DuG3Nx62.js +0 -1
- package/dist/gui/assets/SettingsPage-DP1pJNJm.js +0 -1
- package/dist/gui/assets/StartWorkPage-CcCPKOv8.js +0 -1
- package/dist/gui/assets/WorkroomsPage-CnH1E8mW.js +0 -4
- package/dist/gui/assets/WorkspacesPage-DblU6U2z.js +0 -17
- package/dist/gui/assets/index-BO8b3Dk2.css +0 -2
- package/dist/gui/assets/label-C610MHwq.js +0 -1
- package/dist/gui/assets/queries-F2H3SwGY.js +0 -3
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import * as fs from 'node:fs/promises';
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
import * as os from 'node:os';
|
|
8
|
+
import { MODULAR_SESSION_READERS, modularSessionReader } from './session-readers.js';
|
|
9
|
+
export { SESSION_SOURCES } from './cli-harnesses.js';
|
|
8
10
|
export function extractRecordUsage(record) {
|
|
9
11
|
if (!record || typeof record !== 'object')
|
|
10
12
|
return {};
|
|
@@ -437,13 +439,14 @@ export async function canTransferClaudeSessionInWorkspace(workspacePath, repoPat
|
|
|
437
439
|
}
|
|
438
440
|
/**
|
|
439
441
|
* Scans the local filesystem for conversation histories belonging to Claude, Antigravity,
|
|
440
|
-
* Codex, and
|
|
442
|
+
* Codex, Copilot, and Pi that relate to the specified workspace.
|
|
441
443
|
*
|
|
442
444
|
* @param workspacePath - Root directory of the active workspace.
|
|
443
445
|
* @param repoPaths - Directories of sub-repositories included in the workspace.
|
|
446
|
+
* @param source - Optional history source to scan independently for progressive loading.
|
|
444
447
|
* @returns A promise that resolves to an array of {@link AISession} objects sorted by update time descending.
|
|
445
448
|
*/
|
|
446
|
-
export async function findSessions(workspacePath, repoPaths = []) {
|
|
449
|
+
export async function findSessions(workspacePath, repoPaths = [], source) {
|
|
447
450
|
const sessions = [];
|
|
448
451
|
const wsFolderName = path.basename(workspacePath);
|
|
449
452
|
// Normalization helper for accurate path comparison
|
|
@@ -482,392 +485,410 @@ export async function findSessions(workspacePath, repoPaths = []) {
|
|
|
482
485
|
return workspacePath;
|
|
483
486
|
};
|
|
484
487
|
// ─── 1. Scan Antigravity Sessions ──────────────────────────────────────
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (
|
|
496
|
-
conversationsMap.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
488
|
+
if (!source || source === 'antigravity') {
|
|
489
|
+
const agDir = getAntigravityDir();
|
|
490
|
+
const agHistoryPath = path.join(agDir, 'history.jsonl');
|
|
491
|
+
try {
|
|
492
|
+
const historyContent = await fs.readFile(agHistoryPath, 'utf-8');
|
|
493
|
+
const lines = historyContent.split('\n').filter(Boolean);
|
|
494
|
+
const conversationsMap = new Map();
|
|
495
|
+
for (const line of lines) {
|
|
496
|
+
try {
|
|
497
|
+
const entry = JSON.parse(line);
|
|
498
|
+
if (entry.conversationId && isPathMatch(entry.workspace)) {
|
|
499
|
+
if (!conversationsMap.has(entry.conversationId)) {
|
|
500
|
+
conversationsMap.set(entry.conversationId, {
|
|
501
|
+
id: entry.conversationId,
|
|
502
|
+
title: entry.display || 'Untitled Conversation',
|
|
503
|
+
entries: [],
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
conversationsMap.get(entry.conversationId).entries.push(entry);
|
|
501
507
|
}
|
|
502
|
-
conversationsMap.get(entry.conversationId).entries.push(entry);
|
|
503
508
|
}
|
|
509
|
+
catch { }
|
|
504
510
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
firstUserMessage = tObj.content;
|
|
511
|
+
for (const [convId, convData] of conversationsMap.entries()) {
|
|
512
|
+
const transcriptPath = path.join(agDir, 'brain', convId, '.system_generated', 'logs', 'transcript.jsonl');
|
|
513
|
+
let messageCount = 0;
|
|
514
|
+
const times = sessionTimes();
|
|
515
|
+
for (const entry of convData.entries)
|
|
516
|
+
times.add(entry.timestamp);
|
|
517
|
+
let title = convData.title;
|
|
518
|
+
let sessionUsage;
|
|
519
|
+
let latestQuota;
|
|
520
|
+
try {
|
|
521
|
+
const transcriptContent = await fs.readFile(transcriptPath, 'utf-8');
|
|
522
|
+
const transcriptLines = transcriptContent.split('\n').filter(Boolean);
|
|
523
|
+
let firstUserMessage = null;
|
|
524
|
+
for (const line of transcriptLines) {
|
|
525
|
+
try {
|
|
526
|
+
const tObj = JSON.parse(line);
|
|
527
|
+
times.add(tObj.timestamp);
|
|
528
|
+
if (tObj.type === 'USER_INPUT' || tObj.type === 'PLANNER_RESPONSE') {
|
|
529
|
+
messageCount++;
|
|
530
|
+
if (tObj.type === 'USER_INPUT' && !firstUserMessage) {
|
|
531
|
+
firstUserMessage = tObj.content;
|
|
532
|
+
}
|
|
528
533
|
}
|
|
534
|
+
const { usage: turnUsage, quota: turnQuota } = extractRecordUsage(tObj);
|
|
535
|
+
if (turnUsage)
|
|
536
|
+
sessionUsage = accumulateUsage(sessionUsage, turnUsage);
|
|
537
|
+
if (turnQuota)
|
|
538
|
+
latestQuota = turnQuota;
|
|
529
539
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
540
|
+
catch { }
|
|
541
|
+
}
|
|
542
|
+
if (firstUserMessage) {
|
|
543
|
+
const cleanMatch = firstUserMessage.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/);
|
|
544
|
+
title = cleanMatch ? cleanMatch[1].trim() : firstUserMessage.trim();
|
|
535
545
|
}
|
|
536
|
-
catch { }
|
|
537
546
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
title = cleanMatch ? cleanMatch[1].trim() : firstUserMessage.trim();
|
|
547
|
+
catch {
|
|
548
|
+
messageCount = convData.entries.length * 2;
|
|
541
549
|
}
|
|
550
|
+
sessions.push({
|
|
551
|
+
id: convId,
|
|
552
|
+
assistant: 'antigravity',
|
|
553
|
+
title: sanitizeSessionTitle(title),
|
|
554
|
+
...times.values(),
|
|
555
|
+
messageCount,
|
|
556
|
+
workspacePath: resolveTargetCwd(convData.entries[0]?.workspace),
|
|
557
|
+
recordedCwd: convData.entries[0]?.workspace,
|
|
558
|
+
threadKind: 'unknown',
|
|
559
|
+
...(sessionUsage ? { usage: sessionUsage } : {}),
|
|
560
|
+
...(latestQuota ? { quota: latestQuota } : {}),
|
|
561
|
+
});
|
|
542
562
|
}
|
|
543
|
-
catch {
|
|
544
|
-
messageCount = convData.entries.length * 2;
|
|
545
|
-
}
|
|
546
|
-
sessions.push({
|
|
547
|
-
id: convId,
|
|
548
|
-
assistant: 'antigravity',
|
|
549
|
-
title: sanitizeSessionTitle(title),
|
|
550
|
-
...times.values(),
|
|
551
|
-
messageCount,
|
|
552
|
-
workspacePath: resolveTargetCwd(convData.entries[0]?.workspace),
|
|
553
|
-
recordedCwd: convData.entries[0]?.workspace,
|
|
554
|
-
threadKind: 'unknown',
|
|
555
|
-
...(sessionUsage ? { usage: sessionUsage } : {}),
|
|
556
|
-
...(latestQuota ? { quota: latestQuota } : {}),
|
|
557
|
-
});
|
|
558
563
|
}
|
|
564
|
+
catch { }
|
|
559
565
|
}
|
|
560
|
-
catch { }
|
|
561
566
|
// ─── 2. Scan Claude Code Sessions ──────────────────────────────────────
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
times.add(record.timestamp);
|
|
596
|
-
if (record.sessionId === sessionId && typeof record.cwd === 'string')
|
|
597
|
-
recordedCwd ??= record.cwd;
|
|
598
|
-
if (record.isSidechain === true)
|
|
599
|
-
threadKind = 'subagent';
|
|
600
|
-
else if (record.isSidechain === false && threadKind === 'unknown')
|
|
601
|
-
threadKind = 'main';
|
|
602
|
-
if (record.type === 'user' || record.type === 'assistant') {
|
|
603
|
-
const text = claudeRecordText(record);
|
|
604
|
-
// CLI-injected caveats, slash commands and tool results aren't
|
|
605
|
-
// real turns — exclude them from the count and the title.
|
|
606
|
-
const isNoise = record.type === 'user' && isNoiseUserRecord(record, text);
|
|
607
|
-
if (isNoise)
|
|
567
|
+
if (!source || source === 'claude') {
|
|
568
|
+
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
569
|
+
const claudeProjectsDir = path.join(claudeConfigDir, 'projects');
|
|
570
|
+
try {
|
|
571
|
+
const candidateTargets = [
|
|
572
|
+
{ folder: getClaudeProjectFolderName(workspacePath), targetPath: workspacePath },
|
|
573
|
+
...repoPaths.map(r => ({ folder: getClaudeProjectFolderName(r), targetPath: r }))
|
|
574
|
+
];
|
|
575
|
+
for (const { folder, targetPath } of candidateTargets) {
|
|
576
|
+
const projectPath = path.join(claudeProjectsDir, folder);
|
|
577
|
+
try {
|
|
578
|
+
const files = await fs.readdir(projectPath);
|
|
579
|
+
const jsonlFiles = files.filter(f => f.endsWith('.jsonl'));
|
|
580
|
+
for (const file of jsonlFiles) {
|
|
581
|
+
const filePath = path.join(projectPath, file);
|
|
582
|
+
const sessionId = path.basename(file, '.jsonl');
|
|
583
|
+
try {
|
|
584
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
585
|
+
const lines = content.split('\n').filter(Boolean);
|
|
586
|
+
let messageCount = 0;
|
|
587
|
+
const times = sessionTimes();
|
|
588
|
+
let title = 'Claude Session';
|
|
589
|
+
let recordedCwd;
|
|
590
|
+
let threadKind = file.startsWith('agent-') ? 'subagent' : 'unknown';
|
|
591
|
+
let sessionUsage;
|
|
592
|
+
let latestQuota;
|
|
593
|
+
for (const line of lines) {
|
|
594
|
+
let record;
|
|
595
|
+
try {
|
|
596
|
+
record = JSON.parse(line);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
608
599
|
continue;
|
|
609
|
-
messageCount++;
|
|
610
|
-
if (record.type === 'user' && title === 'Claude Session' && text.trim()) {
|
|
611
|
-
title = text.trim();
|
|
612
600
|
}
|
|
601
|
+
times.add(record.timestamp);
|
|
602
|
+
if (record.sessionId === sessionId && typeof record.cwd === 'string')
|
|
603
|
+
recordedCwd ??= record.cwd;
|
|
604
|
+
if (record.isSidechain === true)
|
|
605
|
+
threadKind = 'subagent';
|
|
606
|
+
else if (record.isSidechain === false && threadKind === 'unknown')
|
|
607
|
+
threadKind = 'main';
|
|
608
|
+
if (record.type === 'user' || record.type === 'assistant') {
|
|
609
|
+
const text = claudeRecordText(record);
|
|
610
|
+
// CLI-injected caveats, slash commands and tool results aren't
|
|
611
|
+
// real turns — exclude them from the count and the title.
|
|
612
|
+
const isNoise = record.type === 'user' && isNoiseUserRecord(record, text);
|
|
613
|
+
if (isNoise)
|
|
614
|
+
continue;
|
|
615
|
+
messageCount++;
|
|
616
|
+
if (record.type === 'user' && title === 'Claude Session' && text.trim()) {
|
|
617
|
+
title = text.trim();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const { usage: turnUsage, quota: turnQuota } = extractRecordUsage(record);
|
|
621
|
+
if (turnUsage)
|
|
622
|
+
sessionUsage = accumulateUsage(sessionUsage, turnUsage);
|
|
623
|
+
if (turnQuota)
|
|
624
|
+
latestQuota = turnQuota;
|
|
613
625
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
626
|
+
sessions.push({
|
|
627
|
+
id: sessionId,
|
|
628
|
+
assistant: 'claude',
|
|
629
|
+
title: sanitizeSessionTitle(title),
|
|
630
|
+
...times.values(),
|
|
631
|
+
messageCount,
|
|
632
|
+
workspacePath: targetPath,
|
|
633
|
+
recordedCwd,
|
|
634
|
+
threadKind,
|
|
635
|
+
...(sessionUsage ? { usage: sessionUsage } : {}),
|
|
636
|
+
...(latestQuota ? { quota: latestQuota } : {}),
|
|
637
|
+
});
|
|
619
638
|
}
|
|
620
|
-
|
|
621
|
-
id: sessionId,
|
|
622
|
-
assistant: 'claude',
|
|
623
|
-
title: sanitizeSessionTitle(title),
|
|
624
|
-
...times.values(),
|
|
625
|
-
messageCount,
|
|
626
|
-
workspacePath: targetPath,
|
|
627
|
-
recordedCwd,
|
|
628
|
-
threadKind,
|
|
629
|
-
...(sessionUsage ? { usage: sessionUsage } : {}),
|
|
630
|
-
...(latestQuota ? { quota: latestQuota } : {}),
|
|
631
|
-
});
|
|
639
|
+
catch { }
|
|
632
640
|
}
|
|
633
|
-
catch { }
|
|
634
641
|
}
|
|
642
|
+
catch { }
|
|
635
643
|
}
|
|
636
|
-
catch { }
|
|
637
644
|
}
|
|
645
|
+
catch { }
|
|
638
646
|
}
|
|
639
|
-
catch { }
|
|
640
647
|
// ─── 3. OpenAI Codex Sessions (rollout-*.jsonl) ──────────────────────────
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const content = await fs.readFile(file, 'utf-8');
|
|
660
|
-
const lines = content.split('\n').filter(Boolean);
|
|
661
|
-
let sessionCwd = null;
|
|
662
|
-
let sessionId = null;
|
|
663
|
-
let messageCount = 0;
|
|
664
|
-
const times = sessionTimes();
|
|
665
|
-
let title = 'Codex Session';
|
|
666
|
-
let identity = { threadKind: 'unknown' };
|
|
667
|
-
let sessionUsage;
|
|
668
|
-
let reportedTotalUsage;
|
|
669
|
-
let latestQuota;
|
|
670
|
-
for (const line of lines) {
|
|
671
|
-
let record;
|
|
672
|
-
try {
|
|
673
|
-
record = JSON.parse(line);
|
|
674
|
-
}
|
|
675
|
-
catch {
|
|
648
|
+
if (!source || source === 'codex') {
|
|
649
|
+
// Codex writes a `session_meta` record (payload.cwd) followed by
|
|
650
|
+
// `response_item` records (payload.type==='message', role, content[].text).
|
|
651
|
+
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
652
|
+
const codexSessionsDir = path.join(codexHome, 'sessions');
|
|
653
|
+
try {
|
|
654
|
+
const codexFiles = await getFilesRecursively(codexSessionsDir, '.jsonl');
|
|
655
|
+
// Sort candidate rollout files so most recent / newest are scanned first
|
|
656
|
+
const sortedCodexFiles = [...codexFiles].sort((a, b) => b.localeCompare(a));
|
|
657
|
+
const MAX_CODEX_SCAN = 200;
|
|
658
|
+
const filesToScan = sortedCodexFiles.slice(0, MAX_CODEX_SCAN);
|
|
659
|
+
for (const file of filesToScan) {
|
|
660
|
+
try {
|
|
661
|
+
const header = await codexSessionHeader(file);
|
|
662
|
+
// Recent rollouts record cwd up front. Skip unrelated conversations
|
|
663
|
+
// before reading and parsing their full, sometimes very large logs.
|
|
664
|
+
if (header?.type === 'session_meta' && typeof header.payload?.cwd === 'string'
|
|
665
|
+
&& !isPathMatch(header.payload.cwd))
|
|
676
666
|
continue;
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
667
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
668
|
+
const lines = content.split('\n').filter(Boolean);
|
|
669
|
+
let sessionCwd = null;
|
|
670
|
+
let sessionId = null;
|
|
671
|
+
let messageCount = 0;
|
|
672
|
+
const times = sessionTimes();
|
|
673
|
+
let title = 'Codex Session';
|
|
674
|
+
let identity = { threadKind: 'unknown' };
|
|
675
|
+
let sessionUsage;
|
|
676
|
+
let reportedTotalUsage;
|
|
677
|
+
let latestQuota;
|
|
678
|
+
for (const line of lines) {
|
|
679
|
+
let record;
|
|
680
|
+
try {
|
|
681
|
+
record = JSON.parse(line);
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
692
684
|
continue;
|
|
693
|
-
messageCount++;
|
|
694
|
-
if (role === 'user' && title === 'Codex Session' && text) {
|
|
695
|
-
title = text;
|
|
696
685
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
686
|
+
times.add(record.timestamp);
|
|
687
|
+
times.add(record.payload?.timestamp);
|
|
688
|
+
if (record.type === 'session_meta') {
|
|
689
|
+
if (record.payload?.cwd)
|
|
690
|
+
sessionCwd = record.payload.cwd;
|
|
691
|
+
sessionId = codexSessionId(record) ?? sessionId;
|
|
692
|
+
identity = codexThreadIdentity(record.payload);
|
|
693
|
+
}
|
|
694
|
+
else if (record.type === 'response_item' && record.payload?.type === 'message') {
|
|
695
|
+
const role = record.payload.role;
|
|
696
|
+
if (role !== 'user' && role !== 'assistant')
|
|
697
|
+
continue;
|
|
698
|
+
const text = codexMessageText(record.payload).trim();
|
|
699
|
+
if (role === 'user' && (!text || isInjectedContextText(text)))
|
|
700
|
+
continue;
|
|
701
|
+
messageCount++;
|
|
702
|
+
if (role === 'user' && title === 'Codex Session' && text) {
|
|
703
|
+
title = text;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// Interactive Codex writes cumulative totals in token_count events,
|
|
707
|
+
// rather than a top-level usage field. Keep the latest total instead
|
|
708
|
+
// of summing snapshots from each turn.
|
|
709
|
+
if (record.type === 'event_msg' && record.payload?.type === 'token_count') {
|
|
710
|
+
const total = record.payload.info?.total_token_usage;
|
|
711
|
+
if (total) {
|
|
712
|
+
const parsed = extractRecordUsage({ usage: total }).usage;
|
|
713
|
+
if (parsed) {
|
|
714
|
+
if (typeof total.cache_write_input_tokens === 'number') {
|
|
715
|
+
parsed.cachedInputTokens = (typeof total.cached_input_tokens === 'number' ? total.cached_input_tokens : 0)
|
|
716
|
+
+ total.cache_write_input_tokens;
|
|
717
|
+
}
|
|
718
|
+
reportedTotalUsage = parsed;
|
|
709
719
|
}
|
|
710
|
-
reportedTotalUsage = parsed;
|
|
711
720
|
}
|
|
712
721
|
}
|
|
722
|
+
const { usage: turnUsage, quota: turnQuota } = extractRecordUsage(record);
|
|
723
|
+
if (turnUsage)
|
|
724
|
+
sessionUsage = accumulateUsage(sessionUsage, turnUsage);
|
|
725
|
+
if (turnQuota)
|
|
726
|
+
latestQuota = turnQuota;
|
|
713
727
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
728
|
+
// Match on the recorded cwd; fall back to a content scan for older files.
|
|
729
|
+
const matched = sessionCwd
|
|
730
|
+
? isPathMatch(sessionCwd)
|
|
731
|
+
: (content.toLowerCase().includes(normWorkspace) || content.includes(wsFolderName));
|
|
732
|
+
if (!matched)
|
|
733
|
+
continue;
|
|
734
|
+
// Skip contentless sessions (only injected context, no real turns).
|
|
735
|
+
if (messageCount === 0)
|
|
736
|
+
continue;
|
|
737
|
+
// Current rollout filenames include timestamps. The stable resume id is
|
|
738
|
+
// session_meta.payload.id, not the filename.
|
|
739
|
+
if (!sessionId)
|
|
740
|
+
continue;
|
|
741
|
+
sessions.push({
|
|
742
|
+
id: sessionId,
|
|
743
|
+
assistant: 'codex',
|
|
744
|
+
title: sanitizeSessionTitle(title),
|
|
745
|
+
...times.values(),
|
|
746
|
+
messageCount,
|
|
747
|
+
workspacePath: resolveTargetCwd(sessionCwd || undefined),
|
|
748
|
+
recordedCwd: sessionCwd ?? undefined,
|
|
749
|
+
...identity,
|
|
750
|
+
...((reportedTotalUsage ?? sessionUsage) ? { usage: reportedTotalUsage ?? sessionUsage } : {}),
|
|
751
|
+
...(latestQuota ? { quota: latestQuota } : {}),
|
|
752
|
+
});
|
|
719
753
|
}
|
|
720
|
-
|
|
721
|
-
const matched = sessionCwd
|
|
722
|
-
? isPathMatch(sessionCwd)
|
|
723
|
-
: (content.toLowerCase().includes(normWorkspace) || content.includes(wsFolderName));
|
|
724
|
-
if (!matched)
|
|
725
|
-
continue;
|
|
726
|
-
// Skip contentless sessions (only injected context, no real turns).
|
|
727
|
-
if (messageCount === 0)
|
|
728
|
-
continue;
|
|
729
|
-
// Current rollout filenames include timestamps. The stable resume id is
|
|
730
|
-
// session_meta.payload.id, not the filename.
|
|
731
|
-
if (!sessionId)
|
|
732
|
-
continue;
|
|
733
|
-
sessions.push({
|
|
734
|
-
id: sessionId,
|
|
735
|
-
assistant: 'codex',
|
|
736
|
-
title: sanitizeSessionTitle(title),
|
|
737
|
-
...times.values(),
|
|
738
|
-
messageCount,
|
|
739
|
-
workspacePath: resolveTargetCwd(sessionCwd || undefined),
|
|
740
|
-
recordedCwd: sessionCwd ?? undefined,
|
|
741
|
-
...identity,
|
|
742
|
-
...((reportedTotalUsage ?? sessionUsage) ? { usage: reportedTotalUsage ?? sessionUsage } : {}),
|
|
743
|
-
...(latestQuota ? { quota: latestQuota } : {}),
|
|
744
|
-
});
|
|
754
|
+
catch { }
|
|
745
755
|
}
|
|
746
|
-
catch { }
|
|
747
756
|
}
|
|
757
|
+
catch { }
|
|
748
758
|
}
|
|
749
|
-
catch { }
|
|
750
759
|
// ─── 4. GitHub Copilot Sessions (SQLite session store) ───────────────────
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
760
|
+
if (!source || source === 'copilot') {
|
|
761
|
+
// Copilot keeps sessions in ~/.copilot/session-store.db: a `sessions` table
|
|
762
|
+
// (id, cwd, summary, timestamps) and a `turns` table (user_message /
|
|
763
|
+
// assistant_response per turn). Match on `cwd` like the other harnesses.
|
|
764
|
+
const copilotDb = await openCopilotDb();
|
|
765
|
+
if (copilotDb) {
|
|
766
|
+
try {
|
|
767
|
+
const rows = copilotDb.prepare('SELECT id, cwd, summary, created_at, updated_at FROM sessions').all();
|
|
768
|
+
for (const row of rows) {
|
|
769
|
+
if (!isPathMatch(row.cwd))
|
|
770
|
+
continue;
|
|
771
|
+
const counted = copilotDb.prepare(`SELECT
|
|
762
772
|
SUM(CASE WHEN TRIM(COALESCE(user_message, '')) != '' THEN 1 ELSE 0 END)
|
|
763
773
|
+ SUM(CASE WHEN TRIM(COALESCE(assistant_response, '')) != '' THEN 1 ELSE 0 END) AS n
|
|
764
774
|
FROM turns WHERE session_id = ?`).get(row.id);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
775
|
+
const messageCount = Number(counted?.n ?? 0);
|
|
776
|
+
let title = (row.summary || '').trim();
|
|
777
|
+
if (!title) {
|
|
778
|
+
const first = copilotDb.prepare(`SELECT user_message FROM turns
|
|
769
779
|
WHERE session_id = ? AND TRIM(COALESCE(user_message, '')) != ''
|
|
770
780
|
AND TRIM(user_message) NOT LIKE '<%'
|
|
771
781
|
ORDER BY turn_index LIMIT 1`).get(row.id);
|
|
772
|
-
|
|
782
|
+
title = (first?.user_message || '').trim();
|
|
783
|
+
}
|
|
784
|
+
// Skip contentless sessions (no summary and no turns).
|
|
785
|
+
if (!title && messageCount === 0)
|
|
786
|
+
continue;
|
|
787
|
+
if (!title)
|
|
788
|
+
title = 'Copilot Session';
|
|
789
|
+
sessions.push({
|
|
790
|
+
id: row.id,
|
|
791
|
+
assistant: 'copilot',
|
|
792
|
+
title: sanitizeSessionTitle(title),
|
|
793
|
+
createdAt: sessionTimestamp(row.created_at) ?? '',
|
|
794
|
+
updatedAt: sessionTimestamp(row.updated_at) ?? '',
|
|
795
|
+
messageCount,
|
|
796
|
+
workspacePath: resolveTargetCwd(row.cwd),
|
|
797
|
+
recordedCwd: row.cwd,
|
|
798
|
+
threadKind: 'unknown',
|
|
799
|
+
});
|
|
773
800
|
}
|
|
774
|
-
// Skip contentless sessions (no summary and no turns).
|
|
775
|
-
if (!title && messageCount === 0)
|
|
776
|
-
continue;
|
|
777
|
-
if (!title)
|
|
778
|
-
title = 'Copilot Session';
|
|
779
|
-
sessions.push({
|
|
780
|
-
id: row.id,
|
|
781
|
-
assistant: 'copilot',
|
|
782
|
-
title: sanitizeSessionTitle(title),
|
|
783
|
-
createdAt: sessionTimestamp(row.created_at) ?? '',
|
|
784
|
-
updatedAt: sessionTimestamp(row.updated_at) ?? '',
|
|
785
|
-
messageCount,
|
|
786
|
-
workspacePath: resolveTargetCwd(row.cwd),
|
|
787
|
-
recordedCwd: row.cwd,
|
|
788
|
-
threadKind: 'unknown',
|
|
789
|
-
});
|
|
790
801
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
802
|
+
catch {
|
|
803
|
+
// Leave Copilot out rather than fail the whole listing.
|
|
804
|
+
}
|
|
805
|
+
finally {
|
|
806
|
+
try {
|
|
807
|
+
copilotDb.close();
|
|
808
|
+
}
|
|
809
|
+
catch { }
|
|
798
810
|
}
|
|
799
|
-
catch { }
|
|
800
811
|
}
|
|
801
812
|
}
|
|
802
|
-
// ───
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
let latestQuota;
|
|
819
|
-
for (const line of lines) {
|
|
820
|
-
let record;
|
|
821
|
-
try {
|
|
822
|
-
record = JSON.parse(line);
|
|
823
|
-
}
|
|
824
|
-
catch {
|
|
813
|
+
// ─── Modular history readers ────────────────────────────────────────────
|
|
814
|
+
const scanContext = { roots: [workspacePath, ...repoPaths], matchesCwd: isPathMatch, targetCwd: resolveTargetCwd };
|
|
815
|
+
for (const [id, reader] of Object.entries(MODULAR_SESSION_READERS)) {
|
|
816
|
+
if (!source || source === id)
|
|
817
|
+
sessions.push(...await reader.list(scanContext));
|
|
818
|
+
}
|
|
819
|
+
// ─── Scan Workspace .sessions Directory ──────────────────────────────────
|
|
820
|
+
if (!source || source === 'workspace') {
|
|
821
|
+
try {
|
|
822
|
+
const wsSessionsDir = path.join(workspacePath, '.sessions');
|
|
823
|
+
const sessionFiles = await getFilesRecursively(wsSessionsDir, '.jsonl').catch(() => []);
|
|
824
|
+
for (const filePath of sessionFiles) {
|
|
825
|
+
try {
|
|
826
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
827
|
+
const lines = content.split('\n').filter(Boolean);
|
|
828
|
+
if (lines.length === 0)
|
|
825
829
|
continue;
|
|
830
|
+
let sessionId = path.basename(filePath, '.jsonl');
|
|
831
|
+
let assistant = 'claude';
|
|
832
|
+
let title = 'AI Session';
|
|
833
|
+
let messageCount = 0;
|
|
834
|
+
const times = sessionTimes();
|
|
835
|
+
let sessionUsage;
|
|
836
|
+
let latestQuota;
|
|
837
|
+
for (const line of lines) {
|
|
838
|
+
let record;
|
|
839
|
+
try {
|
|
840
|
+
record = JSON.parse(line);
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
times.add(record.timestamp);
|
|
846
|
+
if (record.sessionId)
|
|
847
|
+
sessionId = record.sessionId;
|
|
848
|
+
if (record.provider) {
|
|
849
|
+
const p = String(record.provider).toLowerCase();
|
|
850
|
+
if (p.includes('claude'))
|
|
851
|
+
assistant = 'claude';
|
|
852
|
+
else if (p.includes('codex'))
|
|
853
|
+
assistant = 'codex';
|
|
854
|
+
else if (p.includes('antigravity') || p.includes('gemini'))
|
|
855
|
+
assistant = 'antigravity';
|
|
856
|
+
else if (p.includes('copilot'))
|
|
857
|
+
assistant = 'copilot';
|
|
858
|
+
else if (p === 'pi')
|
|
859
|
+
assistant = 'pi';
|
|
860
|
+
}
|
|
861
|
+
if (record.userPrompt && title === 'AI Session') {
|
|
862
|
+
title = String(record.userPrompt).trim();
|
|
863
|
+
}
|
|
864
|
+
if (record.userPrompt || record.assistantResponse) {
|
|
865
|
+
messageCount += (record.userPrompt ? 1 : 0) + (record.assistantResponse ? 1 : 0);
|
|
866
|
+
}
|
|
867
|
+
const { usage: turnUsage, quota: turnQuota } = extractRecordUsage(record);
|
|
868
|
+
if (turnUsage)
|
|
869
|
+
sessionUsage = accumulateUsage(sessionUsage, turnUsage);
|
|
870
|
+
if (turnQuota)
|
|
871
|
+
latestQuota = turnQuota;
|
|
826
872
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
assistant = 'copilot';
|
|
840
|
-
}
|
|
841
|
-
if (record.userPrompt && title === 'AI Session') {
|
|
842
|
-
title = String(record.userPrompt).trim();
|
|
843
|
-
}
|
|
844
|
-
if (record.userPrompt || record.assistantResponse) {
|
|
845
|
-
messageCount += (record.userPrompt ? 1 : 0) + (record.assistantResponse ? 1 : 0);
|
|
873
|
+
if (!sessions.some((s) => s.id === sessionId)) {
|
|
874
|
+
sessions.push({
|
|
875
|
+
id: sessionId,
|
|
876
|
+
assistant,
|
|
877
|
+
title: sanitizeSessionTitle(title),
|
|
878
|
+
...times.values(),
|
|
879
|
+
messageCount: messageCount || lines.length,
|
|
880
|
+
workspacePath,
|
|
881
|
+
threadKind: 'unknown',
|
|
882
|
+
...(sessionUsage ? { usage: sessionUsage } : {}),
|
|
883
|
+
...(latestQuota ? { quota: latestQuota } : {}),
|
|
884
|
+
});
|
|
846
885
|
}
|
|
847
|
-
const { usage: turnUsage, quota: turnQuota } = extractRecordUsage(record);
|
|
848
|
-
if (turnUsage)
|
|
849
|
-
sessionUsage = accumulateUsage(sessionUsage, turnUsage);
|
|
850
|
-
if (turnQuota)
|
|
851
|
-
latestQuota = turnQuota;
|
|
852
|
-
}
|
|
853
|
-
if (!sessions.some((s) => s.id === sessionId)) {
|
|
854
|
-
sessions.push({
|
|
855
|
-
id: sessionId,
|
|
856
|
-
assistant,
|
|
857
|
-
title: sanitizeSessionTitle(title),
|
|
858
|
-
...times.values(),
|
|
859
|
-
messageCount: messageCount || lines.length,
|
|
860
|
-
workspacePath,
|
|
861
|
-
threadKind: 'unknown',
|
|
862
|
-
...(sessionUsage ? { usage: sessionUsage } : {}),
|
|
863
|
-
...(latestQuota ? { quota: latestQuota } : {}),
|
|
864
|
-
});
|
|
865
886
|
}
|
|
887
|
+
catch { }
|
|
866
888
|
}
|
|
867
|
-
catch { }
|
|
868
889
|
}
|
|
890
|
+
catch { }
|
|
869
891
|
}
|
|
870
|
-
catch { }
|
|
871
892
|
// Sort by updatedAt descending
|
|
872
893
|
sessions.sort((a, b) => (Date.parse(b.updatedAt) || 0) - (Date.parse(a.updatedAt) || 0));
|
|
873
894
|
return sessions;
|
|
@@ -879,7 +900,7 @@ export async function findSessions(workspacePath, repoPaths = []) {
|
|
|
879
900
|
*
|
|
880
901
|
* @param workspacePath - Root directory of the active workspace.
|
|
881
902
|
* @param repoPaths - Directories of sub-repositories included in the workspace.
|
|
882
|
-
* @returns A promise that resolves to an array of unique
|
|
903
|
+
* @returns A promise that resolves to an array of unique session assistant names.
|
|
883
904
|
*/
|
|
884
905
|
// ─── High-Performance mtime-aware Session Caches ────────────────────────────
|
|
885
906
|
let agHistoryCache = null;
|
|
@@ -1000,7 +1021,7 @@ async function getCodexCwds(codexHome) {
|
|
|
1000
1021
|
*
|
|
1001
1022
|
* @param workspacePath - Root directory of the active workspace.
|
|
1002
1023
|
* @param repoPaths - Directories of sub-repositories included in the workspace.
|
|
1003
|
-
* @returns A promise that resolves to an array of unique
|
|
1024
|
+
* @returns A promise that resolves to an array of unique session assistant names.
|
|
1004
1025
|
*/
|
|
1005
1026
|
export async function findActiveAssistants(workspacePath, repoPaths = []) {
|
|
1006
1027
|
const active = new Set();
|
|
@@ -1058,19 +1079,27 @@ export async function findActiveAssistants(workspacePath, repoPaths = []) {
|
|
|
1058
1079
|
if (copilotCwds.some(isPathMatch)) {
|
|
1059
1080
|
active.add('copilot');
|
|
1060
1081
|
}
|
|
1082
|
+
for (const [id, reader] of Object.entries(MODULAR_SESSION_READERS)) {
|
|
1083
|
+
if (await reader.hasAny({ roots: [workspacePath, ...repoPaths], matchesCwd: isPathMatch }))
|
|
1084
|
+
active.add(id);
|
|
1085
|
+
}
|
|
1061
1086
|
return Array.from(active);
|
|
1062
1087
|
}
|
|
1063
1088
|
/**
|
|
1064
1089
|
* Reads and parses the full transcript for a specific assistant conversation session.
|
|
1065
1090
|
*
|
|
1066
|
-
* @param assistant - The name of the AI assistant ('antigravity', 'claude', 'codex', 'copilot').
|
|
1091
|
+
* @param assistant - The name of the AI assistant ('antigravity', 'claude', 'codex', 'copilot', 'pi').
|
|
1067
1092
|
* @param sessionId - Unique ID of the conversation session.
|
|
1068
1093
|
* @returns A promise that resolves to an array of {@link ChatMessage} objects.
|
|
1069
1094
|
*/
|
|
1070
1095
|
export async function getSessionTranscript(assistant, sessionId) {
|
|
1071
1096
|
assertSafeSessionId(assistant, sessionId);
|
|
1072
1097
|
const messages = [];
|
|
1073
|
-
|
|
1098
|
+
const modularReader = modularSessionReader(assistant);
|
|
1099
|
+
if (modularReader) {
|
|
1100
|
+
messages.push(...await modularReader.transcript(sessionId));
|
|
1101
|
+
}
|
|
1102
|
+
else if (assistant === 'antigravity') {
|
|
1074
1103
|
const agDir = getAntigravityDir();
|
|
1075
1104
|
const transcriptPath = path.join(agDir, 'brain', sessionId, '.system_generated', 'logs', 'transcript.jsonl');
|
|
1076
1105
|
const content = await fs.readFile(transcriptPath, 'utf-8');
|