spiracha 1.0.0 → 1.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/AGENTS.md +28 -1
- package/README.md +47 -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-BjYaHqXk.js +1 -0
- package/apps/ui/dist/client/assets/checkbox-wPoGG3of.js +1 -0
- package/apps/ui/dist/client/assets/data-table-6yDgAdtf.js +4 -0
- package/apps/ui/dist/client/assets/delete-confirm-dialog-DJUAk7ha.js +11 -0
- package/apps/ui/dist/client/assets/download-BhWd-Pm5.js +1 -0
- package/apps/ui/dist/client/assets/es2015-BlyMI4CF.js +41 -0
- package/apps/ui/dist/client/assets/formatters-BxjZwWSE.js +1 -0
- package/apps/ui/dist/client/assets/index-T01rPkb4.js +22 -0
- package/apps/ui/dist/client/assets/input-B3YN8gzg.js +1 -0
- package/apps/ui/dist/client/assets/metric-card-BWW7TWER.js +1 -0
- package/apps/ui/dist/client/assets/page-header-BZ8Gnxgs.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-B7XcpoLt.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-EfBhCHPY.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-4vfIwLjw.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-DzEZ4pAJ.js +1 -0
- package/apps/ui/dist/client/assets/routes-CWCCZykE.js +1 -0
- package/apps/ui/dist/client/assets/select-DLXGsyZ4.js +1 -0
- package/apps/ui/dist/client/assets/settings-b0Xthfae.js +1 -0
- package/apps/ui/dist/client/assets/styles-8Wtc8YJw.css +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-CgtoCqTb.js +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-DBiDb38K.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-BjsXNYgm.js +99 -0
- package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +6 -0
- package/apps/ui/dist/server/assets/analytics-Br_fZB6a.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-Cqh0hb93.js +1995 -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-CzHmFWGk.js +286 -0
- package/apps/ui/dist/server/assets/formatters-B6o5pTY9.js +72 -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/page-header-CxdZM86z.js +25 -0
- package/apps/ui/dist/server/assets/path-transforms-DD1e7rhY.js +31 -0
- package/apps/ui/dist/server/assets/projects._project-Bwf6iJC-.js +335 -0
- package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +26 -0
- package/apps/ui/dist/server/assets/projects._project-DdVSdfPe.js +18 -0
- package/apps/ui/dist/server/assets/projects.index-CaplpeMy.js +26 -0
- package/apps/ui/dist/server/assets/projects.index-DKeVeqUZ.js +171 -0
- package/apps/ui/dist/server/assets/router-ve2Hrl2Y.js +307 -0
- package/apps/ui/dist/server/assets/routes-BJyx5OmO.js +34 -0
- package/apps/ui/dist/server/assets/routes-pkOwjjYc.js +168 -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-BAvbjjfs.js +4 -0
- package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +26 -0
- package/apps/ui/dist/server/assets/threads._threadId-D3PYZIwl.js +18 -0
- package/apps/ui/dist/server/assets/threads._threadId-D3xaWM86.js +1037 -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 +47 -7
- package/src/export-chats.ts +1 -14
- package/src/lib/codex-analytics.ts +100 -0
- package/src/lib/codex-browser-db.ts +518 -0
- package/src/lib/codex-browser-export.ts +418 -0
- package/src/lib/codex-browser-types.ts +224 -0
- package/src/lib/codex-exporter-cli.ts +5 -0
- package/src/lib/codex-exporter-transcript.ts +143 -32
- 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 +5 -13
- package/src/lib/native-open.ts +54 -0
- package/src/lib/path-transforms.ts +45 -0
- package/src/lib/shared.ts +37 -1
- package/src/lib/sqlite-error.ts +14 -0
- package/src/lib/sqlite-retry.ts +39 -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 +14 -1
- package/src/ui-cli.ts +310 -0
|
@@ -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,
|
|
@@ -49,7 +55,64 @@ 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
|
+
const transcriptStream = await createExportWriteStream(transcriptOutputPath);
|
|
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
|
+
for await (const parsed of readJsonlObjects(target.sessionFile)) {
|
|
78
|
+
captureSessionMeta(parsed, state.sessionMeta);
|
|
79
|
+
const block = renderCodexTranscriptRecord(parsed, options, state);
|
|
80
|
+
if (!block) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
transcriptStream.write(transform(wroteSection ? `${getSectionSeparator(options)}${block}` : block));
|
|
85
|
+
wroteSection = true;
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
transcriptStream.destroy();
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await finalizeExportWriteStream(transcriptStream);
|
|
93
|
+
|
|
94
|
+
if (!matchesFilters(target.thread?.cwd ?? state.sessionMeta.cwd ?? null, options) || !wroteSection) {
|
|
95
|
+
await rm(transcriptOutputPath, { force: true });
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const outputStream = await createExportWriteStream(outputPath);
|
|
100
|
+
const prefix = buildStreamExportPrefix(target, state.sessionMeta, options);
|
|
101
|
+
if (prefix) {
|
|
102
|
+
outputStream.write(transform(prefix));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const transcriptReadStream = createReadStream(transcriptOutputPath, { encoding: 'utf8' });
|
|
106
|
+
transcriptReadStream.pipe(outputStream, { end: false });
|
|
107
|
+
await finished(transcriptReadStream);
|
|
108
|
+
outputStream.write('\n');
|
|
109
|
+
await finalizeExportWriteStream(outputStream);
|
|
110
|
+
await rm(transcriptOutputPath, { force: true });
|
|
111
|
+
return true;
|
|
112
|
+
};
|
|
113
|
+
|
|
52
114
|
type CodexTranscriptState = {
|
|
115
|
+
assistantModel: string | null;
|
|
53
116
|
sessionMeta: SessionMeta;
|
|
54
117
|
sections: string[];
|
|
55
118
|
startedTranscript: boolean;
|
|
@@ -57,6 +120,7 @@ type CodexTranscriptState = {
|
|
|
57
120
|
|
|
58
121
|
const collectCodexTranscript = async (sessionFile: string, options: CodexCliOptions): Promise<CodexTranscriptState> => {
|
|
59
122
|
const state: CodexTranscriptState = {
|
|
123
|
+
assistantModel: null,
|
|
60
124
|
sections: [],
|
|
61
125
|
sessionMeta: {},
|
|
62
126
|
startedTranscript: false,
|
|
@@ -69,46 +133,52 @@ const collectCodexTranscript = async (sessionFile: string, options: CodexCliOpti
|
|
|
69
133
|
return state;
|
|
70
134
|
};
|
|
71
135
|
|
|
136
|
+
const getSectionSeparator = (options: CodexCliOptions) => {
|
|
137
|
+
return options.optimized ? '\n\n' : '\n';
|
|
138
|
+
};
|
|
139
|
+
|
|
72
140
|
const processCodexTranscriptRecord = (
|
|
73
141
|
parsed: Record<string, JsonValue>,
|
|
74
142
|
options: CodexCliOptions,
|
|
75
143
|
state: CodexTranscriptState,
|
|
76
144
|
) => {
|
|
77
145
|
captureSessionMeta(parsed, state.sessionMeta);
|
|
146
|
+
const block = renderCodexTranscriptRecord(parsed, options, state);
|
|
147
|
+
if (block) {
|
|
148
|
+
state.sections.push(block);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
78
151
|
|
|
152
|
+
const renderCodexTranscriptRecord = (
|
|
153
|
+
parsed: Record<string, JsonValue>,
|
|
154
|
+
options: CodexCliOptions,
|
|
155
|
+
state: CodexTranscriptState,
|
|
156
|
+
) => {
|
|
79
157
|
const message = extractMessageRecord(parsed);
|
|
80
158
|
if (message) {
|
|
81
|
-
processCodexMessageRecord(message, options, state);
|
|
82
|
-
return;
|
|
159
|
+
return processCodexMessageRecord(message, options, state);
|
|
83
160
|
}
|
|
84
161
|
|
|
85
162
|
if (!options.includeTools) {
|
|
86
|
-
return;
|
|
163
|
+
return '';
|
|
87
164
|
}
|
|
88
165
|
|
|
89
166
|
const tool = extractToolRecord(parsed);
|
|
90
167
|
if (!tool) {
|
|
91
|
-
return;
|
|
168
|
+
return '';
|
|
92
169
|
}
|
|
93
170
|
|
|
94
|
-
|
|
171
|
+
return options.optimized
|
|
95
172
|
? renderCompactToolBlock(tool, options.outputFormat)
|
|
96
173
|
: renderToolBlock(tool, options.outputFormat);
|
|
97
|
-
if (block) {
|
|
98
|
-
state.sections.push(block);
|
|
99
|
-
}
|
|
100
174
|
};
|
|
101
175
|
|
|
102
176
|
const processCodexMessageRecord = (message: MessageRecord, options: CodexCliOptions, state: CodexTranscriptState) => {
|
|
103
177
|
if (options.optimized) {
|
|
104
|
-
processOptimizedCodexMessageRecord(message, options, state);
|
|
105
|
-
return;
|
|
178
|
+
return processOptimizedCodexMessageRecord(message, options, state);
|
|
106
179
|
}
|
|
107
180
|
|
|
108
|
-
|
|
109
|
-
if (block) {
|
|
110
|
-
state.sections.push(block);
|
|
111
|
-
}
|
|
181
|
+
return renderMessageBlock(message, options.outputFormat, state.assistantModel, options.includeCommentary);
|
|
112
182
|
};
|
|
113
183
|
|
|
114
184
|
const processOptimizedCodexMessageRecord = (
|
|
@@ -117,25 +187,44 @@ const processOptimizedCodexMessageRecord = (
|
|
|
117
187
|
state: CodexTranscriptState,
|
|
118
188
|
) => {
|
|
119
189
|
if (message.role !== 'user' && message.role !== 'assistant') {
|
|
120
|
-
return;
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (message.role === 'assistant' && message.phase === 'commentary' && !options.includeCommentary) {
|
|
194
|
+
return '';
|
|
121
195
|
}
|
|
122
196
|
|
|
123
197
|
const compact = compactMessageText(message, true);
|
|
124
198
|
if (!compact) {
|
|
125
|
-
return;
|
|
199
|
+
return '';
|
|
126
200
|
}
|
|
127
201
|
|
|
128
202
|
if (!state.startedTranscript) {
|
|
129
203
|
if (shouldSkipOptimizedPrelude(message.role, compact)) {
|
|
130
|
-
return;
|
|
204
|
+
return '';
|
|
131
205
|
}
|
|
132
206
|
state.startedTranscript = true;
|
|
133
207
|
}
|
|
134
208
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
209
|
+
return renderCompactBlock(message, compact, options.outputFormat, state.assistantModel);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const buildStreamExportPrefix = (target: ExportTarget, sessionMeta: SessionMeta, options: CodexCliOptions) => {
|
|
213
|
+
if (options.optimized) {
|
|
214
|
+
return '';
|
|
138
215
|
}
|
|
216
|
+
|
|
217
|
+
const title = getTitle(target, sessionMeta);
|
|
218
|
+
const metadata = buildMetadataEntries(target, sessionMeta, options);
|
|
219
|
+
const parts = [
|
|
220
|
+
renderDocumentTitle(title, options.outputFormat),
|
|
221
|
+
'',
|
|
222
|
+
renderMetadataBlock(metadata, options.outputFormat),
|
|
223
|
+
]
|
|
224
|
+
.filter(Boolean)
|
|
225
|
+
.join('\n');
|
|
226
|
+
|
|
227
|
+
return `${parts}\n`;
|
|
139
228
|
};
|
|
140
229
|
|
|
141
230
|
export const compactMessageText = (message: MessageRecord, optimized: boolean): string => {
|
|
@@ -183,17 +272,18 @@ export const formatToolOutputSummary = (outputText: string, outputFormat: Export
|
|
|
183
272
|
|
|
184
273
|
export const parseExecCommandArguments = (argumentsText?: string) => {
|
|
185
274
|
if (!argumentsText) {
|
|
186
|
-
return { cmd: null as string | null, workdir: null as string | null };
|
|
275
|
+
return { argumentsParseFailed: false, cmd: null as string | null, workdir: null as string | null };
|
|
187
276
|
}
|
|
188
277
|
|
|
189
278
|
try {
|
|
190
279
|
const parsed = JSON.parse(argumentsText) as Record<string, unknown>;
|
|
191
280
|
return {
|
|
281
|
+
argumentsParseFailed: false,
|
|
192
282
|
cmd: typeof parsed.cmd === 'string' ? parsed.cmd : null,
|
|
193
283
|
workdir: typeof parsed.workdir === 'string' ? parsed.workdir : null,
|
|
194
284
|
};
|
|
195
285
|
} catch {
|
|
196
|
-
return { cmd: null as string | null, workdir: null as string | null };
|
|
286
|
+
return { argumentsParseFailed: true, cmd: null as string | null, workdir: null as string | null };
|
|
197
287
|
}
|
|
198
288
|
};
|
|
199
289
|
|
|
@@ -397,7 +487,11 @@ const extractMessageRecord = (parsed: Record<string, JsonValue>): MessageRecord
|
|
|
397
487
|
}
|
|
398
488
|
|
|
399
489
|
const payload = asObject(parsed.payload);
|
|
400
|
-
if (!payload
|
|
490
|
+
if (!payload) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (payload.type !== 'message' && payload.type !== 'agent_message' && payload.type !== 'user_message') {
|
|
401
495
|
return null;
|
|
402
496
|
}
|
|
403
497
|
|
|
@@ -405,15 +499,17 @@ const extractMessageRecord = (parsed: Record<string, JsonValue>): MessageRecord
|
|
|
405
499
|
};
|
|
406
500
|
|
|
407
501
|
const normalizeMessage = (value: Record<string, JsonValue>): MessageRecord | null => {
|
|
408
|
-
const
|
|
409
|
-
const
|
|
502
|
+
const type = asString(value.type);
|
|
503
|
+
const role =
|
|
504
|
+
asString(value.role) ?? (type === 'agent_message' ? 'assistant' : type === 'user_message' ? 'user' : null);
|
|
505
|
+
const content = value.content ?? asString(value.message);
|
|
410
506
|
const phase = asString(value.phase);
|
|
411
507
|
|
|
412
508
|
if (!role || content === undefined) {
|
|
413
509
|
return null;
|
|
414
510
|
}
|
|
415
511
|
|
|
416
|
-
return { content, phase: phase ?? undefined, role };
|
|
512
|
+
return { content, model: asString(value.model), phase: phase ?? undefined, role };
|
|
417
513
|
};
|
|
418
514
|
|
|
419
515
|
const extractToolRecord = (parsed: Record<string, JsonValue>): ToolRecord | null => {
|
|
@@ -462,17 +558,26 @@ const extractToolRecord = (parsed: Record<string, JsonValue>): ToolRecord | null
|
|
|
462
558
|
return null;
|
|
463
559
|
};
|
|
464
560
|
|
|
465
|
-
const renderMessageBlock = (
|
|
561
|
+
const renderMessageBlock = (
|
|
562
|
+
message: MessageRecord,
|
|
563
|
+
outputFormat: ExportFormat,
|
|
564
|
+
assistantModel: string | null,
|
|
565
|
+
includeCommentary: boolean,
|
|
566
|
+
): string => {
|
|
466
567
|
if (message.role !== 'user' && message.role !== 'assistant') {
|
|
467
568
|
return '';
|
|
468
569
|
}
|
|
469
570
|
|
|
571
|
+
if (message.role === 'assistant' && message.phase === 'commentary' && !includeCommentary) {
|
|
572
|
+
return '';
|
|
573
|
+
}
|
|
574
|
+
|
|
470
575
|
const text = cleanExtractedText(extractText(message.content)).trim();
|
|
471
576
|
if (!text || shouldSkipMessage(message.role, text)) {
|
|
472
577
|
return '';
|
|
473
578
|
}
|
|
474
579
|
|
|
475
|
-
const title = message.role === 'user' ? 'User' :
|
|
580
|
+
const title = message.role === 'user' ? 'User' : formatModelLabel(message.model ?? assistantModel);
|
|
476
581
|
const body = message.phase ? `Phase: ${message.phase}\n\n${text}` : text;
|
|
477
582
|
|
|
478
583
|
return renderSection(title, body, outputFormat);
|
|
@@ -488,8 +593,13 @@ const renderToolBlock = (tool: ToolRecord, outputFormat: ExportFormat): string =
|
|
|
488
593
|
return summary ? renderSection('Tool Output', summary, outputFormat) : '';
|
|
489
594
|
};
|
|
490
595
|
|
|
491
|
-
const renderCompactBlock = (
|
|
492
|
-
|
|
596
|
+
const renderCompactBlock = (
|
|
597
|
+
message: MessageRecord,
|
|
598
|
+
text: string,
|
|
599
|
+
outputFormat: ExportFormat,
|
|
600
|
+
assistantModel: string | null,
|
|
601
|
+
): string => {
|
|
602
|
+
const prefix = message.role === 'user' ? 'U:' : `${formatModelLabel(message.model ?? assistantModel)}:`;
|
|
493
603
|
const lines = text.split('\n');
|
|
494
604
|
const [firstLine, ...rest] = lines;
|
|
495
605
|
|
|
@@ -525,11 +635,12 @@ const stripPreviewBlock = (text: string): string => {
|
|
|
525
635
|
|
|
526
636
|
const first = parts[0];
|
|
527
637
|
const second = parts[1];
|
|
638
|
+
const isTranscriptHeading = (value: string) => /^##\s+.+$/i.test(value);
|
|
528
639
|
const looksLikePreview =
|
|
529
640
|
!/^([UA]):/i.test(first) &&
|
|
530
|
-
|
|
641
|
+
!isTranscriptHeading(first) &&
|
|
531
642
|
/^([UA]):/i.test(second) === false &&
|
|
532
|
-
|
|
643
|
+
isTranscriptHeading(second);
|
|
533
644
|
|
|
534
645
|
if (!looksLikePreview) {
|
|
535
646
|
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
|
+
};
|