spiracha 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +27 -12
- package/README.md +42 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +36 -11
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +46 -3
- package/apps/ui/src/components/breadcrumbs.tsx +21 -3
- package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
- package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
- package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
- package/apps/ui/src/components/data-table.tsx +23 -5
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +79 -74
- package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
- package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
- package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
- package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
- package/apps/ui/src/components/page-header.tsx +15 -9
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
- package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
- package/apps/ui/src/components/threads-table.tsx +13 -47
- package/apps/ui/src/components/transcript-view.tsx +148 -52
- package/apps/ui/src/components/ui/select.tsx +1 -1
- package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
- package/apps/ui/src/lib/antigravity-server.ts +209 -11
- package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +185 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +16 -0
- package/apps/ui/src/lib/codex-server.ts +134 -52
- package/apps/ui/src/lib/cursor-server.ts +68 -53
- package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
- package/apps/ui/src/lib/delete-batch.ts +52 -0
- package/apps/ui/src/lib/error-presentation.ts +43 -0
- package/apps/ui/src/lib/export-mutation.ts +20 -0
- package/apps/ui/src/lib/export-options.ts +15 -0
- package/apps/ui/src/lib/formatters.ts +3 -5
- package/apps/ui/src/lib/grok-queries.ts +22 -0
- package/apps/ui/src/lib/grok-server.ts +168 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +97 -7
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +96 -7
- package/apps/ui/src/lib/path-utils.ts +1 -1
- package/apps/ui/src/lib/qoder-queries.ts +15 -0
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/route-state-reset.tsx +10 -0
- package/apps/ui/src/lib/settings-server.ts +33 -0
- package/apps/ui/src/lib/settings-store.tsx +82 -38
- package/apps/ui/src/lib/settings.ts +65 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
- package/apps/ui/src/routeTree.gen.ts +341 -234
- package/apps/ui/src/routes/__root.tsx +12 -15
- package/apps/ui/src/routes/analytics.tsx +10 -61
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +149 -29
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
- package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
- package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +168 -28
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
- package/apps/ui/src/routes/claude-code.index.tsx +2 -2
- package/apps/ui/src/routes/codex.$project.tsx +33 -31
- package/apps/ui/src/routes/codex.index.tsx +5 -10
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +31 -18
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
- package/apps/ui/src/routes/opencode.index.tsx +6 -2
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +37 -27
- package/src/client.ts +183 -5
- package/src/lib/antigravity-db.ts +424 -94
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +534 -42
- package/src/lib/claude-code-exporter-types.ts +5 -0
- package/src/lib/claude-code-transcript-phase.ts +60 -1
- package/src/lib/claude-code-transcript.ts +8 -5
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +18 -27
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +106 -23
- package/src/lib/codex-thread-parser.ts +149 -55
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +87 -79
- package/src/lib/concurrency.ts +56 -3
- package/src/lib/conversation-api.ts +410 -27
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
- package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
- package/src/lib/conversation-data/codex-adapter.ts +108 -73
- package/src/lib/conversation-data/cursor-adapter.ts +77 -29
- package/src/lib/conversation-data/grok-adapter.ts +223 -0
- package/src/lib/conversation-data/index.ts +105 -11
- package/src/lib/conversation-data/kiro-adapter.ts +59 -12
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +67 -17
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +67 -37
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +75 -0
- package/src/lib/cursor-db.ts +68 -28
- package/src/lib/cursor-id.ts +17 -0
- package/src/lib/cursor-recovery.ts +146 -35
- package/src/lib/cursor-transcript-phase.ts +40 -0
- package/src/lib/cursor-transcript.ts +21 -5
- package/src/lib/grok-db.ts +1068 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +61 -0
- package/src/lib/grok-transcript.ts +167 -0
- package/src/lib/kiro-db.ts +187 -43
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +599 -60
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/portable-path.ts +9 -0
- package/src/lib/qoder-acp-client.ts +11 -2
- package/src/lib/qoder-db.ts +51 -17
- package/src/lib/qoder-transcript-phase.ts +64 -0
- package/src/lib/qoder-transcript.ts +30 -2
- package/src/lib/shared.ts +63 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +87 -0
- package/src/lib/ui-cache.ts +185 -24
- package/src/lib/ui-export-archive.ts +58 -39
- package/src/lib/ui-export-files.ts +75 -15
- package/src/lib/ui-export-zip.ts +40 -0
- package/apps/ui/package.json +0 -65
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, stat } from 'node:fs/promises';
|
|
1
|
+
import { readdir, stat, unlink } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {
|
|
4
4
|
type ClaudeCodeSessionSummary,
|
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
getDefaultClaudeCodeDataDir,
|
|
10
10
|
resolveClaudeCodeProjectsDir,
|
|
11
11
|
} from './claude-code-exporter-types';
|
|
12
|
+
import {
|
|
13
|
+
getClaudeCodeAssistantMessagePhase,
|
|
14
|
+
isClaudeCodeRawFlagEnabled,
|
|
15
|
+
isClaudeCodeSyntheticTranscriptEntry,
|
|
16
|
+
} from './claude-code-transcript-phase';
|
|
12
17
|
import { mapWithConcurrency } from './concurrency';
|
|
18
|
+
import { getPortablePathBasename } from './portable-path';
|
|
13
19
|
import {
|
|
14
20
|
asBoolean,
|
|
15
21
|
asNumber,
|
|
@@ -17,9 +23,9 @@ import {
|
|
|
17
23
|
asString,
|
|
18
24
|
cleanExtractedText,
|
|
19
25
|
cleanInlineTitle,
|
|
20
|
-
getPortablePathBasename,
|
|
21
26
|
isWorkspacePathQuery,
|
|
22
27
|
type JsonValue,
|
|
28
|
+
readDirectoryEntriesIfExists,
|
|
23
29
|
readJsonlObjects,
|
|
24
30
|
workspacePathMatchesQuery,
|
|
25
31
|
} from './shared';
|
|
@@ -38,6 +44,18 @@ type ParsedTranscriptFile = {
|
|
|
38
44
|
transcript: ClaudeCodeSessionTranscript;
|
|
39
45
|
};
|
|
40
46
|
|
|
47
|
+
type ReadTranscriptFileOptions = {
|
|
48
|
+
includeRawPayloads?: boolean;
|
|
49
|
+
maxRawPayloadFileSizeBytes?: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type ReadClaudeCodeSessionTranscriptOptions = ReadTranscriptFileOptions;
|
|
53
|
+
|
|
54
|
+
export type DeleteClaudeCodeSessionResult = {
|
|
55
|
+
deletedFiles: string[];
|
|
56
|
+
deletedSessionIds: string[];
|
|
57
|
+
};
|
|
58
|
+
|
|
41
59
|
type SessionStats = {
|
|
42
60
|
assistantMessageCount: number;
|
|
43
61
|
attachmentCount: number;
|
|
@@ -52,6 +70,11 @@ type SessionStats = {
|
|
|
52
70
|
userMessageCount: number;
|
|
53
71
|
};
|
|
54
72
|
|
|
73
|
+
type SessionStatsTracker = {
|
|
74
|
+
assistantMessageIds: Set<string>;
|
|
75
|
+
usageMessageIds: Set<string>;
|
|
76
|
+
};
|
|
77
|
+
|
|
55
78
|
type SessionIdentity = {
|
|
56
79
|
cwd: string | null;
|
|
57
80
|
firstUserText: string | null;
|
|
@@ -75,6 +98,18 @@ const pathExists = async (target: string): Promise<boolean> => {
|
|
|
75
98
|
.catch(() => false);
|
|
76
99
|
};
|
|
77
100
|
|
|
101
|
+
const unlinkIfPresent = async (target: string): Promise<boolean> => {
|
|
102
|
+
try {
|
|
103
|
+
await unlink(target);
|
|
104
|
+
return true;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
78
113
|
const cleanLabel = (value: string | null | undefined): string | null => {
|
|
79
114
|
const cleaned = value?.replace(/\s+/g, ' ').trim();
|
|
80
115
|
return cleaned ? cleaned : null;
|
|
@@ -281,19 +316,21 @@ const parseTranscriptEntry = (raw: Record<string, JsonValue>): ClaudeCodeTranscr
|
|
|
281
316
|
const type = asString(raw.type ?? null) ?? 'unknown';
|
|
282
317
|
const message = asObject(raw.message ?? null);
|
|
283
318
|
const parts = getTranscriptEntryParts(type, message, raw);
|
|
319
|
+
const role = getTranscriptEntryRole(type, message);
|
|
284
320
|
|
|
285
321
|
if (parts.length === 0) {
|
|
286
322
|
return null;
|
|
287
323
|
}
|
|
288
324
|
|
|
289
325
|
return {
|
|
326
|
+
assistantPhase: getClaudeCodeAssistantMessagePhase({ parts, raw, role }),
|
|
290
327
|
cwd: asString(raw.cwd ?? null),
|
|
291
328
|
entryId: getTranscriptEntryId(type, raw),
|
|
292
329
|
model: asString(message?.model ?? null),
|
|
293
330
|
parentEntryId: asString(raw.parentUuid ?? null),
|
|
294
331
|
parts,
|
|
295
332
|
raw,
|
|
296
|
-
role
|
|
333
|
+
role,
|
|
297
334
|
timestamp: asString(raw.timestamp ?? null),
|
|
298
335
|
type,
|
|
299
336
|
};
|
|
@@ -313,30 +350,86 @@ const createEmptyStats = (): SessionStats => ({
|
|
|
313
350
|
userMessageCount: 0,
|
|
314
351
|
});
|
|
315
352
|
|
|
316
|
-
const
|
|
353
|
+
const createStatsTracker = (): SessionStatsTracker => ({
|
|
354
|
+
assistantMessageIds: new Set(),
|
|
355
|
+
usageMessageIds: new Set(),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const getMessageIdentity = (entry: ClaudeCodeTranscriptEntry): string => {
|
|
359
|
+
const message = asObject(entry.raw.message ?? null);
|
|
360
|
+
return asString(message?.id ?? null) ?? entry.entryId;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const addUsageStats = (stats: SessionStats, tracker: SessionStatsTracker, entry: ClaudeCodeTranscriptEntry) => {
|
|
364
|
+
const message = asObject(entry.raw.message ?? null);
|
|
317
365
|
const usage = asObject(message?.usage ?? null);
|
|
318
366
|
if (!usage) {
|
|
319
367
|
return;
|
|
320
368
|
}
|
|
321
369
|
|
|
370
|
+
const messageId = getMessageIdentity(entry);
|
|
371
|
+
if (tracker.usageMessageIds.has(messageId)) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
tracker.usageMessageIds.add(messageId);
|
|
375
|
+
|
|
322
376
|
stats.inputTokens += asNumber(usage.input_tokens ?? null) ?? 0;
|
|
323
377
|
stats.outputTokens += asNumber(usage.output_tokens ?? null) ?? 0;
|
|
324
378
|
stats.cacheCreationInputTokens += asNumber(usage.cache_creation_input_tokens ?? null) ?? 0;
|
|
325
379
|
stats.cacheReadInputTokens += asNumber(usage.cache_read_input_tokens ?? null) ?? 0;
|
|
326
380
|
};
|
|
327
381
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
382
|
+
const hasTextPart = (entry: ClaudeCodeTranscriptEntry): boolean => {
|
|
383
|
+
return entry.parts.some((part) => part.type === 'text' && part.text?.trim());
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const extractUserPromptText = (text: string): string | null => {
|
|
387
|
+
const cleaned = cleanExtractedText(text).trim();
|
|
388
|
+
if (!cleaned.startsWith('<!-- attach -->')) {
|
|
389
|
+
return cleaned || null;
|
|
331
390
|
}
|
|
332
391
|
|
|
333
|
-
|
|
392
|
+
const lines = cleaned.split('\n').slice(1);
|
|
393
|
+
let sawQuotedAttachment = false;
|
|
394
|
+
for (const [index, line] of lines.entries()) {
|
|
395
|
+
const trimmed = line.trim();
|
|
396
|
+
if (trimmed.startsWith('>')) {
|
|
397
|
+
sawQuotedAttachment = true;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (!trimmed) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (sawQuotedAttachment) {
|
|
404
|
+
return lines.slice(index).join('\n').trim() || null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return null;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const updateMessageStats = (stats: SessionStats, tracker: SessionStatsTracker, entry: ClaudeCodeTranscriptEntry) => {
|
|
412
|
+
if (entry.role === 'user' && hasTextPart(entry) && !isClaudeCodeSyntheticTranscriptEntry(entry)) {
|
|
413
|
+
stats.messageCount += 1;
|
|
334
414
|
stats.userMessageCount += 1;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (entry.role !== 'assistant' || !hasTextPart(entry)) {
|
|
419
|
+
return;
|
|
335
420
|
}
|
|
336
421
|
|
|
337
|
-
|
|
338
|
-
|
|
422
|
+
const messageId = getMessageIdentity(entry);
|
|
423
|
+
if (tracker.assistantMessageIds.has(messageId)) {
|
|
424
|
+
return;
|
|
339
425
|
}
|
|
426
|
+
tracker.assistantMessageIds.add(messageId);
|
|
427
|
+
stats.messageCount += 1;
|
|
428
|
+
stats.assistantMessageCount += 1;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const updateStatsFromEntry = (stats: SessionStats, tracker: SessionStatsTracker, entry: ClaudeCodeTranscriptEntry) => {
|
|
432
|
+
updateMessageStats(stats, tracker, entry);
|
|
340
433
|
|
|
341
434
|
if (entry.type === 'attachment') {
|
|
342
435
|
stats.attachmentCount += 1;
|
|
@@ -345,7 +438,7 @@ const updateStatsFromEntry = (stats: SessionStats, entry: ClaudeCodeTranscriptEn
|
|
|
345
438
|
stats.toolCallCount += entry.parts.filter((part) => part.type === 'tool_use').length;
|
|
346
439
|
stats.toolResultCount += entry.parts.filter((part) => part.type === 'tool_result').length;
|
|
347
440
|
stats.renderablePartCount += entry.parts.filter(isRenderablePart).length;
|
|
348
|
-
addUsageStats(stats,
|
|
441
|
+
addUsageStats(stats, tracker, entry);
|
|
349
442
|
};
|
|
350
443
|
|
|
351
444
|
const updateTimeline = (
|
|
@@ -382,9 +475,9 @@ const updateIdentityFromRaw = (identity: SessionIdentity, raw: Record<string, Js
|
|
|
382
475
|
};
|
|
383
476
|
|
|
384
477
|
const updateIdentityFromEntry = (identity: SessionIdentity, entry: ClaudeCodeTranscriptEntry) => {
|
|
385
|
-
if (entry.role === 'user' && !identity.firstUserText) {
|
|
478
|
+
if (entry.role === 'user' && !identity.firstUserText && !isClaudeCodeSyntheticTranscriptEntry(entry)) {
|
|
386
479
|
const textPart = entry.parts.find((part) => part.type === 'text' && part.text?.trim());
|
|
387
|
-
identity.firstUserText =
|
|
480
|
+
identity.firstUserText = extractUserPromptText(textPart?.text ?? '');
|
|
388
481
|
}
|
|
389
482
|
};
|
|
390
483
|
|
|
@@ -450,10 +543,59 @@ const isRenderablePart = (part: ClaudeCodeTranscriptPart): boolean => {
|
|
|
450
543
|
return false;
|
|
451
544
|
};
|
|
452
545
|
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
.
|
|
546
|
+
const stripEntryRawPayloads = (entry: ClaudeCodeTranscriptEntry): ClaudeCodeTranscriptEntry => {
|
|
547
|
+
return {
|
|
548
|
+
...entry,
|
|
549
|
+
parts: entry.parts.map((part) => ({ ...part, raw: {} })),
|
|
550
|
+
raw: {},
|
|
551
|
+
};
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const markIncompleteAssistantParentAsCommentary = (
|
|
555
|
+
entries: ClaudeCodeTranscriptEntry[],
|
|
556
|
+
entry: ClaudeCodeTranscriptEntry,
|
|
557
|
+
): void => {
|
|
558
|
+
const interruptedByUser =
|
|
559
|
+
entry.role === 'user' &&
|
|
560
|
+
entry.parts.some(
|
|
561
|
+
(part) => part.type === 'text' && part.text?.trim().startsWith('[Request interrupted by user]'),
|
|
562
|
+
);
|
|
563
|
+
if ((!interruptedByUser && !isClaudeCodeRawFlagEnabled(entry.raw.isApiErrorMessage)) || !entry.parentEntryId) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
568
|
+
const candidate = entries[index];
|
|
569
|
+
if (candidate?.entryId === entry.parentEntryId && candidate.role === 'assistant') {
|
|
570
|
+
candidate.assistantPhase = 'commentary';
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const markLatestAssistantAsCommentary = (entries: ClaudeCodeTranscriptEntry[]): void => {
|
|
577
|
+
const latestEntry = entries.at(-1);
|
|
578
|
+
if (latestEntry?.role === 'assistant') {
|
|
579
|
+
latestEntry.assistantPhase = 'commentary';
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const isTaskNotification = (raw: Record<string, JsonValue>): boolean => {
|
|
584
|
+
const content =
|
|
585
|
+
raw.type === 'queue-operation'
|
|
586
|
+
? asString(raw.content ?? null)
|
|
587
|
+
: raw.type === 'user'
|
|
588
|
+
? textFromContentValue(asObject(raw.message ?? null)?.content)
|
|
589
|
+
: null;
|
|
590
|
+
return content?.trimStart().startsWith('<task-notification>') === true;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const buildTranscriptFromRawEvents = (
|
|
594
|
+
file: TranscriptFile,
|
|
595
|
+
sourceRawEvents: Record<string, JsonValue>[],
|
|
596
|
+
fallbackMtimeMs: number | null,
|
|
597
|
+
includeRawPayloads: boolean,
|
|
598
|
+
): ClaudeCodeSessionTranscript => {
|
|
457
599
|
const identity: SessionIdentity = {
|
|
458
600
|
cwd: null,
|
|
459
601
|
firstUserText: null,
|
|
@@ -464,39 +606,72 @@ const readTranscriptFile = async (file: TranscriptFile): Promise<ParsedTranscrip
|
|
|
464
606
|
version: null,
|
|
465
607
|
};
|
|
466
608
|
const stats = createEmptyStats();
|
|
609
|
+
const statsTracker = createStatsTracker();
|
|
467
610
|
const timeline = { firstMs: null as number | null, lastMs: null as number | null };
|
|
468
611
|
const entries: ClaudeCodeTranscriptEntry[] = [];
|
|
469
|
-
const rawEvents: Record<string, JsonValue>[] = [];
|
|
470
612
|
|
|
471
|
-
for
|
|
472
|
-
rawEvents.push(raw);
|
|
613
|
+
for (const raw of sourceRawEvents) {
|
|
473
614
|
updateTimeline(timeline, raw);
|
|
474
615
|
updateIdentityFromRaw(identity, raw);
|
|
475
616
|
|
|
617
|
+
if (isTaskNotification(raw)) {
|
|
618
|
+
markLatestAssistantAsCommentary(entries);
|
|
619
|
+
}
|
|
620
|
+
|
|
476
621
|
const entry = parseTranscriptEntry(raw);
|
|
477
622
|
if (!entry) {
|
|
478
623
|
continue;
|
|
479
624
|
}
|
|
480
625
|
|
|
481
|
-
entries
|
|
482
|
-
|
|
626
|
+
markIncompleteAssistantParentAsCommentary(entries, entry);
|
|
627
|
+
if (isClaudeCodeSyntheticTranscriptEntry(entry)) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
entries.push(includeRawPayloads ? entry : stripEntryRawPayloads(entry));
|
|
632
|
+
updateStatsFromEntry(stats, statsTracker, entry);
|
|
483
633
|
updateIdentityFromEntry(identity, entry);
|
|
484
634
|
}
|
|
485
635
|
|
|
486
636
|
const session = toSessionSummary(file, identity, stats, toTimeline(timeline, fallbackMtimeMs));
|
|
487
637
|
return {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
638
|
+
entries,
|
|
639
|
+
rawEvents: includeRawPayloads ? sourceRawEvents : [],
|
|
640
|
+
rawPayloadsOmitted: includeRawPayloads ? undefined : true,
|
|
641
|
+
renderablePartCount: stats.renderablePartCount,
|
|
642
|
+
session,
|
|
643
|
+
};
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const readTranscriptFile = async (
|
|
647
|
+
file: TranscriptFile,
|
|
648
|
+
options: ReadTranscriptFileOptions = {},
|
|
649
|
+
): Promise<ParsedTranscriptFile | null> => {
|
|
650
|
+
const includeRawPayloads = options.includeRawPayloads ?? true;
|
|
651
|
+
const fallbackMtimeMs = await stat(file.filePath)
|
|
652
|
+
.then((stats) => stats.mtimeMs)
|
|
653
|
+
.catch(() => null);
|
|
654
|
+
const rawEvents: Record<string, JsonValue>[] = [];
|
|
655
|
+
|
|
656
|
+
try {
|
|
657
|
+
for await (const raw of readJsonlObjects(file.filePath)) {
|
|
658
|
+
rawEvents.push(raw);
|
|
659
|
+
}
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
transcript: buildTranscriptFromRawEvents(file, rawEvents, fallbackMtimeMs, includeRawPayloads),
|
|
494
669
|
};
|
|
495
670
|
};
|
|
496
671
|
|
|
497
672
|
const listTranscriptFilesForProject = async (projectsDir: string, directoryName: string): Promise<TranscriptFile[]> => {
|
|
498
673
|
const projectDir = path.join(projectsDir, directoryName);
|
|
499
|
-
const entries = await
|
|
674
|
+
const entries = await readDirectoryEntriesIfExists(projectDir);
|
|
500
675
|
return entries
|
|
501
676
|
.filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
502
677
|
.map((entry) => ({
|
|
@@ -521,16 +696,245 @@ const listTranscriptFiles = async (projectsDir: string): Promise<TranscriptFile[
|
|
|
521
696
|
};
|
|
522
697
|
|
|
523
698
|
const readTranscriptFiles = async (files: TranscriptFile[]): Promise<ClaudeCodeSessionTranscript[]> => {
|
|
524
|
-
const parsed = await mapWithConcurrency(files, READ_CONCURRENCY, readTranscriptFile);
|
|
699
|
+
const parsed = await mapWithConcurrency(files, READ_CONCURRENCY, (file) => readTranscriptFile(file));
|
|
525
700
|
return parsed.flatMap((item) => (item ? [item.transcript] : []));
|
|
526
701
|
};
|
|
527
702
|
|
|
703
|
+
const getCompactionSummaryId = (raw: Record<string, JsonValue>): string | null => {
|
|
704
|
+
return raw.isCompactSummary === true ? asString(raw.uuid ?? null) : null;
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const getCompactionSummaryIds = (transcript: ClaudeCodeSessionTranscript): string[] => {
|
|
708
|
+
return [...new Set(transcript.rawEvents.map(getCompactionSummaryId).filter((id): id is string => id !== null))];
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
type TranscriptLineageIndex = {
|
|
712
|
+
anchorsByIndex: string[][];
|
|
713
|
+
indexesByAnchor: Map<string, number[]>;
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const indexTranscriptLineageAnchors = (transcripts: ClaudeCodeSessionTranscript[]): TranscriptLineageIndex => {
|
|
717
|
+
const indexesByAnchor = new Map<string, number[]>();
|
|
718
|
+
const anchorsByIndex = transcripts.map((transcript, index) => {
|
|
719
|
+
const anchors = getCompactionSummaryIds(transcript).map(
|
|
720
|
+
(anchorId) => `${transcript.session.workspaceKey}\0${anchorId}`,
|
|
721
|
+
);
|
|
722
|
+
for (const anchor of anchors) {
|
|
723
|
+
const indexes = indexesByAnchor.get(anchor) ?? [];
|
|
724
|
+
indexes.push(index);
|
|
725
|
+
indexesByAnchor.set(anchor, indexes);
|
|
726
|
+
}
|
|
727
|
+
return anchors;
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
return { anchorsByIndex, indexesByAnchor };
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const collectTranscriptLineage = (
|
|
734
|
+
startIndex: number,
|
|
735
|
+
transcripts: ClaudeCodeSessionTranscript[],
|
|
736
|
+
index: TranscriptLineageIndex,
|
|
737
|
+
visited: Set<number>,
|
|
738
|
+
): ClaudeCodeSessionTranscript[] => {
|
|
739
|
+
const lineage: ClaudeCodeSessionTranscript[] = [];
|
|
740
|
+
const pending = [startIndex];
|
|
741
|
+
visited.add(startIndex);
|
|
742
|
+
|
|
743
|
+
while (pending.length > 0) {
|
|
744
|
+
const currentIndex = pending.pop() as number;
|
|
745
|
+
const transcript = transcripts[currentIndex];
|
|
746
|
+
if (!transcript) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
lineage.push(transcript);
|
|
750
|
+
|
|
751
|
+
const neighborIndexes = (index.anchorsByIndex[currentIndex] ?? []).flatMap(
|
|
752
|
+
(anchor) => index.indexesByAnchor.get(anchor) ?? [],
|
|
753
|
+
);
|
|
754
|
+
for (const neighborIndex of neighborIndexes) {
|
|
755
|
+
if (!visited.has(neighborIndex)) {
|
|
756
|
+
visited.add(neighborIndex);
|
|
757
|
+
pending.push(neighborIndex);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return lineage;
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
const getTranscriptLineages = (transcripts: ClaudeCodeSessionTranscript[]): ClaudeCodeSessionTranscript[][] => {
|
|
766
|
+
const index = indexTranscriptLineageAnchors(transcripts);
|
|
767
|
+
const visited = new Set<number>();
|
|
768
|
+
const lineages: ClaudeCodeSessionTranscript[][] = [];
|
|
769
|
+
|
|
770
|
+
for (const startIndex of transcripts.keys()) {
|
|
771
|
+
if (visited.has(startIndex)) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
lineages.push(collectTranscriptLineage(startIndex, transcripts, index, visited));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return lineages;
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const countContentEntriesBefore = (
|
|
781
|
+
transcript: ClaudeCodeSessionTranscript,
|
|
782
|
+
rawEventIndex: number,
|
|
783
|
+
prefixCounts: WeakMap<ClaudeCodeSessionTranscript, number[]>,
|
|
784
|
+
): number => {
|
|
785
|
+
let counts = prefixCounts.get(transcript);
|
|
786
|
+
if (!counts) {
|
|
787
|
+
counts = [0];
|
|
788
|
+
for (const raw of transcript.rawEvents) {
|
|
789
|
+
const entry = parseTranscriptEntry(raw);
|
|
790
|
+
counts.push((counts.at(-1) ?? 0) + (entry && !isClaudeCodeSyntheticTranscriptEntry(entry) ? 1 : 0));
|
|
791
|
+
}
|
|
792
|
+
prefixCounts.set(transcript, counts);
|
|
793
|
+
}
|
|
794
|
+
return counts[Math.max(0, Math.min(rawEventIndex, counts.length - 1))] ?? 0;
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
const deduplicateRawEvents = (rawEvents: Record<string, JsonValue>[]): Record<string, JsonValue>[] => {
|
|
798
|
+
const seenIds = new Set<string>();
|
|
799
|
+
return rawEvents.filter((raw) => {
|
|
800
|
+
const eventId = asString(raw.uuid ?? null);
|
|
801
|
+
if (!eventId) {
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
if (seenIds.has(eventId)) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
seenIds.add(eventId);
|
|
808
|
+
return true;
|
|
809
|
+
});
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
const MAX_CLAUDE_LINEAGE_DEPTH = 64;
|
|
813
|
+
|
|
814
|
+
const buildLogicalRawEvents = (
|
|
815
|
+
lineage: ClaudeCodeSessionTranscript[],
|
|
816
|
+
current: ClaudeCodeSessionTranscript,
|
|
817
|
+
context: {
|
|
818
|
+
memo: WeakMap<ClaudeCodeSessionTranscript, Record<string, JsonValue>[]>;
|
|
819
|
+
prefixCounts: WeakMap<ClaudeCodeSessionTranscript, number[]>;
|
|
820
|
+
} = { memo: new WeakMap(), prefixCounts: new WeakMap() },
|
|
821
|
+
visiting = new Set<string>(),
|
|
822
|
+
depth = 0,
|
|
823
|
+
): Record<string, JsonValue>[] => {
|
|
824
|
+
const cached = context.memo.get(current);
|
|
825
|
+
if (cached) {
|
|
826
|
+
return cached;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const finish = (events: Record<string, JsonValue>[]) => {
|
|
830
|
+
const deduplicated = deduplicateRawEvents(events);
|
|
831
|
+
context.memo.set(current, deduplicated);
|
|
832
|
+
return deduplicated;
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
if (visiting.has(current.session.sessionId) || depth >= MAX_CLAUDE_LINEAGE_DEPTH) {
|
|
836
|
+
return deduplicateRawEvents(current.rawEvents);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const firstAnchorIndex = current.rawEvents.findIndex((raw) => getCompactionSummaryId(raw) !== null);
|
|
840
|
+
const firstAnchor = firstAnchorIndex < 0 ? null : getCompactionSummaryId(current.rawEvents[firstAnchorIndex] ?? {});
|
|
841
|
+
if (!firstAnchor) {
|
|
842
|
+
return finish(current.rawEvents);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const currentPrefixSize = countContentEntriesBefore(current, firstAnchorIndex, context.prefixCounts);
|
|
846
|
+
const ancestor = lineage
|
|
847
|
+
.filter((candidate) => candidate.session.sessionId !== current.session.sessionId)
|
|
848
|
+
.map((candidate) => {
|
|
849
|
+
const anchorIndex = candidate.rawEvents.findIndex((raw) => getCompactionSummaryId(raw) === firstAnchor);
|
|
850
|
+
return {
|
|
851
|
+
anchorIndex,
|
|
852
|
+
candidate,
|
|
853
|
+
prefixSize:
|
|
854
|
+
anchorIndex < 0 ? -1 : countContentEntriesBefore(candidate, anchorIndex, context.prefixCounts),
|
|
855
|
+
};
|
|
856
|
+
})
|
|
857
|
+
.filter((candidate) => candidate.prefixSize > currentPrefixSize)
|
|
858
|
+
.sort(
|
|
859
|
+
(left, right) =>
|
|
860
|
+
right.prefixSize - left.prefixSize ||
|
|
861
|
+
(left.candidate.session.createdAtMs ?? 0) - (right.candidate.session.createdAtMs ?? 0),
|
|
862
|
+
)[0];
|
|
863
|
+
if (!ancestor) {
|
|
864
|
+
return finish(current.rawEvents);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const nextVisiting = new Set(visiting);
|
|
868
|
+
nextVisiting.add(current.session.sessionId);
|
|
869
|
+
const ancestorEvents = buildLogicalRawEvents(lineage, ancestor.candidate, context, nextVisiting, depth + 1);
|
|
870
|
+
const ancestorAnchorIndex = ancestorEvents.findIndex((raw) => getCompactionSummaryId(raw) === firstAnchor);
|
|
871
|
+
if (ancestorAnchorIndex < 0) {
|
|
872
|
+
return finish(current.rawEvents);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return finish([...ancestorEvents.slice(0, ancestorAnchorIndex), ...current.rawEvents.slice(firstAnchorIndex)]);
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
const compareTranscriptsByActivity = (
|
|
879
|
+
left: ClaudeCodeSessionTranscript,
|
|
880
|
+
right: ClaudeCodeSessionTranscript,
|
|
881
|
+
): number => {
|
|
882
|
+
return (
|
|
883
|
+
compareNullableMsDesc(left.session.lastActiveAtMs, right.session.lastActiveAtMs) ||
|
|
884
|
+
left.session.sessionId.localeCompare(right.session.sessionId)
|
|
885
|
+
);
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const coalesceTranscriptLineage = (lineage: ClaudeCodeSessionTranscript[]): ClaudeCodeSessionTranscript | null => {
|
|
889
|
+
const canonical = [...lineage].sort(compareTranscriptsByActivity)[0];
|
|
890
|
+
if (!canonical) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const directoryName = getDirectoryNameFromWorkspaceKey(canonical.session.workspaceKey);
|
|
895
|
+
if (!directoryName) {
|
|
896
|
+
return canonical;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const transcript = buildTranscriptFromRawEvents(
|
|
900
|
+
{ directoryName, filePath: canonical.session.filePath },
|
|
901
|
+
buildLogicalRawEvents(lineage, canonical),
|
|
902
|
+
canonical.session.lastActiveAtMs,
|
|
903
|
+
true,
|
|
904
|
+
);
|
|
905
|
+
transcript.session.filePath = canonical.session.filePath;
|
|
906
|
+
transcript.session.sessionId = canonical.session.sessionId;
|
|
907
|
+
return transcript;
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
const coalesceTranscriptLineages = (transcripts: ClaudeCodeSessionTranscript[]): ClaudeCodeSessionTranscript[] => {
|
|
911
|
+
return getTranscriptLineages(transcripts).flatMap((lineage) => {
|
|
912
|
+
const transcript = coalesceTranscriptLineage(lineage);
|
|
913
|
+
return transcript ? [transcript] : [];
|
|
914
|
+
});
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
const omitTranscriptRawPayloads = (transcript: ClaudeCodeSessionTranscript): ClaudeCodeSessionTranscript => ({
|
|
918
|
+
...transcript,
|
|
919
|
+
entries: transcript.entries.map(stripEntryRawPayloads),
|
|
920
|
+
rawEvents: [],
|
|
921
|
+
rawPayloadsOmitted: true,
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
const hasSessionContent = (transcript: ClaudeCodeSessionTranscript): boolean => {
|
|
925
|
+
return transcript.session.userMessageCount > 0 || transcript.session.assistantMessageCount > 0;
|
|
926
|
+
};
|
|
927
|
+
|
|
528
928
|
const compareNullableMsDesc = (left: number | null, right: number | null): number => {
|
|
529
929
|
return (right ?? 0) - (left ?? 0);
|
|
530
930
|
};
|
|
531
931
|
|
|
532
932
|
const toWorkspaceGroup = (directoryName: string, sessions: ClaudeCodeSessionSummary[]): ClaudeCodeWorkspaceGroup => {
|
|
533
|
-
const
|
|
933
|
+
const decodedWorktree = decodeWorktreeFromDirectoryName(directoryName);
|
|
934
|
+
const worktree =
|
|
935
|
+
sessions.find((session) => session.worktree !== decodedWorktree)?.worktree ??
|
|
936
|
+
sessions[0]?.worktree ??
|
|
937
|
+
decodedWorktree;
|
|
534
938
|
const lastActiveAtMs = sessions.reduce<number | null>((latest, session) => {
|
|
535
939
|
if (session.lastActiveAtMs === null) {
|
|
536
940
|
return latest;
|
|
@@ -560,7 +964,7 @@ export const listClaudeCodeWorkspaceGroups = async (
|
|
|
560
964
|
projectsDir = resolveClaudeCodeProjectsDir(),
|
|
561
965
|
): Promise<ClaudeCodeWorkspaceGroup[]> => {
|
|
562
966
|
const files = await listTranscriptFiles(projectsDir);
|
|
563
|
-
const transcripts = await readTranscriptFiles(files);
|
|
967
|
+
const transcripts = coalesceTranscriptLineages(await readTranscriptFiles(files));
|
|
564
968
|
const sessionsByDirectory = new Map<string, ClaudeCodeSessionSummary[]>();
|
|
565
969
|
|
|
566
970
|
for (const transcript of transcripts) {
|
|
@@ -569,6 +973,10 @@ export const listClaudeCodeWorkspaceGroups = async (
|
|
|
569
973
|
continue;
|
|
570
974
|
}
|
|
571
975
|
|
|
976
|
+
if (!hasSessionContent(transcript)) {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
|
|
572
980
|
const sessions = sessionsByDirectory.get(directoryName) ?? [];
|
|
573
981
|
sessions.push(transcript.session);
|
|
574
982
|
sessionsByDirectory.set(directoryName, sessions);
|
|
@@ -612,35 +1020,56 @@ export const findClaudeCodeWorkspaceGroups = (
|
|
|
612
1020
|
return groups.filter((group) => claudeCodeWorkspaceMatchesQuery(group, query));
|
|
613
1021
|
};
|
|
614
1022
|
|
|
1023
|
+
const compareSessions = (left: ClaudeCodeSessionSummary, right: ClaudeCodeSessionSummary) =>
|
|
1024
|
+
compareNullableMsDesc(left.lastActiveAtMs, right.lastActiveAtMs) || left.title.localeCompare(right.title);
|
|
1025
|
+
|
|
615
1026
|
const sortSessions = (sessions: ClaudeCodeSessionSummary[]): ClaudeCodeSessionSummary[] => {
|
|
616
|
-
return [...sessions].sort(
|
|
617
|
-
(left, right) =>
|
|
618
|
-
compareNullableMsDesc(left.lastActiveAtMs, right.lastActiveAtMs) || left.title.localeCompare(right.title),
|
|
619
|
-
);
|
|
1027
|
+
return [...sessions].sort(compareSessions);
|
|
620
1028
|
};
|
|
621
1029
|
|
|
622
|
-
export const
|
|
1030
|
+
export const listClaudeCodeSessionTranscriptsForGroup = async (
|
|
623
1031
|
workspaceKey: string,
|
|
624
1032
|
projectsDir = resolveClaudeCodeProjectsDir(),
|
|
625
|
-
): Promise<
|
|
1033
|
+
): Promise<ClaudeCodeSessionTranscript[]> => {
|
|
626
1034
|
const directoryName = getDirectoryNameFromWorkspaceKey(workspaceKey);
|
|
627
1035
|
if (!directoryName || !(await pathExists(projectsDir))) {
|
|
628
1036
|
return [];
|
|
629
1037
|
}
|
|
630
1038
|
|
|
631
1039
|
const files = await listTranscriptFilesForProject(projectsDir, directoryName);
|
|
632
|
-
const transcripts = await readTranscriptFiles(files);
|
|
633
|
-
return
|
|
1040
|
+
const transcripts = coalesceTranscriptLineages(await readTranscriptFiles(files));
|
|
1041
|
+
return transcripts.filter(hasSessionContent).sort((left, right) => compareSessions(left.session, right.session));
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
export const listClaudeCodeSessionsForGroup = async (
|
|
1045
|
+
workspaceKey: string,
|
|
1046
|
+
projectsDir = resolveClaudeCodeProjectsDir(),
|
|
1047
|
+
): Promise<ClaudeCodeSessionSummary[]> => {
|
|
1048
|
+
return sortSessions(
|
|
1049
|
+
(await listClaudeCodeSessionTranscriptsForGroup(workspaceKey, projectsDir)).map(
|
|
1050
|
+
(transcript) => transcript.session,
|
|
1051
|
+
),
|
|
1052
|
+
);
|
|
634
1053
|
};
|
|
635
1054
|
|
|
636
1055
|
const locateSessionFile = async (projectsDir: string, sessionId: string): Promise<TranscriptFile | null> => {
|
|
637
1056
|
const files = await listTranscriptFiles(projectsDir);
|
|
638
|
-
|
|
1057
|
+
const filenameMatch = files.find((file) => path.basename(file.filePath, '.jsonl') === sessionId);
|
|
1058
|
+
if (filenameMatch) {
|
|
1059
|
+
return filenameMatch;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const bodyMatches = await mapWithConcurrency(files, READ_CONCURRENCY, async (file) => {
|
|
1063
|
+
const parsed = await readTranscriptFile(file, { includeRawPayloads: false });
|
|
1064
|
+
return parsed?.transcript.session.sessionId === sessionId ? file : null;
|
|
1065
|
+
});
|
|
1066
|
+
return bodyMatches.find((file): file is TranscriptFile => file !== null) ?? null;
|
|
639
1067
|
};
|
|
640
1068
|
|
|
641
1069
|
export const readClaudeCodeSessionTranscript = async (
|
|
642
1070
|
projectsDir: string,
|
|
643
1071
|
sessionId: string,
|
|
1072
|
+
options: ReadClaudeCodeSessionTranscriptOptions = {},
|
|
644
1073
|
): Promise<ClaudeCodeSessionTranscript | null> => {
|
|
645
1074
|
if (!(await pathExists(projectsDir))) {
|
|
646
1075
|
return null;
|
|
@@ -651,5 +1080,68 @@ export const readClaudeCodeSessionTranscript = async (
|
|
|
651
1080
|
return null;
|
|
652
1081
|
}
|
|
653
1082
|
|
|
654
|
-
|
|
1083
|
+
const files = await listTranscriptFilesForProject(projectsDir, file.directoryName);
|
|
1084
|
+
const transcripts = await readTranscriptFiles(files);
|
|
1085
|
+
const lineage = getTranscriptLineages(transcripts).find((candidate) =>
|
|
1086
|
+
candidate.some((transcript) => transcript.session.sessionId === sessionId),
|
|
1087
|
+
);
|
|
1088
|
+
if (!lineage) {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const transcript = coalesceTranscriptLineage(lineage);
|
|
1093
|
+
if (!transcript) {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
const lineageFileSizeBytes =
|
|
1097
|
+
options.maxRawPayloadFileSizeBytes === undefined
|
|
1098
|
+
? 0
|
|
1099
|
+
: (
|
|
1100
|
+
await Promise.all(
|
|
1101
|
+
lineage.map((candidate) =>
|
|
1102
|
+
stat(candidate.session.filePath)
|
|
1103
|
+
.then((metadata) => metadata.size)
|
|
1104
|
+
.catch(() => options.maxRawPayloadFileSizeBytes! + 1),
|
|
1105
|
+
),
|
|
1106
|
+
)
|
|
1107
|
+
).reduce((total, size) => total + size, 0);
|
|
1108
|
+
const omitRawPayloads =
|
|
1109
|
+
options.includeRawPayloads === false ||
|
|
1110
|
+
(options.maxRawPayloadFileSizeBytes !== undefined && lineageFileSizeBytes > options.maxRawPayloadFileSizeBytes);
|
|
1111
|
+
return omitRawPayloads ? omitTranscriptRawPayloads(transcript) : transcript;
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
export const deleteClaudeCodeSession = async (
|
|
1115
|
+
projectsDir: string,
|
|
1116
|
+
sessionId: string,
|
|
1117
|
+
): Promise<DeleteClaudeCodeSessionResult> => {
|
|
1118
|
+
if (!(await pathExists(projectsDir))) {
|
|
1119
|
+
return { deletedFiles: [], deletedSessionIds: [] };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const file = await locateSessionFile(projectsDir, sessionId);
|
|
1123
|
+
if (!file) {
|
|
1124
|
+
return { deletedFiles: [], deletedSessionIds: [] };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const files = await listTranscriptFilesForProject(projectsDir, file.directoryName);
|
|
1128
|
+
const transcripts = await readTranscriptFiles(files);
|
|
1129
|
+
const lineage = getTranscriptLineages(transcripts).find((candidate) =>
|
|
1130
|
+
candidate.some((transcript) => transcript.session.sessionId === sessionId),
|
|
1131
|
+
);
|
|
1132
|
+
const targets = (lineage ?? [])
|
|
1133
|
+
.map((transcript) => ({
|
|
1134
|
+
filePath: transcript.session.filePath,
|
|
1135
|
+
sessionId: transcript.session.sessionId,
|
|
1136
|
+
}))
|
|
1137
|
+
.sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
1138
|
+
if (targets.length === 0) {
|
|
1139
|
+
return { deletedFiles: [], deletedSessionIds: [] };
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const removed = await Promise.all(targets.map((target) => unlinkIfPresent(target.filePath)));
|
|
1143
|
+
return {
|
|
1144
|
+
deletedFiles: targets.flatMap((target, index) => (removed[index] ? [target.filePath] : [])),
|
|
1145
|
+
deletedSessionIds: targets.flatMap((target, index) => (removed[index] ? [target.sessionId] : [])),
|
|
1146
|
+
};
|
|
655
1147
|
};
|