spiracha 2.0.0 → 2.1.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/README.md +6 -1
- package/apps/ui/README.md +20 -2
- package/apps/ui/package.json +2 -2
- package/apps/ui/src/components/antigravity-conversations-table.tsx +50 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +1 -0
- 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 +7 -5
- package/apps/ui/src/components/export-dialog.tsx +2 -12
- 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/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/threads-table.tsx +9 -46
- 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 +152 -7
- package/apps/ui/src/lib/antigravity-transcript-events.ts +11 -3
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +175 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +19 -3
- package/apps/ui/src/lib/codex-server.ts +135 -53
- package/apps/ui/src/lib/cursor-server.ts +48 -5
- 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 +169 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +85 -7
- package/apps/ui/src/lib/opencode-server.ts +85 -7
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
- package/apps/ui/src/routeTree.gen.ts +107 -0
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +76 -10
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +269 -31
- 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 +143 -17
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +165 -26
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +2 -2
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +11 -1
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +66 -15
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +199 -32
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +105 -19
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +236 -44
- package/apps/ui/src/routes/opencode.index.tsx +16 -3
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +7 -4
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +75 -28
- package/apps/ui/src/routes/threads.$threadId.tsx +548 -64
- package/package.json +19 -18
- package/src/client.ts +134 -1
- package/src/lib/antigravity-db.ts +208 -32
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/claude-code-db.ts +498 -32
- 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-browser-export.ts +23 -27
- package/src/lib/codex-thread-cache.ts +41 -13
- package/src/lib/codex-thread-parser.ts +86 -35
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +39 -19
- package/src/lib/concurrency.ts +41 -0
- package/src/lib/conversation-api.ts +399 -19
- package/src/lib/conversation-data/antigravity-adapter.ts +21 -2
- package/src/lib/conversation-data/claude-code-adapter.ts +37 -6
- package/src/lib/conversation-data/codex-adapter.ts +28 -4
- package/src/lib/conversation-data/cursor-adapter.ts +35 -1
- package/src/lib/conversation-data/grok-adapter.ts +210 -0
- package/src/lib/conversation-data/index.ts +76 -1
- package/src/lib/conversation-data/kiro-adapter.ts +41 -7
- package/src/lib/conversation-data/opencode-adapter.ts +24 -2
- package/src/lib/conversation-data/qoder-adapter.ts +44 -19
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +57 -0
- package/src/lib/cursor-db.ts +34 -5
- package/src/lib/grok-db.ts +1026 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +52 -0
- package/src/lib/grok-transcript.ts +154 -0
- package/src/lib/kiro-db.ts +52 -1
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +598 -55
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/shared.ts +12 -0
- package/src/lib/transcript-load-limiter.ts +82 -0
- package/src/lib/ui-cache.ts +43 -17
- package/src/lib/ui-export-archive.ts +60 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import type { ClaudeCodeAssistantMessagePhase } from './claude-code-transcript-phase';
|
|
3
4
|
import type { ExportFormat, JsonValue } from './shared';
|
|
4
5
|
|
|
5
6
|
export type { ClaudeCodeAssistantMessagePhase } from './claude-code-transcript-phase';
|
|
@@ -84,6 +85,7 @@ export type ClaudeCodeTranscriptPart = {
|
|
|
84
85
|
};
|
|
85
86
|
|
|
86
87
|
export type ClaudeCodeTranscriptEntry = {
|
|
88
|
+
assistantPhase?: ClaudeCodeAssistantMessagePhase | null;
|
|
87
89
|
cwd: string | null;
|
|
88
90
|
entryId: string;
|
|
89
91
|
model?: string | null;
|
|
@@ -97,6 +99,9 @@ export type ClaudeCodeTranscriptEntry = {
|
|
|
97
99
|
|
|
98
100
|
export type ClaudeCodeSessionTranscript = {
|
|
99
101
|
entries: ClaudeCodeTranscriptEntry[];
|
|
102
|
+
isPartial?: boolean;
|
|
103
|
+
omittedEntryCount?: number;
|
|
104
|
+
rawPayloadsOmitted?: boolean;
|
|
100
105
|
rawEvents: Record<string, JsonValue>[];
|
|
101
106
|
renderablePartCount: number;
|
|
102
107
|
session: ClaudeCodeSessionSummary;
|
|
@@ -3,16 +3,39 @@ import type { JsonValue } from './shared';
|
|
|
3
3
|
export type ClaudeCodeAssistantMessagePhase = 'commentary' | 'final_answer';
|
|
4
4
|
|
|
5
5
|
type ClaudeCodeAssistantPhaseEntry = {
|
|
6
|
+
assistantPhase?: ClaudeCodeAssistantMessagePhase | null;
|
|
7
|
+
parts?: Array<{
|
|
8
|
+
text?: string;
|
|
9
|
+
type: string;
|
|
10
|
+
}>;
|
|
6
11
|
raw: Record<string, JsonValue>;
|
|
7
12
|
role: string;
|
|
8
13
|
};
|
|
9
14
|
|
|
15
|
+
type ClaudeCodeFilterEntry = ClaudeCodeAssistantPhaseEntry & {
|
|
16
|
+
parts: Array<{
|
|
17
|
+
text?: string;
|
|
18
|
+
type: string;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SYNTHETIC_USER_TEXT_PREFIXES = [
|
|
23
|
+
'<local-command-caveat>',
|
|
24
|
+
'<local-command-stdout>',
|
|
25
|
+
'<task-notification>',
|
|
26
|
+
'[Request interrupted by user]',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
10
29
|
const asMessageObject = (value: JsonValue | undefined): Record<string, JsonValue> | null => {
|
|
11
30
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
12
31
|
? (value as Record<string, JsonValue>)
|
|
13
32
|
: null;
|
|
14
33
|
};
|
|
15
34
|
|
|
35
|
+
export const isClaudeCodeRawFlagEnabled = (value: JsonValue | undefined): boolean => {
|
|
36
|
+
return value === true || value === 'true';
|
|
37
|
+
};
|
|
38
|
+
|
|
16
39
|
export const getClaudeCodeAssistantMessagePhase = (
|
|
17
40
|
entry: ClaudeCodeAssistantPhaseEntry,
|
|
18
41
|
): ClaudeCodeAssistantMessagePhase | null => {
|
|
@@ -20,6 +43,42 @@ export const getClaudeCodeAssistantMessagePhase = (
|
|
|
20
43
|
return null;
|
|
21
44
|
}
|
|
22
45
|
|
|
46
|
+
if (entry.assistantPhase) {
|
|
47
|
+
return entry.assistantPhase;
|
|
48
|
+
}
|
|
49
|
+
|
|
23
50
|
const stopReason = asMessageObject(entry.raw.message)?.stop_reason;
|
|
24
|
-
|
|
51
|
+
if (stopReason !== 'tool_use') {
|
|
52
|
+
return 'final_answer';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const toolUseIndex = entry.parts?.findIndex((part) => part.type === 'tool_use') ?? -1;
|
|
56
|
+
const hasTextAfterToolUse =
|
|
57
|
+
toolUseIndex >= 0 &&
|
|
58
|
+
entry.parts?.slice(toolUseIndex + 1).some((part) => part.type === 'text' && part.text?.trim()) === true;
|
|
59
|
+
return hasTextAfterToolUse ? 'final_answer' : 'commentary';
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const isClaudeCodeSyntheticTranscriptEntry = (entry: ClaudeCodeFilterEntry): boolean => {
|
|
63
|
+
if (
|
|
64
|
+
isClaudeCodeRawFlagEnabled(entry.raw.isApiErrorMessage) ||
|
|
65
|
+
isClaudeCodeRawFlagEnabled(entry.raw.isMeta) ||
|
|
66
|
+
isClaudeCodeRawFlagEnabled(entry.raw.isCompactSummary)
|
|
67
|
+
) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (asMessageObject(entry.raw.message)?.model === '<synthetic>') {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (entry.role !== 'user') {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const text = entry.parts.find((part) => part.type === 'text')?.text?.trim() ?? '';
|
|
80
|
+
return (
|
|
81
|
+
text.startsWith('<command-name>/compact</command-name>') ||
|
|
82
|
+
SYNTHETIC_USER_TEXT_PREFIXES.some((prefix) => text.startsWith(prefix))
|
|
83
|
+
);
|
|
25
84
|
};
|
|
@@ -5,7 +5,10 @@ import type {
|
|
|
5
5
|
ClaudeCodeTranscriptEntry,
|
|
6
6
|
ClaudeCodeTranscriptPart,
|
|
7
7
|
} from './claude-code-exporter-types';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getClaudeCodeAssistantMessagePhase,
|
|
10
|
+
isClaudeCodeSyntheticTranscriptEntry,
|
|
11
|
+
} from './claude-code-transcript-phase';
|
|
9
12
|
import {
|
|
10
13
|
cleanExtractedText,
|
|
11
14
|
cleanInlineTitle,
|
|
@@ -102,7 +105,7 @@ const renderToolResultPart = (part: ClaudeCodeTranscriptPart, options: ClaudeCod
|
|
|
102
105
|
return '';
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
const outputText = part.outputText
|
|
108
|
+
const outputText = (part.outputText ?? '').trim();
|
|
106
109
|
if (!outputText) {
|
|
107
110
|
return '';
|
|
108
111
|
}
|
|
@@ -160,9 +163,9 @@ export const renderClaudeCodeTranscript = (
|
|
|
160
163
|
transcript: ClaudeCodeSessionTranscript,
|
|
161
164
|
options: ClaudeCodeExportOptions,
|
|
162
165
|
): string | null => {
|
|
163
|
-
const sections = transcript.entries
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
const sections = transcript.entries
|
|
167
|
+
.filter((entry) => !isClaudeCodeSyntheticTranscriptEntry(entry))
|
|
168
|
+
.flatMap((entry) => entry.parts.map((part) => renderPart(entry, part, options)).filter(Boolean));
|
|
166
169
|
if (sections.length === 0) {
|
|
167
170
|
return null;
|
|
168
171
|
}
|
|
@@ -5,8 +5,14 @@ import { getThreadBrowseData } from './codex-browser-db';
|
|
|
5
5
|
import type { CodexTranscriptRenderOptions } from './codex-thread-types';
|
|
6
6
|
import { renderCodexSessionFile, writeCodexSessionFileExport } from './codex-transcript-renderer';
|
|
7
7
|
import { applyPathTransforms, type PathDisplaySettings } from './path-transforms';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import type { ExportFormat } from './shared';
|
|
9
|
+
import {
|
|
10
|
+
buildBatchExportBaseName,
|
|
11
|
+
buildConversationExportBaseName,
|
|
12
|
+
getExportMimeType,
|
|
13
|
+
zipExportDirectory,
|
|
14
|
+
zipExportFile,
|
|
15
|
+
} from './ui-export-archive';
|
|
10
16
|
import { buildUiExportDownloadUrl, ensureUiExportDir } from './ui-export-files';
|
|
11
17
|
|
|
12
18
|
type RenderCodexThreadDownloadInput = {
|
|
@@ -42,31 +48,15 @@ export type CodexThreadDownload =
|
|
|
42
48
|
|
|
43
49
|
const LARGE_BROWSER_EXPORT_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
44
50
|
|
|
45
|
-
const formatReadableExportDate = (value: number) => {
|
|
46
|
-
const date = new Date(value);
|
|
47
|
-
const year = date.getUTCFullYear();
|
|
48
|
-
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
49
|
-
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
50
|
-
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
51
|
-
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
52
|
-
return `${year}-${month}-${day}-${hours}${minutes}`;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
51
|
const buildExportBaseName = (thread: ReturnType<typeof getThreadBrowseData>['thread']) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
throw new Error('No threads selected for export');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const projectName = sanitizeExportFileName(getPortablePathBasename(firstThread.cwd) || 'threads') || 'threads';
|
|
68
|
-
const latestTimestamp = Math.max(...threads.map((thread) => thread.updated_at_ms ?? thread.updated_at * 1000));
|
|
69
|
-
return `${projectName}-${formatReadableExportDate(latestTimestamp)}-threads-${threads.length}`;
|
|
52
|
+
return buildConversationExportBaseName(
|
|
53
|
+
{
|
|
54
|
+
cwd: thread.cwd,
|
|
55
|
+
id: thread.id,
|
|
56
|
+
updatedAtMs: thread.updated_at_ms ?? thread.updated_at * 1000,
|
|
57
|
+
},
|
|
58
|
+
'thread',
|
|
59
|
+
);
|
|
70
60
|
};
|
|
71
61
|
|
|
72
62
|
const buildUniqueArchivePath = (exportDir: string, exportBaseName: string) => {
|
|
@@ -302,7 +292,13 @@ export const renderCodexThreadsDownload = async (
|
|
|
302
292
|
const browseEntries = threadIds.map((threadId) => getThreadBrowseData(input.dbPath, threadId));
|
|
303
293
|
const threads = browseEntries.map((entry) => entry.thread);
|
|
304
294
|
const exportDir = await resolvePublicExportDir(input.publicExportDir);
|
|
305
|
-
const exportBaseName = buildBatchExportBaseName(
|
|
295
|
+
const exportBaseName = buildBatchExportBaseName(
|
|
296
|
+
threads.map((thread) => ({
|
|
297
|
+
cwd: thread.cwd,
|
|
298
|
+
updatedAtMs: thread.updated_at_ms ?? thread.updated_at * 1000,
|
|
299
|
+
})),
|
|
300
|
+
'threads',
|
|
301
|
+
);
|
|
306
302
|
const bundleDirectory = await createExportWorkspace(exportDir, exportBaseName);
|
|
307
303
|
const zipPath = buildUniqueArchivePath(exportDir, exportBaseName);
|
|
308
304
|
const usedBatchEntryBaseNames = new Set<string>();
|
|
@@ -2,10 +2,15 @@ import { stat } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import type { ParsedCodexTranscript } from './codex-browser-types';
|
|
4
4
|
import { parseCodexTranscriptFile } from './codex-thread-parser';
|
|
5
|
+
import type { CodexTranscriptEventFilters } from './codex-transcript-filter';
|
|
6
|
+
import { shouldShowCodexTranscriptEvent } from './codex-transcript-filter';
|
|
7
|
+
import { runWithTranscriptLoadLimit } from './transcript-load-limiter';
|
|
5
8
|
import { getFileFingerprint, hashCacheKeyParts, withCachedJson } from './ui-cache';
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
// Keep initial thread payloads below sizes that make TanStack Start SSR responses unreliable.
|
|
11
|
+
export const LARGE_THREAD_SIZE_BYTES = 8 * 1024 * 1024;
|
|
8
12
|
export const LARGE_THREAD_PREVIEW_EVENT_LIMIT = 200;
|
|
13
|
+
const CODEX_TRANSCRIPT_CACHE_VERSION = 'v2';
|
|
9
14
|
|
|
10
15
|
const isMissingFileError = (error: unknown) => {
|
|
11
16
|
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
@@ -13,12 +18,18 @@ const isMissingFileError = (error: unknown) => {
|
|
|
13
18
|
|
|
14
19
|
export const getCachedParsedCodexTranscript = async (sessionFile: string): Promise<ParsedCodexTranscript> => {
|
|
15
20
|
const fingerprint = await getFileFingerprint(sessionFile);
|
|
16
|
-
const key = `thread-${hashCacheKeyParts(path.basename(sessionFile), fingerprint)}`;
|
|
21
|
+
const key = `thread-${hashCacheKeyParts(CODEX_TRANSCRIPT_CACHE_VERSION, path.basename(sessionFile), fingerprint)}`;
|
|
17
22
|
|
|
18
|
-
return withCachedJson(key, async () =>
|
|
23
|
+
return withCachedJson(key, async () =>
|
|
24
|
+
runWithTranscriptLoadLimit(() => parseCodexTranscriptFile(sessionFile), {
|
|
25
|
+
path: sessionFile,
|
|
26
|
+
source: 'codex-full',
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
19
29
|
};
|
|
20
30
|
|
|
21
31
|
type CachedThreadTranscriptPreviewOptions = {
|
|
32
|
+
filters?: CodexTranscriptEventFilters;
|
|
22
33
|
largeTranscriptThresholdBytes?: number;
|
|
23
34
|
previewEventLimit?: number;
|
|
24
35
|
};
|
|
@@ -53,22 +64,39 @@ export const getCachedThreadTranscriptPreview = async (
|
|
|
53
64
|
): Promise<ParsedCodexTranscript> => {
|
|
54
65
|
const threshold = options.largeTranscriptThresholdBytes ?? LARGE_THREAD_SIZE_BYTES;
|
|
55
66
|
const previewEventLimit = options.previewEventLimit ?? LARGE_THREAD_PREVIEW_EVENT_LIMIT;
|
|
67
|
+
const filters = options.filters;
|
|
56
68
|
const fingerprint = await getFileFingerprint(sessionFile);
|
|
57
69
|
const { fileSizeBytes, shouldDeferTranscriptLoad } = await getThreadRolloutLoadState(sessionFile, threshold);
|
|
58
|
-
const
|
|
70
|
+
const filterKey = filters ? JSON.stringify(filters) : 'all';
|
|
71
|
+
const key = `thread-preview-${hashCacheKeyParts(CODEX_TRANSCRIPT_CACHE_VERSION, path.basename(sessionFile), fingerprint, String(threshold), String(previewEventLimit), filterKey)}`;
|
|
59
72
|
|
|
60
73
|
return withCachedJson(key, async () => {
|
|
61
74
|
if (!shouldDeferTranscriptLoad) {
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
return runWithTranscriptLoadLimit(
|
|
76
|
+
() =>
|
|
77
|
+
parseCodexTranscriptFile(sessionFile, {
|
|
78
|
+
sourceFileSizeBytes: fileSizeBytes,
|
|
79
|
+
}),
|
|
80
|
+
{
|
|
81
|
+
path: sessionFile,
|
|
82
|
+
source: 'codex-preview-full',
|
|
83
|
+
},
|
|
84
|
+
);
|
|
65
85
|
}
|
|
66
86
|
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
return runWithTranscriptLoadLimit(
|
|
88
|
+
() =>
|
|
89
|
+
parseCodexTranscriptFile(sessionFile, {
|
|
90
|
+
eventFilter: filters ? (event) => shouldShowCodexTranscriptEvent(event, filters) : undefined,
|
|
91
|
+
includeRaw: false,
|
|
92
|
+
maxTurnContexts: 0,
|
|
93
|
+
sourceFileSizeBytes: fileSizeBytes,
|
|
94
|
+
tailEventLimit: previewEventLimit,
|
|
95
|
+
}),
|
|
96
|
+
{
|
|
97
|
+
path: sessionFile,
|
|
98
|
+
source: 'codex-preview',
|
|
99
|
+
},
|
|
100
|
+
);
|
|
73
101
|
});
|
|
74
102
|
};
|
|
@@ -14,13 +14,27 @@ import type {
|
|
|
14
14
|
TurnContextRecord,
|
|
15
15
|
WebSearchEvent,
|
|
16
16
|
} from './codex-browser-types';
|
|
17
|
-
import { asNumber, asObject, asString, type JsonValue, readJsonlObjects } from './shared';
|
|
17
|
+
import { asNumber, asObject, asString, type JsonValue, readJsonlObjects, stripCodexAppDirectiveLines } from './shared';
|
|
18
18
|
|
|
19
19
|
type ParseCodexTranscriptOptions = {
|
|
20
|
+
eventFilter?: (event: ThreadEvent) => boolean;
|
|
20
21
|
includeRaw?: boolean;
|
|
21
22
|
maxEvents?: number;
|
|
22
23
|
maxTurnContexts?: number;
|
|
23
24
|
sourceFileSizeBytes?: number | null;
|
|
25
|
+
tailEventLimit?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ParseCodexTranscriptState = {
|
|
29
|
+
events: ThreadEvent[];
|
|
30
|
+
eventFilter: (event: ThreadEvent) => boolean;
|
|
31
|
+
includeRaw: boolean;
|
|
32
|
+
maxEvents: number;
|
|
33
|
+
maxTurnContexts: number;
|
|
34
|
+
sequence: number;
|
|
35
|
+
shouldStop: boolean;
|
|
36
|
+
tailEventLimit: number;
|
|
37
|
+
turnContexts: TurnContextRecord[];
|
|
24
38
|
};
|
|
25
39
|
|
|
26
40
|
const createEmptyStats = (): ThreadTranscriptStats => {
|
|
@@ -64,45 +78,74 @@ export const parseCodexTranscriptFile = async (
|
|
|
64
78
|
const includeRaw = options.includeRaw ?? true;
|
|
65
79
|
const maxEvents = options.maxEvents ?? Number.POSITIVE_INFINITY;
|
|
66
80
|
const maxTurnContexts = options.maxTurnContexts ?? Number.POSITIVE_INFINITY;
|
|
67
|
-
|
|
81
|
+
const tailEventLimit = options.tailEventLimit ?? Number.POSITIVE_INFINITY;
|
|
82
|
+
const eventFilter = options.eventFilter ?? (() => true);
|
|
83
|
+
const state: ParseCodexTranscriptState = {
|
|
84
|
+
eventFilter,
|
|
85
|
+
events,
|
|
86
|
+
includeRaw,
|
|
87
|
+
maxEvents,
|
|
88
|
+
maxTurnContexts,
|
|
89
|
+
sequence: 0,
|
|
90
|
+
shouldStop: false,
|
|
91
|
+
tailEventLimit,
|
|
92
|
+
turnContexts,
|
|
93
|
+
};
|
|
68
94
|
|
|
69
95
|
for await (const parsed of readJsonlObjects(sessionFile)) {
|
|
70
96
|
captureSessionMeta(parsed, sessionMeta);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (turnContexts.length < maxTurnContexts) {
|
|
75
|
-
captureTurnContext(parsed, turnContexts);
|
|
76
|
-
}
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const event = toThreadEvent(parsed, sequence, includeRaw);
|
|
81
|
-
if (!event) {
|
|
82
|
-
continue;
|
|
97
|
+
captureTranscriptRecord(parsed, state);
|
|
98
|
+
if (state.shouldStop) {
|
|
99
|
+
break;
|
|
83
100
|
}
|
|
101
|
+
}
|
|
84
102
|
|
|
85
|
-
|
|
103
|
+
for (const event of events) {
|
|
86
104
|
updateTranscriptStats(stats, event);
|
|
87
|
-
sequence += 1;
|
|
88
|
-
|
|
89
|
-
if (events.length >= maxEvents) {
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
return {
|
|
95
108
|
events,
|
|
96
|
-
isPartial:
|
|
109
|
+
isPartial:
|
|
110
|
+
Number.isFinite(maxEvents) ||
|
|
111
|
+
Number.isFinite(maxTurnContexts) ||
|
|
112
|
+
Number.isFinite(tailEventLimit) ||
|
|
113
|
+
options.eventFilter !== undefined,
|
|
97
114
|
rawIncluded: includeRaw,
|
|
98
115
|
sessionMeta,
|
|
99
116
|
sourceFileSizeBytes: options.sourceFileSizeBytes ?? null,
|
|
100
117
|
stats,
|
|
101
|
-
statsArePartial:
|
|
118
|
+
statsArePartial:
|
|
119
|
+
Number.isFinite(maxEvents) || Number.isFinite(tailEventLimit) || options.eventFilter !== undefined,
|
|
102
120
|
turnContexts,
|
|
103
121
|
};
|
|
104
122
|
};
|
|
105
123
|
|
|
124
|
+
const captureTranscriptRecord = (parsed: Record<string, JsonValue>, state: ParseCodexTranscriptState) => {
|
|
125
|
+
if (asString(parsed.type) === 'turn_context') {
|
|
126
|
+
if (state.turnContexts.length < state.maxTurnContexts) {
|
|
127
|
+
captureTurnContext(parsed, state.turnContexts);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const event = toThreadEvent(parsed, state.sequence, state.includeRaw);
|
|
133
|
+
if (!event) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
state.sequence += 1;
|
|
138
|
+
if (!state.eventFilter(event)) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
state.events.push(event);
|
|
143
|
+
if (state.events.length > state.tailEventLimit) {
|
|
144
|
+
state.events.shift();
|
|
145
|
+
}
|
|
146
|
+
state.shouldStop = state.events.length >= state.maxEvents;
|
|
147
|
+
};
|
|
148
|
+
|
|
106
149
|
const captureSessionMeta = (parsed: Record<string, JsonValue>, sessionMeta: SessionMetaExtended) => {
|
|
107
150
|
if (parsed.type !== 'session_meta') {
|
|
108
151
|
return;
|
|
@@ -261,12 +304,13 @@ const createMessageEvent = (
|
|
|
261
304
|
): MessageEvent | null => {
|
|
262
305
|
const role = asString(payload.role);
|
|
263
306
|
const content = payload.content;
|
|
307
|
+
const text = extractText(content);
|
|
264
308
|
if (!role || content === undefined) {
|
|
265
309
|
return null;
|
|
266
310
|
}
|
|
267
311
|
|
|
268
312
|
return {
|
|
269
|
-
isHiddenByDefault: shouldHideTranscriptText(role,
|
|
313
|
+
isHiddenByDefault: shouldHideTranscriptText(role, text),
|
|
270
314
|
kind: 'message',
|
|
271
315
|
memoryCitation: null,
|
|
272
316
|
model: asString(payload.model),
|
|
@@ -274,7 +318,7 @@ const createMessageEvent = (
|
|
|
274
318
|
raw,
|
|
275
319
|
role,
|
|
276
320
|
sequence,
|
|
277
|
-
text
|
|
321
|
+
text,
|
|
278
322
|
timestamp,
|
|
279
323
|
variant: 'message',
|
|
280
324
|
};
|
|
@@ -286,8 +330,9 @@ const createUserMessageEvent = (
|
|
|
286
330
|
sequence: number,
|
|
287
331
|
timestamp: string | null,
|
|
288
332
|
): MessageEvent => {
|
|
333
|
+
const text = stripMemoryCitationBlocks(asString(payload.message) ?? '');
|
|
289
334
|
return {
|
|
290
|
-
isHiddenByDefault: shouldHideTranscriptText('user',
|
|
335
|
+
isHiddenByDefault: shouldHideTranscriptText('user', text),
|
|
291
336
|
kind: 'message',
|
|
292
337
|
memoryCitation: null,
|
|
293
338
|
model: null,
|
|
@@ -295,7 +340,7 @@ const createUserMessageEvent = (
|
|
|
295
340
|
raw,
|
|
296
341
|
role: 'user',
|
|
297
342
|
sequence,
|
|
298
|
-
text
|
|
343
|
+
text,
|
|
299
344
|
timestamp,
|
|
300
345
|
variant: 'user_message',
|
|
301
346
|
};
|
|
@@ -307,8 +352,9 @@ const createAgentMessageEvent = (
|
|
|
307
352
|
sequence: number,
|
|
308
353
|
timestamp: string | null,
|
|
309
354
|
): MessageEvent => {
|
|
355
|
+
const text = stripMemoryCitationBlocks(asString(payload.message) ?? '');
|
|
310
356
|
return {
|
|
311
|
-
isHiddenByDefault:
|
|
357
|
+
isHiddenByDefault: shouldHideTranscriptText('assistant', text),
|
|
312
358
|
kind: 'message',
|
|
313
359
|
memoryCitation: payload.memory_citation ?? null,
|
|
314
360
|
model: asString(payload.model),
|
|
@@ -316,7 +362,7 @@ const createAgentMessageEvent = (
|
|
|
316
362
|
raw,
|
|
317
363
|
role: 'assistant',
|
|
318
364
|
sequence,
|
|
319
|
-
text
|
|
365
|
+
text,
|
|
320
366
|
timestamp,
|
|
321
367
|
variant: 'agent_message',
|
|
322
368
|
};
|
|
@@ -548,24 +594,29 @@ const parseExecCommandArguments = (argumentsText: string | null) => {
|
|
|
548
594
|
|
|
549
595
|
const extractText = (content: JsonValue): string => {
|
|
550
596
|
if (typeof content === 'string') {
|
|
551
|
-
return content
|
|
597
|
+
return stripMemoryCitationBlocks(content);
|
|
552
598
|
}
|
|
553
599
|
|
|
554
600
|
if (Array.isArray(content)) {
|
|
555
|
-
return
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
601
|
+
return stripMemoryCitationBlocks(
|
|
602
|
+
content
|
|
603
|
+
.map((entry) => extractTextPart(entry))
|
|
604
|
+
.filter(Boolean)
|
|
605
|
+
.join('\n\n'),
|
|
606
|
+
);
|
|
560
607
|
}
|
|
561
608
|
|
|
562
609
|
if (content && typeof content === 'object') {
|
|
563
|
-
return asString((content as Record<string, JsonValue>).text)
|
|
610
|
+
return stripMemoryCitationBlocks(asString((content as Record<string, JsonValue>).text) ?? '');
|
|
564
611
|
}
|
|
565
612
|
|
|
566
613
|
return '';
|
|
567
614
|
};
|
|
568
615
|
|
|
616
|
+
const stripMemoryCitationBlocks = (text: string): string => {
|
|
617
|
+
return stripCodexAppDirectiveLines(text.replace(/<oai-mem-citation>[\s\S]*?<\/oai-mem-citation>/gu, ''));
|
|
618
|
+
};
|
|
619
|
+
|
|
569
620
|
const extractTextPart = (entry: JsonValue): string => {
|
|
570
621
|
const objectValue = asObject(entry);
|
|
571
622
|
if (!objectValue) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ThreadEvent } from './codex-browser-types';
|
|
2
|
+
|
|
3
|
+
const isCommentaryMessage = (event: ThreadEvent) =>
|
|
4
|
+
event.kind === 'message' && event.role === 'assistant' && event.phase === 'commentary';
|
|
5
|
+
|
|
6
|
+
export type CodexTranscriptEventFilters = {
|
|
7
|
+
showCommentary: boolean;
|
|
8
|
+
showExtraEvents: boolean;
|
|
9
|
+
showToolCalls: boolean;
|
|
10
|
+
showUserMessages: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const shouldShowCodexTranscriptEvent = (event: ThreadEvent, filters: CodexTranscriptEventFilters) => {
|
|
14
|
+
if (isCommentaryMessage(event) && !filters.showCommentary) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (event.kind === 'message') {
|
|
19
|
+
if (event.role === 'user' && !filters.showUserMessages) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return !event.isHiddenByDefault || filters.showExtraEvents;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (event.kind === 'tool_call' || event.kind === 'tool_output') {
|
|
27
|
+
return filters.showToolCalls;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return filters.showExtraEvents;
|
|
31
|
+
};
|
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
renderDocumentTitle,
|
|
27
27
|
renderMetadataBlock,
|
|
28
28
|
renderSection,
|
|
29
|
+
stripCodexAppDirectiveLines,
|
|
29
30
|
} from './shared';
|
|
31
|
+
import { runWithTranscriptLoadLimit } from './transcript-load-limiter';
|
|
30
32
|
|
|
31
33
|
export const renderCodexSessionFile = async (
|
|
32
34
|
target: CodexTranscriptExportTarget,
|
|
@@ -35,7 +37,14 @@ export const renderCodexSessionFile = async (
|
|
|
35
37
|
let transcriptState: CodexTranscriptState;
|
|
36
38
|
|
|
37
39
|
try {
|
|
38
|
-
transcriptState = await
|
|
40
|
+
transcriptState = await runWithTranscriptLoadLimit(
|
|
41
|
+
() => collectCodexTranscript(target.sessionFile, options, target.thread?.model ?? null),
|
|
42
|
+
{
|
|
43
|
+
id: target.thread?.id,
|
|
44
|
+
path: target.sessionFile,
|
|
45
|
+
source: 'codex-export-inline',
|
|
46
|
+
},
|
|
47
|
+
);
|
|
39
48
|
} catch (error) {
|
|
40
49
|
const message = error instanceof Error ? error.message : String(error);
|
|
41
50
|
throw new Error(`Failed to read Codex transcript ${target.sessionFile}: ${message}`);
|
|
@@ -78,19 +87,28 @@ export const writeCodexSessionFileExport = async (
|
|
|
78
87
|
let wroteSection = false;
|
|
79
88
|
|
|
80
89
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
await runWithTranscriptLoadLimit(
|
|
91
|
+
async () => {
|
|
92
|
+
transcriptStream = await createExportWriteStream(transcriptOutputPath);
|
|
93
|
+
for await (const parsed of readJsonlObjects(target.sessionFile)) {
|
|
94
|
+
captureSessionMeta(parsed, state.sessionMeta);
|
|
95
|
+
const block = renderCodexTranscriptRecord(parsed, options, state);
|
|
96
|
+
if (!block) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
transcriptStream.write(transform(wroteSection ? `${getSectionSeparator()}${block}` : block));
|
|
101
|
+
wroteSection = true;
|
|
102
|
+
}
|
|
103
|
+
await finalizeExportWriteStream(transcriptStream);
|
|
104
|
+
transcriptStream = null;
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: target.thread?.id,
|
|
108
|
+
path: target.sessionFile,
|
|
109
|
+
source: 'codex-export-stream',
|
|
110
|
+
},
|
|
111
|
+
);
|
|
94
112
|
|
|
95
113
|
if (!wroteSection) {
|
|
96
114
|
return false;
|
|
@@ -189,7 +207,7 @@ const processCodexMessageRecord = (
|
|
|
189
207
|
options: CodexTranscriptRenderOptions,
|
|
190
208
|
state: CodexTranscriptState,
|
|
191
209
|
) => {
|
|
192
|
-
return renderMessageBlock(message, options.outputFormat, state
|
|
210
|
+
return renderMessageBlock(message, options.outputFormat, state, options.includeCommentary);
|
|
193
211
|
};
|
|
194
212
|
|
|
195
213
|
const buildStreamExportPrefix = (
|
|
@@ -542,7 +560,7 @@ const extractToolRecord = (parsed: Record<string, JsonValue>): ToolRecord | null
|
|
|
542
560
|
const renderMessageBlock = (
|
|
543
561
|
message: MessageRecord,
|
|
544
562
|
outputFormat: ExportFormat,
|
|
545
|
-
|
|
563
|
+
state: CodexTranscriptState,
|
|
546
564
|
includeCommentary: boolean,
|
|
547
565
|
): string => {
|
|
548
566
|
if (message.role !== 'user' && message.role !== 'assistant') {
|
|
@@ -553,12 +571,13 @@ const renderMessageBlock = (
|
|
|
553
571
|
return '';
|
|
554
572
|
}
|
|
555
573
|
|
|
556
|
-
const
|
|
574
|
+
const extractedText = extractText(message.content);
|
|
575
|
+
const text = stripCodexAppDirectiveLines(cleanExtractedText(stripPreviewBlock(extractedText)));
|
|
557
576
|
if (!text || shouldSkipMessage(message.role, text)) {
|
|
558
577
|
return '';
|
|
559
578
|
}
|
|
560
579
|
|
|
561
|
-
const title = message.role === 'user' ? 'User' : formatModelLabel(message.model ?? assistantModel);
|
|
580
|
+
const title = message.role === 'user' ? 'User' : formatModelLabel(message.model ?? state.assistantModel);
|
|
562
581
|
const body = message.phase ? `Phase: ${message.phase}\n\n${text}` : text;
|
|
563
582
|
|
|
564
583
|
return renderSection(title, body, outputFormat);
|
|
@@ -570,7 +589,8 @@ const renderToolBlock = (tool: ToolRecord, outputFormat: ExportFormat): string =
|
|
|
570
589
|
return details ? renderSection('Tool', details, outputFormat) : '';
|
|
571
590
|
}
|
|
572
591
|
|
|
573
|
-
const
|
|
592
|
+
const outputText = tool.outputText ?? '';
|
|
593
|
+
const summary = formatToolOutputSummary(outputText, outputFormat);
|
|
574
594
|
return summary ? renderSection('Tool Output', summary, outputFormat) : '';
|
|
575
595
|
};
|
|
576
596
|
|