spiracha 1.0.0 → 1.1.1
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 +31 -1
- package/README.md +61 -7
- package/apps/ui/AGENTS.md +70 -0
- package/apps/ui/README.md +72 -0
- package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +1 -0
- package/apps/ui/dist/client/assets/analytics-CqWZmyV6.js +1 -0
- package/apps/ui/dist/client/assets/checkbox-DXM4lkJq.js +1 -0
- package/apps/ui/dist/client/assets/data-table-DnPYMPCD.js +4 -0
- package/apps/ui/dist/client/assets/delete-confirm-dialog-CcZaRX33.js +11 -0
- package/apps/ui/dist/client/assets/download-DOwxk-cG.js +1 -0
- package/apps/ui/dist/client/assets/es2015-Bm0kEzx2.js +41 -0
- package/apps/ui/dist/client/assets/formatters-C12LmYaa.js +1 -0
- package/apps/ui/dist/client/assets/index-DdJ7ahIt.js +22 -0
- package/apps/ui/dist/client/assets/input-CEsI7EpI.js +1 -0
- package/apps/ui/dist/client/assets/metric-card-9jwBF7rG.js +1 -0
- package/apps/ui/dist/client/assets/page-header-Dr_h1CVv.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-uyNGnpjH.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-zoM8d2nH.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-D1CWVN-O.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-DukMuny6.js +1 -0
- package/apps/ui/dist/client/assets/routes-Gr2Wwh83.js +1 -0
- package/apps/ui/dist/client/assets/select-CFim44gT.js +1 -0
- package/apps/ui/dist/client/assets/settings-DqhyDxo2.js +1 -0
- package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-DT75NiBa.js +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-Df5VXIuZ.js +7 -0
- package/apps/ui/dist/client/favicon.ico +0 -0
- package/apps/ui/dist/client/logo192.png +0 -0
- package/apps/ui/dist/client/logo512.png +0 -0
- package/apps/ui/dist/client/manifest.json +25 -0
- package/apps/ui/dist/client/robots.txt +3 -0
- package/apps/ui/dist/server/assets/__23tanstack-start-plugin-adapters-BzCA6dXo.js +5 -0
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-C0V305Nt.js +99 -0
- package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +6 -0
- package/apps/ui/dist/server/assets/analytics-BMxW_bZL.js +139 -0
- package/apps/ui/dist/server/assets/button-CmTDnzOn.js +46 -0
- package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +19 -0
- package/apps/ui/dist/server/assets/codex-queries-CAF6HYiG.js +109 -0
- package/apps/ui/dist/server/assets/codex-server-BFZq2Y2O.js +2062 -0
- package/apps/ui/dist/server/assets/data-table-Cdct823O.js +189 -0
- package/apps/ui/dist/server/assets/delete-confirm-dialog-CWqcTXTF.js +139 -0
- package/apps/ui/dist/server/assets/download-C5rkk_Bo.js +289 -0
- package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +91 -0
- package/apps/ui/dist/server/assets/input-B4tEzctc.js +46 -0
- package/apps/ui/dist/server/assets/loading-panel-DbLdvjtR.js +27 -0
- package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +23 -0
- package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +13 -0
- package/apps/ui/dist/server/assets/page-header-CxdZM86z.js +25 -0
- package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +31 -0
- package/apps/ui/dist/server/assets/projects._project-CJ7l0ynC.js +18 -0
- package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +26 -0
- package/apps/ui/dist/server/assets/projects._project-CcJLp_A8.js +337 -0
- package/apps/ui/dist/server/assets/projects.index-CaplpeMy.js +26 -0
- package/apps/ui/dist/server/assets/projects.index-srtogpuF.js +172 -0
- package/apps/ui/dist/server/assets/router-C_w-haH6.js +307 -0
- package/apps/ui/dist/server/assets/routes-BhbxvJE7.js +34 -0
- package/apps/ui/dist/server/assets/routes-CPe-ppmC.js +169 -0
- package/apps/ui/dist/server/assets/select-GW76p-ld.js +76 -0
- package/apps/ui/dist/server/assets/settings-MvWDgc1u.js +100 -0
- package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +52 -0
- package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +13 -0
- package/apps/ui/dist/server/assets/start-HeKLHD9b.js +4 -0
- package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +26 -0
- package/apps/ui/dist/server/assets/threads._threadId-Ba7vv6-K.js +18 -0
- package/apps/ui/dist/server/assets/threads._threadId-euyNckhj.js +1059 -0
- package/apps/ui/dist/server/assets/utils-C_uf36nf.js +8 -0
- package/apps/ui/dist/server/server.js +5678 -0
- package/package.json +53 -7
- package/src/export-chats.ts +4 -18
- package/src/lib/claude-exporter.ts +1 -1
- package/src/lib/codex-analytics.ts +100 -0
- package/src/lib/codex-browser-db.ts +605 -0
- package/src/lib/codex-browser-export.ts +429 -0
- package/src/lib/codex-browser-types.ts +224 -0
- package/src/lib/codex-exporter-cli.ts +6 -1
- package/src/lib/codex-exporter-db.ts +19 -20
- package/src/lib/codex-exporter-transcript.ts +158 -34
- package/src/lib/codex-exporter-types.ts +8 -0
- package/src/lib/codex-thread-cache.ts +58 -0
- package/src/lib/codex-thread-parser.ts +604 -0
- package/src/lib/interactive-cli.ts +10 -25
- package/src/lib/model-label.ts +24 -0
- package/src/lib/native-open.ts +54 -0
- package/src/lib/path-transforms.ts +46 -0
- package/src/lib/shared.ts +15 -1
- package/src/lib/sqlite-error.ts +14 -0
- package/src/lib/sqlite-retry.ts +53 -0
- package/src/lib/ui-cache.ts +96 -0
- package/src/lib/ui-export-files.ts +77 -0
- package/src/mcp-server.ts +1 -0
- package/src/spiracha.ts +16 -4
- package/src/ui-cli.ts +310 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Database } from 'bun:sqlite';
|
|
2
1
|
import { readdir } from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
3
|
+
import { withReadonlyDb } from './codex-browser-db';
|
|
4
4
|
import {
|
|
5
5
|
type CodexCliOptions,
|
|
6
6
|
DEFAULT_CODEX_DIR,
|
|
@@ -17,33 +17,29 @@ export const loadThreadData = (dbPath: string, options: CodexCliOptions): Thread
|
|
|
17
17
|
const parentByChildId = new Map<string, SpawnEdgeRow>();
|
|
18
18
|
const childEdgesByParentId = new Map<string, SpawnEdgeRow[]>();
|
|
19
19
|
|
|
20
|
-
let db: Database | null = null;
|
|
21
|
-
|
|
22
20
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const threadRows = db.query(threadQuery.sql).all(...threadQuery.params) as ThreadRow[];
|
|
21
|
+
withReadonlyDb(dbPath, (db) => {
|
|
22
|
+
const threadQuery = buildThreadQuery(options);
|
|
23
|
+
const threadRows = db.query(threadQuery.sql).all(...threadQuery.params) as ThreadRow[];
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
for (const row of threadRows) {
|
|
26
|
+
threadsById.set(row.id, row);
|
|
27
|
+
}
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
const edgeQuery = buildSpawnEdgeQuery([...threadsById.keys()], options);
|
|
30
|
+
const edgeRows = db.query(edgeQuery.sql).all(...edgeQuery.params) as SpawnEdgeRow[];
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
for (const row of edgeRows) {
|
|
33
|
+
parentByChildId.set(row.child_thread_id, row);
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
const existing = childEdgesByParentId.get(row.parent_thread_id) ?? [];
|
|
36
|
+
existing.push(row);
|
|
37
|
+
childEdgesByParentId.set(row.parent_thread_id, existing);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
42
40
|
} catch (error) {
|
|
43
41
|
const message = error instanceof Error ? error.message : String(error);
|
|
44
42
|
throw new Error(`Failed to read thread database at ${dbPath}: ${message}`);
|
|
45
|
-
} finally {
|
|
46
|
-
db?.close();
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
return {
|
|
@@ -210,14 +206,17 @@ export const toOutputRelativePath = (
|
|
|
210
206
|
return flatName;
|
|
211
207
|
}
|
|
212
208
|
|
|
209
|
+
// Prefer preserving the input sessions tree when the rollout lives under the configured input root.
|
|
213
210
|
if (normalized.startsWith(`${inputRoot}${path.sep}`)) {
|
|
214
211
|
return path.relative(inputRoot, normalized).replace(/\.jsonl$/i, extension);
|
|
215
212
|
}
|
|
216
213
|
|
|
214
|
+
// Fall back to a stable Codex-relative path when the file is under ~/.codex.
|
|
217
215
|
if (normalized.startsWith(`${codexRoot}${path.sep}`)) {
|
|
218
216
|
return path.relative(codexRoot, normalized).replace(/\.jsonl$/i, extension);
|
|
219
217
|
}
|
|
220
218
|
|
|
219
|
+
// Otherwise collapse to the basename so ad hoc session files cannot escape the output directory.
|
|
221
220
|
return path.basename(normalized).replace(/\.jsonl$/i, extension);
|
|
222
221
|
};
|
|
223
222
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
1
3
|
import path from 'node:path';
|
|
4
|
+
import { finished } from 'node:stream/promises';
|
|
2
5
|
import { matchesFilters, toCodexRelativePath } from './codex-exporter-db';
|
|
3
6
|
import type { CodexCliOptions, ExportTarget, MessageRecord, SessionMeta, ToolRecord } from './codex-exporter-types';
|
|
4
7
|
import {
|
|
@@ -6,8 +9,11 @@ import {
|
|
|
6
9
|
asString,
|
|
7
10
|
cleanExtractedText,
|
|
8
11
|
cleanInlineTitle,
|
|
12
|
+
createExportWriteStream,
|
|
9
13
|
type ExportFormat,
|
|
14
|
+
finalizeExportWriteStream,
|
|
10
15
|
formatInlineLiteral,
|
|
16
|
+
formatModelLabel,
|
|
11
17
|
type JsonValue,
|
|
12
18
|
type MetadataEntry,
|
|
13
19
|
readJsonlObjects,
|
|
@@ -20,7 +26,7 @@ export const convertSessionFile = async (target: ExportTarget, options: CodexCli
|
|
|
20
26
|
let transcriptState: CodexTranscriptState;
|
|
21
27
|
|
|
22
28
|
try {
|
|
23
|
-
transcriptState = await collectCodexTranscript(target.sessionFile, options);
|
|
29
|
+
transcriptState = await collectCodexTranscript(target.sessionFile, options, target.thread?.model ?? null);
|
|
24
30
|
} catch (error) {
|
|
25
31
|
const message = error instanceof Error ? error.message : String(error);
|
|
26
32
|
throw new Error(`Failed to read Codex transcript ${target.sessionFile}: ${message}`);
|
|
@@ -49,14 +55,85 @@ export const convertSessionFile = async (target: ExportTarget, options: CodexCli
|
|
|
49
55
|
return parts.join('\n').trimEnd() + '\n';
|
|
50
56
|
};
|
|
51
57
|
|
|
58
|
+
type TranscriptTextTransform = (text: string) => string;
|
|
59
|
+
|
|
60
|
+
export const writeSessionFileExport = async (
|
|
61
|
+
target: ExportTarget,
|
|
62
|
+
options: CodexCliOptions,
|
|
63
|
+
outputPath: string,
|
|
64
|
+
transform: TranscriptTextTransform = (text) => text,
|
|
65
|
+
): Promise<boolean> => {
|
|
66
|
+
const transcriptOutputPath = `${outputPath}.transcript.tmp`;
|
|
67
|
+
let transcriptStream: any = null;
|
|
68
|
+
const state: CodexTranscriptState = {
|
|
69
|
+
assistantModel: target.thread?.model ?? null,
|
|
70
|
+
sections: [],
|
|
71
|
+
sessionMeta: {},
|
|
72
|
+
startedTranscript: false,
|
|
73
|
+
};
|
|
74
|
+
let wroteSection = false;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
transcriptStream = await createExportWriteStream(transcriptOutputPath);
|
|
78
|
+
for await (const parsed of readJsonlObjects(target.sessionFile)) {
|
|
79
|
+
captureSessionMeta(parsed, state.sessionMeta);
|
|
80
|
+
const block = renderCodexTranscriptRecord(parsed, options, state);
|
|
81
|
+
if (!block) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
transcriptStream.write(transform(wroteSection ? `${getSectionSeparator(options)}${block}` : block));
|
|
86
|
+
wroteSection = true;
|
|
87
|
+
}
|
|
88
|
+
await finalizeExportWriteStream(transcriptStream);
|
|
89
|
+
transcriptStream = null;
|
|
90
|
+
|
|
91
|
+
if (!matchesFilters(target.thread?.cwd ?? state.sessionMeta.cwd ?? null, options) || !wroteSection) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const outputStream = await createExportWriteStream(outputPath);
|
|
96
|
+
try {
|
|
97
|
+
const prefix = buildStreamExportPrefix(target, state.sessionMeta, options);
|
|
98
|
+
if (prefix) {
|
|
99
|
+
outputStream.write(transform(prefix));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const transcriptReadStream = createReadStream(transcriptOutputPath, { encoding: 'utf8' });
|
|
103
|
+
transcriptReadStream.pipe(outputStream, { end: false });
|
|
104
|
+
await finished(transcriptReadStream);
|
|
105
|
+
outputStream.write('\n');
|
|
106
|
+
await finalizeExportWriteStream(outputStream);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
outputStream.destroy();
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (transcriptStream) {
|
|
115
|
+
transcriptStream.destroy();
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
} finally {
|
|
119
|
+
await rm(transcriptOutputPath, { force: true });
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
52
123
|
type CodexTranscriptState = {
|
|
124
|
+
assistantModel: string | null;
|
|
53
125
|
sessionMeta: SessionMeta;
|
|
54
126
|
sections: string[];
|
|
55
127
|
startedTranscript: boolean;
|
|
56
128
|
};
|
|
57
129
|
|
|
58
|
-
const collectCodexTranscript = async (
|
|
130
|
+
const collectCodexTranscript = async (
|
|
131
|
+
sessionFile: string,
|
|
132
|
+
options: CodexCliOptions,
|
|
133
|
+
assistantModel: string | null = null,
|
|
134
|
+
): Promise<CodexTranscriptState> => {
|
|
59
135
|
const state: CodexTranscriptState = {
|
|
136
|
+
assistantModel,
|
|
60
137
|
sections: [],
|
|
61
138
|
sessionMeta: {},
|
|
62
139
|
startedTranscript: false,
|
|
@@ -69,46 +146,52 @@ const collectCodexTranscript = async (sessionFile: string, options: CodexCliOpti
|
|
|
69
146
|
return state;
|
|
70
147
|
};
|
|
71
148
|
|
|
149
|
+
const getSectionSeparator = (options: CodexCliOptions) => {
|
|
150
|
+
return options.optimized ? '\n\n' : '\n';
|
|
151
|
+
};
|
|
152
|
+
|
|
72
153
|
const processCodexTranscriptRecord = (
|
|
73
154
|
parsed: Record<string, JsonValue>,
|
|
74
155
|
options: CodexCliOptions,
|
|
75
156
|
state: CodexTranscriptState,
|
|
76
157
|
) => {
|
|
77
158
|
captureSessionMeta(parsed, state.sessionMeta);
|
|
159
|
+
const block = renderCodexTranscriptRecord(parsed, options, state);
|
|
160
|
+
if (block) {
|
|
161
|
+
state.sections.push(block);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
78
164
|
|
|
165
|
+
const renderCodexTranscriptRecord = (
|
|
166
|
+
parsed: Record<string, JsonValue>,
|
|
167
|
+
options: CodexCliOptions,
|
|
168
|
+
state: CodexTranscriptState,
|
|
169
|
+
) => {
|
|
79
170
|
const message = extractMessageRecord(parsed);
|
|
80
171
|
if (message) {
|
|
81
|
-
processCodexMessageRecord(message, options, state);
|
|
82
|
-
return;
|
|
172
|
+
return processCodexMessageRecord(message, options, state);
|
|
83
173
|
}
|
|
84
174
|
|
|
85
175
|
if (!options.includeTools) {
|
|
86
|
-
return;
|
|
176
|
+
return '';
|
|
87
177
|
}
|
|
88
178
|
|
|
89
179
|
const tool = extractToolRecord(parsed);
|
|
90
180
|
if (!tool) {
|
|
91
|
-
return;
|
|
181
|
+
return '';
|
|
92
182
|
}
|
|
93
183
|
|
|
94
|
-
|
|
184
|
+
return options.optimized
|
|
95
185
|
? renderCompactToolBlock(tool, options.outputFormat)
|
|
96
186
|
: renderToolBlock(tool, options.outputFormat);
|
|
97
|
-
if (block) {
|
|
98
|
-
state.sections.push(block);
|
|
99
|
-
}
|
|
100
187
|
};
|
|
101
188
|
|
|
102
189
|
const processCodexMessageRecord = (message: MessageRecord, options: CodexCliOptions, state: CodexTranscriptState) => {
|
|
103
190
|
if (options.optimized) {
|
|
104
|
-
processOptimizedCodexMessageRecord(message, options, state);
|
|
105
|
-
return;
|
|
191
|
+
return processOptimizedCodexMessageRecord(message, options, state);
|
|
106
192
|
}
|
|
107
193
|
|
|
108
|
-
|
|
109
|
-
if (block) {
|
|
110
|
-
state.sections.push(block);
|
|
111
|
-
}
|
|
194
|
+
return renderMessageBlock(message, options.outputFormat, state.assistantModel, options.includeCommentary);
|
|
112
195
|
};
|
|
113
196
|
|
|
114
197
|
const processOptimizedCodexMessageRecord = (
|
|
@@ -117,25 +200,44 @@ const processOptimizedCodexMessageRecord = (
|
|
|
117
200
|
state: CodexTranscriptState,
|
|
118
201
|
) => {
|
|
119
202
|
if (message.role !== 'user' && message.role !== 'assistant') {
|
|
120
|
-
return;
|
|
203
|
+
return '';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (message.role === 'assistant' && message.phase === 'commentary' && !options.includeCommentary) {
|
|
207
|
+
return '';
|
|
121
208
|
}
|
|
122
209
|
|
|
123
210
|
const compact = compactMessageText(message, true);
|
|
124
211
|
if (!compact) {
|
|
125
|
-
return;
|
|
212
|
+
return '';
|
|
126
213
|
}
|
|
127
214
|
|
|
128
215
|
if (!state.startedTranscript) {
|
|
129
216
|
if (shouldSkipOptimizedPrelude(message.role, compact)) {
|
|
130
|
-
return;
|
|
217
|
+
return '';
|
|
131
218
|
}
|
|
132
219
|
state.startedTranscript = true;
|
|
133
220
|
}
|
|
134
221
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
222
|
+
return renderCompactBlock(message, compact, options.outputFormat, state.assistantModel);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const buildStreamExportPrefix = (target: ExportTarget, sessionMeta: SessionMeta, options: CodexCliOptions) => {
|
|
226
|
+
if (options.optimized) {
|
|
227
|
+
return '';
|
|
138
228
|
}
|
|
229
|
+
|
|
230
|
+
const title = getTitle(target, sessionMeta);
|
|
231
|
+
const metadata = buildMetadataEntries(target, sessionMeta, options);
|
|
232
|
+
const parts = [
|
|
233
|
+
renderDocumentTitle(title, options.outputFormat),
|
|
234
|
+
'',
|
|
235
|
+
renderMetadataBlock(metadata, options.outputFormat),
|
|
236
|
+
]
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.join('\n');
|
|
239
|
+
|
|
240
|
+
return `${parts}\n`;
|
|
139
241
|
};
|
|
140
242
|
|
|
141
243
|
export const compactMessageText = (message: MessageRecord, optimized: boolean): string => {
|
|
@@ -183,17 +285,18 @@ export const formatToolOutputSummary = (outputText: string, outputFormat: Export
|
|
|
183
285
|
|
|
184
286
|
export const parseExecCommandArguments = (argumentsText?: string) => {
|
|
185
287
|
if (!argumentsText) {
|
|
186
|
-
return { cmd: null as string | null, workdir: null as string | null };
|
|
288
|
+
return { argumentsParseFailed: false, cmd: null as string | null, workdir: null as string | null };
|
|
187
289
|
}
|
|
188
290
|
|
|
189
291
|
try {
|
|
190
292
|
const parsed = JSON.parse(argumentsText) as Record<string, unknown>;
|
|
191
293
|
return {
|
|
294
|
+
argumentsParseFailed: false,
|
|
192
295
|
cmd: typeof parsed.cmd === 'string' ? parsed.cmd : null,
|
|
193
296
|
workdir: typeof parsed.workdir === 'string' ? parsed.workdir : null,
|
|
194
297
|
};
|
|
195
298
|
} catch {
|
|
196
|
-
return { cmd: null as string | null, workdir: null as string | null };
|
|
299
|
+
return { argumentsParseFailed: true, cmd: null as string | null, workdir: null as string | null };
|
|
197
300
|
}
|
|
198
301
|
};
|
|
199
302
|
|
|
@@ -397,7 +500,11 @@ const extractMessageRecord = (parsed: Record<string, JsonValue>): MessageRecord
|
|
|
397
500
|
}
|
|
398
501
|
|
|
399
502
|
const payload = asObject(parsed.payload);
|
|
400
|
-
if (!payload
|
|
503
|
+
if (!payload) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (payload.type !== 'message' && payload.type !== 'agent_message' && payload.type !== 'user_message') {
|
|
401
508
|
return null;
|
|
402
509
|
}
|
|
403
510
|
|
|
@@ -405,15 +512,17 @@ const extractMessageRecord = (parsed: Record<string, JsonValue>): MessageRecord
|
|
|
405
512
|
};
|
|
406
513
|
|
|
407
514
|
const normalizeMessage = (value: Record<string, JsonValue>): MessageRecord | null => {
|
|
408
|
-
const
|
|
409
|
-
const
|
|
515
|
+
const type = asString(value.type);
|
|
516
|
+
const role =
|
|
517
|
+
asString(value.role) ?? (type === 'agent_message' ? 'assistant' : type === 'user_message' ? 'user' : null);
|
|
518
|
+
const content = value.content ?? asString(value.message);
|
|
410
519
|
const phase = asString(value.phase);
|
|
411
520
|
|
|
412
521
|
if (!role || content === undefined) {
|
|
413
522
|
return null;
|
|
414
523
|
}
|
|
415
524
|
|
|
416
|
-
return { content, phase: phase ?? undefined, role };
|
|
525
|
+
return { content, model: asString(value.model), phase: phase ?? undefined, role };
|
|
417
526
|
};
|
|
418
527
|
|
|
419
528
|
const extractToolRecord = (parsed: Record<string, JsonValue>): ToolRecord | null => {
|
|
@@ -462,17 +571,26 @@ const extractToolRecord = (parsed: Record<string, JsonValue>): ToolRecord | null
|
|
|
462
571
|
return null;
|
|
463
572
|
};
|
|
464
573
|
|
|
465
|
-
const renderMessageBlock = (
|
|
574
|
+
const renderMessageBlock = (
|
|
575
|
+
message: MessageRecord,
|
|
576
|
+
outputFormat: ExportFormat,
|
|
577
|
+
assistantModel: string | null,
|
|
578
|
+
includeCommentary: boolean,
|
|
579
|
+
): string => {
|
|
466
580
|
if (message.role !== 'user' && message.role !== 'assistant') {
|
|
467
581
|
return '';
|
|
468
582
|
}
|
|
469
583
|
|
|
584
|
+
if (message.role === 'assistant' && message.phase === 'commentary' && !includeCommentary) {
|
|
585
|
+
return '';
|
|
586
|
+
}
|
|
587
|
+
|
|
470
588
|
const text = cleanExtractedText(extractText(message.content)).trim();
|
|
471
589
|
if (!text || shouldSkipMessage(message.role, text)) {
|
|
472
590
|
return '';
|
|
473
591
|
}
|
|
474
592
|
|
|
475
|
-
const title = message.role === 'user' ? 'User' :
|
|
593
|
+
const title = message.role === 'user' ? 'User' : formatModelLabel(message.model ?? assistantModel);
|
|
476
594
|
const body = message.phase ? `Phase: ${message.phase}\n\n${text}` : text;
|
|
477
595
|
|
|
478
596
|
return renderSection(title, body, outputFormat);
|
|
@@ -488,8 +606,13 @@ const renderToolBlock = (tool: ToolRecord, outputFormat: ExportFormat): string =
|
|
|
488
606
|
return summary ? renderSection('Tool Output', summary, outputFormat) : '';
|
|
489
607
|
};
|
|
490
608
|
|
|
491
|
-
const renderCompactBlock = (
|
|
492
|
-
|
|
609
|
+
const renderCompactBlock = (
|
|
610
|
+
message: MessageRecord,
|
|
611
|
+
text: string,
|
|
612
|
+
outputFormat: ExportFormat,
|
|
613
|
+
assistantModel: string | null,
|
|
614
|
+
): string => {
|
|
615
|
+
const prefix = message.role === 'user' ? 'U:' : `${formatModelLabel(message.model ?? assistantModel)}:`;
|
|
493
616
|
const lines = text.split('\n');
|
|
494
617
|
const [firstLine, ...rest] = lines;
|
|
495
618
|
|
|
@@ -525,11 +648,12 @@ const stripPreviewBlock = (text: string): string => {
|
|
|
525
648
|
|
|
526
649
|
const first = parts[0];
|
|
527
650
|
const second = parts[1];
|
|
651
|
+
const isTranscriptHeading = (value: string) => /^##\s+.+$/i.test(value);
|
|
528
652
|
const looksLikePreview =
|
|
529
653
|
!/^([UA]):/i.test(first) &&
|
|
530
|
-
|
|
654
|
+
!isTranscriptHeading(first) &&
|
|
531
655
|
/^([UA]):/i.test(second) === false &&
|
|
532
|
-
|
|
656
|
+
isTranscriptHeading(second);
|
|
533
657
|
|
|
534
658
|
if (!looksLikePreview) {
|
|
535
659
|
return text.trim();
|
|
@@ -10,6 +10,7 @@ export type CodexCliOptions = {
|
|
|
10
10
|
projectFilter: string | null;
|
|
11
11
|
threadIds: string[];
|
|
12
12
|
optimized: boolean;
|
|
13
|
+
includeCommentary: boolean;
|
|
13
14
|
includeTools: boolean;
|
|
14
15
|
outputFormat: ExportFormat;
|
|
15
16
|
flat: boolean;
|
|
@@ -35,11 +36,14 @@ export type SessionMeta = {
|
|
|
35
36
|
source?: string;
|
|
36
37
|
originator?: string;
|
|
37
38
|
cli_version?: string;
|
|
39
|
+
thread_source?: string;
|
|
40
|
+
model_provider?: string;
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
export type MessageRecord = {
|
|
41
44
|
role: string;
|
|
42
45
|
content: JsonValue;
|
|
46
|
+
model?: string | null;
|
|
43
47
|
phase?: string;
|
|
44
48
|
};
|
|
45
49
|
|
|
@@ -77,6 +81,10 @@ export type ThreadRow = {
|
|
|
77
81
|
model: string | null;
|
|
78
82
|
reasoning_effort: string | null;
|
|
79
83
|
agent_path: string | null;
|
|
84
|
+
created_at_ms: number | null;
|
|
85
|
+
updated_at_ms: number | null;
|
|
86
|
+
thread_source: string | null;
|
|
87
|
+
preview: string;
|
|
80
88
|
};
|
|
81
89
|
|
|
82
90
|
export type SpawnEdgeRow = {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { ParsedCodexTranscript } from './codex-browser-types';
|
|
4
|
+
import { parseCodexTranscriptFile } from './codex-thread-parser';
|
|
5
|
+
import { getFileFingerprint, hashCacheKeyParts, withCachedJson } from './ui-cache';
|
|
6
|
+
|
|
7
|
+
export const LARGE_THREAD_SIZE_BYTES = 100 * 1024 * 1024;
|
|
8
|
+
export const LARGE_THREAD_PREVIEW_EVENT_LIMIT = 200;
|
|
9
|
+
|
|
10
|
+
export const getCachedParsedCodexTranscript = async (sessionFile: string): Promise<ParsedCodexTranscript> => {
|
|
11
|
+
const fingerprint = await getFileFingerprint(sessionFile);
|
|
12
|
+
const key = `thread-${hashCacheKeyParts(path.basename(sessionFile), fingerprint)}`;
|
|
13
|
+
|
|
14
|
+
return withCachedJson(key, async () => parseCodexTranscriptFile(sessionFile));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type CachedThreadTranscriptPreviewOptions = {
|
|
18
|
+
largeTranscriptThresholdBytes?: number;
|
|
19
|
+
previewEventLimit?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getThreadRolloutLoadState = async (
|
|
23
|
+
sessionFile: string,
|
|
24
|
+
largeTranscriptThresholdBytes = LARGE_THREAD_SIZE_BYTES,
|
|
25
|
+
) => {
|
|
26
|
+
const metadata = await stat(sessionFile);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
fileSizeBytes: metadata.size,
|
|
30
|
+
shouldDeferTranscriptLoad: metadata.size > largeTranscriptThresholdBytes,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const getCachedThreadTranscriptPreview = async (
|
|
35
|
+
sessionFile: string,
|
|
36
|
+
options: CachedThreadTranscriptPreviewOptions = {},
|
|
37
|
+
): Promise<ParsedCodexTranscript> => {
|
|
38
|
+
const threshold = options.largeTranscriptThresholdBytes ?? LARGE_THREAD_SIZE_BYTES;
|
|
39
|
+
const previewEventLimit = options.previewEventLimit ?? LARGE_THREAD_PREVIEW_EVENT_LIMIT;
|
|
40
|
+
const fingerprint = await getFileFingerprint(sessionFile);
|
|
41
|
+
const { fileSizeBytes, shouldDeferTranscriptLoad } = await getThreadRolloutLoadState(sessionFile, threshold);
|
|
42
|
+
const key = `thread-preview-${hashCacheKeyParts(path.basename(sessionFile), fingerprint, String(threshold), String(previewEventLimit))}`;
|
|
43
|
+
|
|
44
|
+
return withCachedJson(key, async () => {
|
|
45
|
+
if (!shouldDeferTranscriptLoad) {
|
|
46
|
+
return parseCodexTranscriptFile(sessionFile, {
|
|
47
|
+
sourceFileSizeBytes: fileSizeBytes,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return parseCodexTranscriptFile(sessionFile, {
|
|
52
|
+
includeRaw: false,
|
|
53
|
+
maxEvents: previewEventLimit,
|
|
54
|
+
maxTurnContexts: 0,
|
|
55
|
+
sourceFileSizeBytes: fileSizeBytes,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
};
|