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,6 +1,10 @@
|
|
|
1
1
|
import { createServerFn } from '@tanstack/react-start';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { renderSourceSessionDownload } from './source-session-export-server';
|
|
3
|
+
import { renderSourceSessionDownload, renderSourceSessionsDownload } from './source-session-export-server';
|
|
4
|
+
|
|
5
|
+
const LARGE_CLAUDE_CODE_SESSION_SIZE_BYTES = 8 * 1024 * 1024;
|
|
6
|
+
const CLAUDE_CODE_DETAIL_PREVIEW_ENTRY_LIMIT = 400;
|
|
7
|
+
const CLAUDE_CODE_DETAIL_PREVIEW_LEADING_ENTRY_LIMIT = 100;
|
|
4
8
|
|
|
5
9
|
const workspaceSchema = z.object({
|
|
6
10
|
workspaceKey: z.string().min(1),
|
|
@@ -19,6 +23,19 @@ const exportSessionSchema = z.object({
|
|
|
19
23
|
zipArchive: z.boolean().default(false),
|
|
20
24
|
});
|
|
21
25
|
|
|
26
|
+
const exportSessionsSchema = z.object({
|
|
27
|
+
includeCommentary: z.boolean().default(true),
|
|
28
|
+
includeMetadata: z.boolean().default(true),
|
|
29
|
+
includeTools: z.boolean().default(true),
|
|
30
|
+
outputFormat: z.enum(['md', 'txt']).default('md'),
|
|
31
|
+
sessionIds: z.array(z.string().min(1)).min(1),
|
|
32
|
+
zipArchive: z.boolean().default(true),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const deleteSessionsSchema = z.object({
|
|
36
|
+
sessionIds: z.array(z.string().min(1)).min(1),
|
|
37
|
+
});
|
|
38
|
+
|
|
22
39
|
export const listClaudeCodeWorkspacesFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
23
40
|
const { listClaudeCodeWorkspaceGroups } = await import('@spiracha/lib/claude-code-db');
|
|
24
41
|
return listClaudeCodeWorkspaceGroups();
|
|
@@ -32,21 +49,112 @@ export const listClaudeCodeSessionsFn = createServerFn({ method: 'GET' })
|
|
|
32
49
|
});
|
|
33
50
|
|
|
34
51
|
const loadClaudeCodeSessionTranscript = async (sessionId: string) => {
|
|
52
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
35
53
|
const { readClaudeCodeSessionTranscript, resolveClaudeCodeProjectsDir } = await import(
|
|
36
54
|
'@spiracha/lib/claude-code-db'
|
|
37
55
|
);
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
const projectsDir = resolveClaudeCodeProjectsDir();
|
|
57
|
+
return runWithTranscriptLoadLimit(
|
|
58
|
+
async () => {
|
|
59
|
+
const transcript = await readClaudeCodeSessionTranscript(projectsDir, sessionId);
|
|
60
|
+
if (!transcript) {
|
|
61
|
+
throw new Error(`Claude Code session not found: ${sessionId}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return transcript;
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: sessionId,
|
|
68
|
+
path: projectsDir,
|
|
69
|
+
source: 'claude-code-ui',
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const buildClaudeCodeSessionDetailPreview = (
|
|
75
|
+
transcript: Awaited<ReturnType<typeof loadClaudeCodeSessionTranscript>>,
|
|
76
|
+
) => {
|
|
77
|
+
if (transcript.entries.length <= CLAUDE_CODE_DETAIL_PREVIEW_ENTRY_LIMIT) {
|
|
78
|
+
return transcript;
|
|
41
79
|
}
|
|
42
80
|
|
|
43
|
-
|
|
81
|
+
const trailingEntryLimit = CLAUDE_CODE_DETAIL_PREVIEW_ENTRY_LIMIT - CLAUDE_CODE_DETAIL_PREVIEW_LEADING_ENTRY_LIMIT;
|
|
82
|
+
return {
|
|
83
|
+
...transcript,
|
|
84
|
+
entries: [
|
|
85
|
+
...transcript.entries.slice(0, CLAUDE_CODE_DETAIL_PREVIEW_LEADING_ENTRY_LIMIT),
|
|
86
|
+
...transcript.entries.slice(-trailingEntryLimit),
|
|
87
|
+
].map((entry) => ({
|
|
88
|
+
...entry,
|
|
89
|
+
parts: entry.parts.map((part) => ({ ...part, raw: {} })),
|
|
90
|
+
raw: {},
|
|
91
|
+
})),
|
|
92
|
+
isPartial: true,
|
|
93
|
+
omittedEntryCount: transcript.entries.length - CLAUDE_CODE_DETAIL_PREVIEW_ENTRY_LIMIT,
|
|
94
|
+
rawEvents: [],
|
|
95
|
+
rawPayloadsOmitted: true,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const loadClaudeCodeSessionDetail = async (sessionId: string) => {
|
|
100
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
101
|
+
const { readClaudeCodeSessionTranscript, resolveClaudeCodeProjectsDir } = await import(
|
|
102
|
+
'@spiracha/lib/claude-code-db'
|
|
103
|
+
);
|
|
104
|
+
const projectsDir = resolveClaudeCodeProjectsDir();
|
|
105
|
+
return runWithTranscriptLoadLimit(
|
|
106
|
+
async () => {
|
|
107
|
+
const transcript = await readClaudeCodeSessionTranscript(projectsDir, sessionId, {
|
|
108
|
+
maxRawPayloadFileSizeBytes: LARGE_CLAUDE_CODE_SESSION_SIZE_BYTES,
|
|
109
|
+
});
|
|
110
|
+
if (!transcript) {
|
|
111
|
+
throw new Error(`Claude Code session not found: ${sessionId}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return buildClaudeCodeSessionDetailPreview(transcript);
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: sessionId,
|
|
118
|
+
path: projectsDir,
|
|
119
|
+
source: 'claude-code-ui-detail',
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const loadClaudeCodeSessionFullDetail = async (sessionId: string) => {
|
|
125
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
126
|
+
const { readClaudeCodeSessionTranscript, resolveClaudeCodeProjectsDir } = await import(
|
|
127
|
+
'@spiracha/lib/claude-code-db'
|
|
128
|
+
);
|
|
129
|
+
const projectsDir = resolveClaudeCodeProjectsDir();
|
|
130
|
+
return runWithTranscriptLoadLimit(
|
|
131
|
+
async () => {
|
|
132
|
+
const transcript = await readClaudeCodeSessionTranscript(projectsDir, sessionId, {
|
|
133
|
+
includeRawPayloads: false,
|
|
134
|
+
});
|
|
135
|
+
if (!transcript) {
|
|
136
|
+
throw new Error(`Claude Code session not found: ${sessionId}`);
|
|
137
|
+
}
|
|
138
|
+
return transcript;
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: sessionId,
|
|
142
|
+
path: projectsDir,
|
|
143
|
+
source: 'claude-code-ui-full-detail',
|
|
144
|
+
},
|
|
145
|
+
);
|
|
44
146
|
};
|
|
45
147
|
|
|
46
148
|
export const getClaudeCodeSessionDetailFn = createServerFn({ method: 'GET' })
|
|
47
149
|
.validator(sessionSchema)
|
|
48
150
|
.handler(async ({ data }) => {
|
|
49
|
-
return
|
|
151
|
+
return loadClaudeCodeSessionDetail(data.sessionId);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export const getClaudeCodeSessionTranscriptFn = createServerFn({ method: 'GET' })
|
|
155
|
+
.validator(sessionSchema)
|
|
156
|
+
.handler(async ({ data }) => {
|
|
157
|
+
return loadClaudeCodeSessionFullDetail(data.sessionId);
|
|
50
158
|
});
|
|
51
159
|
|
|
52
160
|
export const exportClaudeCodeSessionFn = createServerFn({ method: 'POST' })
|
|
@@ -67,9 +175,69 @@ export const exportClaudeCodeSessionFn = createServerFn({ method: 'POST' })
|
|
|
67
175
|
|
|
68
176
|
return renderSourceSessionDownload({
|
|
69
177
|
content,
|
|
178
|
+
cwd: transcript.session.cwd ?? transcript.session.worktree,
|
|
70
179
|
fallbackBaseName: 'claude-code-session',
|
|
71
|
-
fileBaseName: transcript.session.title || transcript.session.sessionId,
|
|
72
180
|
outputFormat: data.outputFormat,
|
|
181
|
+
sessionId: transcript.session.sessionId,
|
|
182
|
+
updatedAtMs: transcript.session.lastActiveAtMs,
|
|
73
183
|
zipArchive: data.zipArchive,
|
|
74
184
|
});
|
|
75
185
|
});
|
|
186
|
+
|
|
187
|
+
export const exportClaudeCodeSessionsFn = createServerFn({ method: 'POST' })
|
|
188
|
+
.validator(exportSessionsSchema)
|
|
189
|
+
.handler(async ({ data }) => {
|
|
190
|
+
const { renderClaudeCodeTranscript } = await import('@spiracha/lib/claude-code-transcript');
|
|
191
|
+
const entries = await Promise.all(
|
|
192
|
+
data.sessionIds.map(async (sessionId) => {
|
|
193
|
+
const transcript = await loadClaudeCodeSessionTranscript(sessionId);
|
|
194
|
+
const content = renderClaudeCodeTranscript(transcript, {
|
|
195
|
+
includeCommentary: data.includeCommentary,
|
|
196
|
+
includeMetadata: data.includeMetadata,
|
|
197
|
+
includeTools: data.includeTools,
|
|
198
|
+
outputFormat: data.outputFormat,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (!content) {
|
|
202
|
+
throw new Error(`Claude Code session has no exportable content: ${sessionId}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
content,
|
|
207
|
+
cwd: transcript.session.cwd ?? transcript.session.worktree,
|
|
208
|
+
fallbackBaseName: 'claude-code-session',
|
|
209
|
+
fileBaseName: transcript.session.title || transcript.session.sessionId,
|
|
210
|
+
sessionId: transcript.session.sessionId,
|
|
211
|
+
updatedAtMs: transcript.session.lastActiveAtMs,
|
|
212
|
+
};
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return renderSourceSessionsDownload({
|
|
217
|
+
entries,
|
|
218
|
+
fallbackBaseName: 'claude-code-sessions',
|
|
219
|
+
outputFormat: data.outputFormat,
|
|
220
|
+
zipArchive: data.zipArchive,
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
export const deleteClaudeCodeSessionFn = createServerFn({ method: 'POST' })
|
|
225
|
+
.validator(sessionSchema)
|
|
226
|
+
.handler(async ({ data }) => {
|
|
227
|
+
const { deleteClaudeCodeSession, resolveClaudeCodeProjectsDir } = await import('@spiracha/lib/claude-code-db');
|
|
228
|
+
return deleteClaudeCodeSession(resolveClaudeCodeProjectsDir(), data.sessionId);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
export const deleteClaudeCodeSessionsFn = createServerFn({ method: 'POST' })
|
|
232
|
+
.validator(deleteSessionsSchema)
|
|
233
|
+
.handler(async ({ data }) => {
|
|
234
|
+
const { deleteClaudeCodeSession, resolveClaudeCodeProjectsDir } = await import('@spiracha/lib/claude-code-db');
|
|
235
|
+
const projectsDir = resolveClaudeCodeProjectsDir();
|
|
236
|
+
const results = await Promise.all(
|
|
237
|
+
data.sessionIds.map((sessionId) => deleteClaudeCodeSession(projectsDir, sessionId)),
|
|
238
|
+
);
|
|
239
|
+
return {
|
|
240
|
+
deletedFiles: [...new Set(results.flatMap((result) => result.deletedFiles))],
|
|
241
|
+
deletedSessionIds: [...new Set(results.flatMap((result) => result.deletedSessionIds))],
|
|
242
|
+
};
|
|
243
|
+
});
|
|
@@ -3,7 +3,10 @@ import type {
|
|
|
3
3
|
ClaudeCodeTranscriptEntry,
|
|
4
4
|
ClaudeCodeTranscriptPart,
|
|
5
5
|
} from '@spiracha/lib/claude-code-exporter-types';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getClaudeCodeAssistantMessagePhase,
|
|
8
|
+
isClaudeCodeSyntheticTranscriptEntry,
|
|
9
|
+
} from '@spiracha/lib/claude-code-transcript-phase';
|
|
7
10
|
import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-types';
|
|
8
11
|
import type { JsonValue } from '@spiracha/lib/shared';
|
|
9
12
|
import { getThreadTranscriptStats } from './thread-transcript-stats';
|
|
@@ -141,6 +144,10 @@ export const claudeCodeTranscriptToThreadEvents = (transcript: ClaudeCodeSession
|
|
|
141
144
|
const events: ThreadEvent[] = [];
|
|
142
145
|
let sequence = 0;
|
|
143
146
|
for (const entry of transcript.entries) {
|
|
147
|
+
if (isClaudeCodeSyntheticTranscriptEntry(entry)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
144
151
|
for (const part of entry.parts) {
|
|
145
152
|
events.push(...partToEvents(entry, part, sequence));
|
|
146
153
|
sequence += 1;
|
|
@@ -5,10 +5,18 @@ import {
|
|
|
5
5
|
getDashboardSummaryFn,
|
|
6
6
|
getThreadSnapshotFn,
|
|
7
7
|
getThreadTranscriptFn,
|
|
8
|
+
getThreadTranscriptPreviewFn,
|
|
8
9
|
listProjectsFn,
|
|
9
10
|
listProjectThreadsFn,
|
|
10
11
|
} from './codex-server';
|
|
11
12
|
|
|
13
|
+
type ThreadTranscriptFilters = {
|
|
14
|
+
showCommentary: boolean;
|
|
15
|
+
showExtraEvents: boolean;
|
|
16
|
+
showToolCalls: boolean;
|
|
17
|
+
showUserMessages: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
12
20
|
const retrySqliteQuery = (failureCount: number, error: unknown) => {
|
|
13
21
|
return failureCount < 3 && isRetryableSqliteError(error);
|
|
14
22
|
};
|
|
@@ -41,10 +49,18 @@ export const projectThreadsQueryOptions = (project: string) =>
|
|
|
41
49
|
retryDelay,
|
|
42
50
|
});
|
|
43
51
|
|
|
44
|
-
export const threadSnapshotQueryOptions = (threadId: string) =>
|
|
52
|
+
export const threadSnapshotQueryOptions = (threadId: string, filters?: ThreadTranscriptFilters) =>
|
|
53
|
+
queryOptions({
|
|
54
|
+
queryFn: () => getThreadSnapshotFn({ data: { filters, threadId } }),
|
|
55
|
+
queryKey: ['thread', threadId, filters ?? 'all'],
|
|
56
|
+
retry: retrySqliteQuery,
|
|
57
|
+
retryDelay,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const threadTranscriptPreviewQueryOptions = (threadId: string, filters?: ThreadTranscriptFilters) =>
|
|
45
61
|
queryOptions({
|
|
46
|
-
queryFn: () =>
|
|
47
|
-
queryKey: ['thread', threadId],
|
|
62
|
+
queryFn: () => getThreadTranscriptPreviewFn({ data: { filters, threadId } }),
|
|
63
|
+
queryKey: ['thread-transcript-preview', threadId, filters ?? 'all'],
|
|
48
64
|
retry: retrySqliteQuery,
|
|
49
65
|
retryDelay,
|
|
50
66
|
});
|
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
deleteCodexProject,
|
|
4
|
-
deleteCodexThread,
|
|
5
|
-
deleteCodexThreads,
|
|
6
|
-
getCodexDashboardSummary,
|
|
7
|
-
getThreadBrowseData,
|
|
8
|
-
listCodexProjects,
|
|
9
|
-
listProjectThreads,
|
|
10
|
-
resolveCodexThreadDbPath,
|
|
11
|
-
} from '@spiracha/lib/codex-browser-db';
|
|
12
|
-
import { renderCodexThreadDownload, renderCodexThreadsDownload } from '@spiracha/lib/codex-browser-export';
|
|
13
|
-
import {
|
|
14
|
-
getCachedParsedCodexTranscript,
|
|
15
|
-
getCachedThreadTranscriptPreview,
|
|
16
|
-
getThreadRolloutLoadState,
|
|
17
|
-
} from '@spiracha/lib/codex-thread-cache';
|
|
18
|
-
import { recoverCodexProjectThreads } from '@spiracha/lib/codex-thread-recovery';
|
|
1
|
+
import type { ParsedCodexTranscript } from '@spiracha/lib/codex-browser-types';
|
|
19
2
|
import { createServerFn } from '@tanstack/react-start';
|
|
20
3
|
import { z } from 'zod';
|
|
21
4
|
|
|
@@ -32,6 +15,18 @@ const threadSchema = z.object({
|
|
|
32
15
|
threadId: z.string().min(1),
|
|
33
16
|
});
|
|
34
17
|
|
|
18
|
+
const transcriptFiltersSchema = z.object({
|
|
19
|
+
showCommentary: z.boolean(),
|
|
20
|
+
showExtraEvents: z.boolean(),
|
|
21
|
+
showToolCalls: z.boolean(),
|
|
22
|
+
showUserMessages: z.boolean(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const threadSnapshotSchema = z.object({
|
|
26
|
+
filters: transcriptFiltersSchema.optional(),
|
|
27
|
+
threadId: z.string().min(1),
|
|
28
|
+
});
|
|
29
|
+
|
|
35
30
|
const deleteThreadSchema = z.object({
|
|
36
31
|
deleteSessionFiles: z.boolean().default(false),
|
|
37
32
|
threadId: z.string().min(1),
|
|
@@ -68,76 +63,157 @@ const exportThreadsSchema = z.object({
|
|
|
68
63
|
zipArchive: z.boolean().default(true),
|
|
69
64
|
});
|
|
70
65
|
|
|
71
|
-
const getDbPath = () =>
|
|
66
|
+
const getDbPath = async () => {
|
|
67
|
+
const configuredDbPath = process.env.SPIRACHA_CODEX_DB?.trim();
|
|
68
|
+
if (configuredDbPath) {
|
|
69
|
+
return configuredDbPath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { resolveCodexThreadDbPath } = await import('@spiracha/lib/codex-browser-db');
|
|
73
|
+
return resolveCodexThreadDbPath();
|
|
74
|
+
};
|
|
72
75
|
|
|
73
76
|
const isMissingFileError = (error: unknown) => {
|
|
74
77
|
return error instanceof Error && /ENOENT|no such file/i.test(error.message);
|
|
75
78
|
};
|
|
76
79
|
|
|
80
|
+
const logCodexThreadLoad = (event: string, details: Record<string, unknown>) => {
|
|
81
|
+
console.info(`[spiracha:codex-thread] ${event}`, details);
|
|
82
|
+
};
|
|
83
|
+
|
|
77
84
|
export const getDashboardSummaryFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
78
|
-
|
|
85
|
+
const { getCodexDashboardSummary } = await import('@spiracha/lib/codex-browser-db');
|
|
86
|
+
return getCodexDashboardSummary(await getDbPath());
|
|
79
87
|
});
|
|
80
88
|
|
|
81
89
|
export const listProjectsFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
82
|
-
|
|
90
|
+
const { listCodexProjects } = await import('@spiracha/lib/codex-browser-db');
|
|
91
|
+
return listCodexProjects(await getDbPath());
|
|
83
92
|
});
|
|
84
93
|
|
|
85
94
|
export const listProjectThreadsFn = createServerFn({ method: 'GET' })
|
|
86
95
|
.validator(projectSchema)
|
|
87
96
|
.handler(async ({ data }) => {
|
|
88
|
-
|
|
97
|
+
const { listProjectThreads } = await import('@spiracha/lib/codex-browser-db');
|
|
98
|
+
return listProjectThreads(await getDbPath(), data.project, {
|
|
89
99
|
includeTranscriptStats: false,
|
|
90
100
|
});
|
|
91
101
|
});
|
|
92
102
|
|
|
93
103
|
export const getThreadSnapshotFn = createServerFn({ method: 'GET' })
|
|
94
|
-
.validator(
|
|
104
|
+
.validator(threadSnapshotSchema)
|
|
95
105
|
.handler(async ({ data }) => {
|
|
96
|
-
const
|
|
106
|
+
const startedAt = Date.now();
|
|
107
|
+
const [{ getThreadBrowseData }, { getThreadRolloutLoadState }] = await Promise.all([
|
|
108
|
+
import('@spiracha/lib/codex-browser-db'),
|
|
109
|
+
import('@spiracha/lib/codex-thread-cache'),
|
|
110
|
+
]);
|
|
111
|
+
const dbPath = await getDbPath();
|
|
112
|
+
logCodexThreadLoad('snapshot_start', {
|
|
113
|
+
threadId: data.threadId,
|
|
114
|
+
});
|
|
97
115
|
const browseData = getThreadBrowseData(dbPath, data.threadId);
|
|
98
|
-
const
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
transcript = await getCachedParsedCodexTranscript(browseData.thread.rollout_path);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (!isMissingFileError(error)) {
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
transcriptState = 'missing';
|
|
116
|
+
const transcript: ParsedCodexTranscript | null = null;
|
|
117
|
+
let rollout: Awaited<ReturnType<typeof getThreadRolloutLoadState>>;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
rollout = await getThreadRolloutLoadState(browseData.thread.rollout_path);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!isMissingFileError(error)) {
|
|
123
|
+
throw error;
|
|
113
124
|
}
|
|
125
|
+
|
|
126
|
+
rollout = {
|
|
127
|
+
fileSizeBytes: null,
|
|
128
|
+
shouldDeferTranscriptLoad: false,
|
|
129
|
+
};
|
|
114
130
|
}
|
|
115
131
|
|
|
132
|
+
const transcriptState: 'available' | 'deferred' | 'missing' =
|
|
133
|
+
rollout.fileSizeBytes === null ? 'missing' : rollout.shouldDeferTranscriptLoad ? 'deferred' : 'available';
|
|
134
|
+
logCodexThreadLoad('snapshot_ready', {
|
|
135
|
+
durationMs: Date.now() - startedAt,
|
|
136
|
+
fileSizeBytes: rollout.fileSizeBytes,
|
|
137
|
+
shouldDeferTranscriptLoad: rollout.shouldDeferTranscriptLoad,
|
|
138
|
+
threadId: data.threadId,
|
|
139
|
+
transcriptState,
|
|
140
|
+
});
|
|
141
|
+
|
|
116
142
|
return {
|
|
117
143
|
...browseData,
|
|
118
|
-
availableTools:
|
|
119
|
-
browseData.dynamicTools.length > 0
|
|
120
|
-
? browseData.dynamicTools
|
|
121
|
-
: (transcript?.sessionMeta.dynamicTools ?? []),
|
|
144
|
+
availableTools: browseData.dynamicTools,
|
|
122
145
|
rollout,
|
|
123
146
|
transcript,
|
|
124
147
|
transcriptState,
|
|
125
148
|
};
|
|
126
149
|
});
|
|
127
150
|
|
|
151
|
+
export const loadThreadTranscriptPreview = async (
|
|
152
|
+
threadId: string,
|
|
153
|
+
filters?: z.infer<typeof transcriptFiltersSchema>,
|
|
154
|
+
) => {
|
|
155
|
+
const startedAt = Date.now();
|
|
156
|
+
const [{ getThreadBrowseData }, { getCachedThreadTranscriptPreview }] = await Promise.all([
|
|
157
|
+
import('@spiracha/lib/codex-browser-db'),
|
|
158
|
+
import('@spiracha/lib/codex-thread-cache'),
|
|
159
|
+
]);
|
|
160
|
+
const dbPath = await getDbPath();
|
|
161
|
+
const browseData = getThreadBrowseData(dbPath, threadId);
|
|
162
|
+
logCodexThreadLoad('preview_start', {
|
|
163
|
+
rolloutPath: browseData.thread.rollout_path,
|
|
164
|
+
threadId,
|
|
165
|
+
});
|
|
166
|
+
const transcript = await getCachedThreadTranscriptPreview(browseData.thread.rollout_path, {
|
|
167
|
+
filters,
|
|
168
|
+
});
|
|
169
|
+
logCodexThreadLoad('preview_ready', {
|
|
170
|
+
durationMs: Date.now() - startedAt,
|
|
171
|
+
eventCount: transcript.events.length,
|
|
172
|
+
isPartial: transcript.isPartial,
|
|
173
|
+
threadId,
|
|
174
|
+
});
|
|
175
|
+
return transcript;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const loadThreadTranscript = async (threadId: string) => {
|
|
179
|
+
const [{ getThreadBrowseData }, { getCachedParsedCodexTranscript }] = await Promise.all([
|
|
180
|
+
import('@spiracha/lib/codex-browser-db'),
|
|
181
|
+
import('@spiracha/lib/codex-thread-cache'),
|
|
182
|
+
]);
|
|
183
|
+
const dbPath = await getDbPath();
|
|
184
|
+
const browseData = getThreadBrowseData(dbPath, threadId);
|
|
185
|
+
const startedAt = Date.now();
|
|
186
|
+
logCodexThreadLoad('full_start', {
|
|
187
|
+
rolloutPath: browseData.thread.rollout_path,
|
|
188
|
+
threadId,
|
|
189
|
+
});
|
|
190
|
+
const transcript = await getCachedParsedCodexTranscript(browseData.thread.rollout_path);
|
|
191
|
+
logCodexThreadLoad('full_ready', {
|
|
192
|
+
durationMs: Date.now() - startedAt,
|
|
193
|
+
eventCount: transcript.events.length,
|
|
194
|
+
threadId,
|
|
195
|
+
});
|
|
196
|
+
return transcript;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const getThreadTranscriptPreviewFn = createServerFn({ method: 'GET' })
|
|
200
|
+
.validator(threadSnapshotSchema)
|
|
201
|
+
.handler(async ({ data }) => {
|
|
202
|
+
return loadThreadTranscriptPreview(data.threadId, data.filters);
|
|
203
|
+
});
|
|
204
|
+
|
|
128
205
|
export const getThreadTranscriptFn = createServerFn({ method: 'GET' })
|
|
129
206
|
.validator(threadSchema)
|
|
130
207
|
.handler(async ({ data }) => {
|
|
131
|
-
|
|
132
|
-
const browseData = getThreadBrowseData(dbPath, data.threadId);
|
|
133
|
-
return getCachedThreadTranscriptPreview(browseData.thread.rollout_path);
|
|
208
|
+
return loadThreadTranscript(data.threadId);
|
|
134
209
|
});
|
|
135
210
|
|
|
136
211
|
export const getAnalyticsFn = createServerFn({ method: 'GET' })
|
|
137
212
|
.validator(analyticsSchema)
|
|
138
213
|
.handler(async ({ data }) => {
|
|
214
|
+
const { getCodexAnalytics } = await import('@spiracha/lib/codex-analytics');
|
|
139
215
|
return getCodexAnalytics({
|
|
140
|
-
dbPath: getDbPath(),
|
|
216
|
+
dbPath: await getDbPath(),
|
|
141
217
|
project: data.project,
|
|
142
218
|
});
|
|
143
219
|
});
|
|
@@ -145,8 +221,9 @@ export const getAnalyticsFn = createServerFn({ method: 'GET' })
|
|
|
145
221
|
export const exportThreadFn = createServerFn({ method: 'POST' })
|
|
146
222
|
.validator(exportSchema)
|
|
147
223
|
.handler(async ({ data }) => {
|
|
224
|
+
const { renderCodexThreadDownload } = await import('@spiracha/lib/codex-browser-export');
|
|
148
225
|
return renderCodexThreadDownload({
|
|
149
|
-
dbPath: getDbPath(),
|
|
226
|
+
dbPath: await getDbPath(),
|
|
150
227
|
includeCommentary: data.includeCommentary,
|
|
151
228
|
includeMetadata: data.includeMetadata,
|
|
152
229
|
includeTools: data.includeTools,
|
|
@@ -163,8 +240,9 @@ export const exportThreadFn = createServerFn({ method: 'POST' })
|
|
|
163
240
|
export const exportThreadsFn = createServerFn({ method: 'POST' })
|
|
164
241
|
.validator(exportThreadsSchema)
|
|
165
242
|
.handler(async ({ data }) => {
|
|
243
|
+
const { renderCodexThreadsDownload } = await import('@spiracha/lib/codex-browser-export');
|
|
166
244
|
return renderCodexThreadsDownload({
|
|
167
|
-
dbPath: getDbPath(),
|
|
245
|
+
dbPath: await getDbPath(),
|
|
168
246
|
includeCommentary: data.includeCommentary,
|
|
169
247
|
includeMetadata: data.includeMetadata,
|
|
170
248
|
includeTools: data.includeTools,
|
|
@@ -181,7 +259,8 @@ export const exportThreadsFn = createServerFn({ method: 'POST' })
|
|
|
181
259
|
export const deleteThreadFn = createServerFn({ method: 'POST' })
|
|
182
260
|
.validator(deleteThreadSchema)
|
|
183
261
|
.handler(async ({ data }) => {
|
|
184
|
-
|
|
262
|
+
const { deleteCodexThread } = await import('@spiracha/lib/codex-browser-db');
|
|
263
|
+
return deleteCodexThread(await getDbPath(), data.threadId, {
|
|
185
264
|
deleteSessionFiles: data.deleteSessionFiles,
|
|
186
265
|
});
|
|
187
266
|
});
|
|
@@ -189,7 +268,8 @@ export const deleteThreadFn = createServerFn({ method: 'POST' })
|
|
|
189
268
|
export const deleteThreadsFn = createServerFn({ method: 'POST' })
|
|
190
269
|
.validator(deleteThreadsSchema)
|
|
191
270
|
.handler(async ({ data }) => {
|
|
192
|
-
|
|
271
|
+
const { deleteCodexThreads } = await import('@spiracha/lib/codex-browser-db');
|
|
272
|
+
return deleteCodexThreads(await getDbPath(), data.threadIds, {
|
|
193
273
|
deleteSessionFiles: data.deleteSessionFiles,
|
|
194
274
|
});
|
|
195
275
|
});
|
|
@@ -197,7 +277,8 @@ export const deleteThreadsFn = createServerFn({ method: 'POST' })
|
|
|
197
277
|
export const deleteProjectFn = createServerFn({ method: 'POST' })
|
|
198
278
|
.validator(deleteProjectSchema)
|
|
199
279
|
.handler(async ({ data }) => {
|
|
200
|
-
|
|
280
|
+
const { deleteCodexProject } = await import('@spiracha/lib/codex-browser-db');
|
|
281
|
+
return deleteCodexProject(await getDbPath(), data.project, {
|
|
201
282
|
deleteSessionFiles: data.deleteSessionFiles,
|
|
202
283
|
});
|
|
203
284
|
});
|
|
@@ -205,5 +286,6 @@ export const deleteProjectFn = createServerFn({ method: 'POST' })
|
|
|
205
286
|
export const recoverProjectThreadsFn = createServerFn({ method: 'POST' })
|
|
206
287
|
.validator(projectSchema)
|
|
207
288
|
.handler(async ({ data }) => {
|
|
208
|
-
|
|
289
|
+
const { recoverCodexProjectThreads } = await import('@spiracha/lib/codex-thread-recovery');
|
|
290
|
+
return recoverCodexProjectThreads(await getDbPath(), data.project);
|
|
209
291
|
});
|
|
@@ -3,6 +3,8 @@ import { mkdtemp, rm } from 'node:fs/promises';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import {
|
|
6
|
+
buildBatchExportBaseName,
|
|
7
|
+
buildConversationExportBaseName,
|
|
6
8
|
getExportMimeType,
|
|
7
9
|
resolveUniqueExportFileBaseName,
|
|
8
10
|
sanitizeExportFileName,
|
|
@@ -72,12 +74,21 @@ const toSafeExportName = (value: string) => {
|
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
const renderCursorZipDownload = async (
|
|
75
|
-
rendered: Array<{ composerId: string; content: string }>,
|
|
77
|
+
rendered: Array<{ composerId: string; content: string; cwd: string | null; updatedAtMs: number | null }>,
|
|
76
78
|
outputFormat: 'md' | 'txt',
|
|
77
79
|
) => {
|
|
78
80
|
const exportDir = await ensureUiExportDir();
|
|
79
81
|
const exportBaseName =
|
|
80
|
-
rendered.length === 1
|
|
82
|
+
rendered.length === 1
|
|
83
|
+
? buildConversationExportBaseName(
|
|
84
|
+
{
|
|
85
|
+
cwd: rendered[0]!.cwd,
|
|
86
|
+
id: rendered[0]!.composerId,
|
|
87
|
+
updatedAtMs: rendered[0]!.updatedAtMs,
|
|
88
|
+
},
|
|
89
|
+
'cursor-thread',
|
|
90
|
+
)
|
|
91
|
+
: buildBatchExportBaseName(rendered, 'cursor');
|
|
81
92
|
const workspaceDir = await mkdtemp(path.join(os.tmpdir(), `${exportBaseName}-`));
|
|
82
93
|
const zipPath = path.join(exportDir, `${exportBaseName}-${randomUUID()}.zip`);
|
|
83
94
|
const usedBaseNames = new Map<string, number>();
|
|
@@ -123,13 +134,23 @@ const renderCursorDownload = async (input: {
|
|
|
123
134
|
outputFormat: 'md' | 'txt';
|
|
124
135
|
zipArchive: boolean;
|
|
125
136
|
}) => {
|
|
137
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
126
138
|
const { readCursorThreadTranscriptWithAgentFiles } = await import('@spiracha/lib/cursor-db');
|
|
127
139
|
const { getCursorGlobalDbPath } = await import('@spiracha/lib/cursor-exporter-types');
|
|
128
140
|
const { renderCursorTranscript } = await import('@spiracha/lib/cursor-transcript');
|
|
129
141
|
const globalDbPath = getCursorGlobalDbPath();
|
|
142
|
+
const { listCursorWorkspaceGroups } = await import('@spiracha/lib/cursor-db');
|
|
143
|
+
const workspaceGroups = await listCursorWorkspaceGroups();
|
|
130
144
|
const rendered = await Promise.all(
|
|
131
145
|
input.composerIds.map(async (composerId) => {
|
|
132
|
-
const transcript = await
|
|
146
|
+
const transcript = await runWithTranscriptLoadLimit(
|
|
147
|
+
() => readCursorThreadTranscriptWithAgentFiles(globalDbPath, composerId),
|
|
148
|
+
{
|
|
149
|
+
id: composerId,
|
|
150
|
+
path: globalDbPath,
|
|
151
|
+
source: 'cursor-ui-export',
|
|
152
|
+
},
|
|
153
|
+
);
|
|
133
154
|
if (!transcript) {
|
|
134
155
|
throw new Error(`No transcript found for thread: ${composerId}`);
|
|
135
156
|
}
|
|
@@ -145,9 +166,16 @@ const renderCursorDownload = async (input: {
|
|
|
145
166
|
throw new Error(`Thread has no exportable content: ${composerId}`);
|
|
146
167
|
}
|
|
147
168
|
|
|
169
|
+
const workspace =
|
|
170
|
+
workspaceGroups.find((candidate) =>
|
|
171
|
+
candidate.buckets.some((bucket) => bucket.threadComposerIds.includes(composerId)),
|
|
172
|
+
) ?? null;
|
|
173
|
+
|
|
148
174
|
return {
|
|
149
175
|
composerId,
|
|
150
176
|
content,
|
|
177
|
+
cwd: workspace?.folders[0] ?? null,
|
|
178
|
+
updatedAtMs: transcript.head.lastUpdatedAtMs,
|
|
151
179
|
};
|
|
152
180
|
}),
|
|
153
181
|
);
|
|
@@ -159,7 +187,14 @@ const renderCursorDownload = async (input: {
|
|
|
159
187
|
if (rendered.length === 1) {
|
|
160
188
|
return {
|
|
161
189
|
content: rendered[0]!.content,
|
|
162
|
-
fileName: `${
|
|
190
|
+
fileName: `${buildConversationExportBaseName(
|
|
191
|
+
{
|
|
192
|
+
cwd: rendered[0]!.cwd,
|
|
193
|
+
id: rendered[0]!.composerId,
|
|
194
|
+
updatedAtMs: rendered[0]!.updatedAtMs,
|
|
195
|
+
},
|
|
196
|
+
'cursor-thread',
|
|
197
|
+
)}.${input.outputFormat}`,
|
|
163
198
|
mimeType: getExportMimeType(input.outputFormat),
|
|
164
199
|
mode: 'download' as const,
|
|
165
200
|
};
|
|
@@ -184,6 +219,7 @@ export const listCursorThreadsFn = createServerFn({ method: 'GET' })
|
|
|
184
219
|
export const getCursorThreadDetailFn = createServerFn({ method: 'GET' })
|
|
185
220
|
.validator(threadSchema)
|
|
186
221
|
.handler(async ({ data }) => {
|
|
222
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
187
223
|
const { readCursorThreadTranscriptWithAgentFiles } = await import('@spiracha/lib/cursor-db');
|
|
188
224
|
const { getCursorGlobalDbPath } = await import('@spiracha/lib/cursor-exporter-types');
|
|
189
225
|
const thread = await findCursorThreadByComposerId(data.composerId);
|
|
@@ -191,7 +227,14 @@ export const getCursorThreadDetailFn = createServerFn({ method: 'GET' })
|
|
|
191
227
|
throw new Error(`Cursor thread not found: ${data.composerId}`);
|
|
192
228
|
}
|
|
193
229
|
|
|
194
|
-
const transcript = await
|
|
230
|
+
const transcript = await runWithTranscriptLoadLimit(
|
|
231
|
+
() => readCursorThreadTranscriptWithAgentFiles(getCursorGlobalDbPath(), data.composerId),
|
|
232
|
+
{
|
|
233
|
+
id: data.composerId,
|
|
234
|
+
path: getCursorGlobalDbPath(),
|
|
235
|
+
source: 'cursor-ui-detail',
|
|
236
|
+
},
|
|
237
|
+
);
|
|
195
238
|
return {
|
|
196
239
|
thread,
|
|
197
240
|
transcript,
|