spiracha 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +27 -12
- package/README.md +37 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +17 -10
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +1 -0
- package/apps/ui/src/components/app-shell.tsx +45 -3
- package/apps/ui/src/components/data-table.tsx +17 -1
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +77 -62
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
- package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
- package/apps/ui/src/components/threads-table.tsx +4 -1
- package/apps/ui/src/lib/antigravity-server.ts +70 -17
- package/apps/ui/src/lib/antigravity-transcript-events.ts +46 -70
- package/apps/ui/src/lib/claude-code-server.ts +14 -4
- package/apps/ui/src/lib/codex-queries.ts +3 -3
- package/apps/ui/src/lib/codex-server.ts +3 -3
- package/apps/ui/src/lib/cursor-server.ts +41 -69
- package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
- package/apps/ui/src/lib/delete-batch.ts +52 -0
- package/apps/ui/src/lib/error-presentation.ts +43 -0
- package/apps/ui/src/lib/export-mutation.ts +20 -0
- package/apps/ui/src/lib/export-options.ts +15 -0
- package/apps/ui/src/lib/grok-server.ts +2 -3
- package/apps/ui/src/lib/kiro-server.ts +14 -2
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +13 -2
- package/apps/ui/src/lib/path-utils.ts +1 -1
- package/apps/ui/src/lib/qoder-queries.ts +15 -0
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-state-reset.tsx +10 -0
- package/apps/ui/src/lib/settings-server.ts +33 -0
- package/apps/ui/src/lib/settings-store.tsx +82 -38
- package/apps/ui/src/lib/settings.ts +65 -0
- package/apps/ui/src/lib/source-session-export-server.ts +1 -1
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +8 -0
- package/apps/ui/src/routeTree.gen.ts +282 -282
- package/apps/ui/src/routes/__root.tsx +12 -15
- package/apps/ui/src/routes/analytics.tsx +10 -61
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +73 -19
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +42 -60
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +25 -11
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +27 -32
- package/apps/ui/src/routes/claude-code.index.tsx +2 -2
- package/apps/ui/src/routes/codex.$project.tsx +33 -31
- package/apps/ui/src/routes/codex.index.tsx +5 -10
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +29 -16
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +33 -27
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +20 -26
- package/apps/ui/src/routes/grok.index.tsx +2 -2
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +27 -32
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +29 -36
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +45 -61
- package/apps/ui/src/routes/opencode.index.tsx +8 -17
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +12 -11
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +19 -25
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +87 -56
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +26 -17
- package/src/client.ts +61 -16
- package/src/lib/antigravity-db.ts +236 -82
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +45 -19
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +2 -7
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +88 -33
- package/src/lib/codex-thread-parser.ts +78 -35
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-renderer.ts +49 -61
- package/src/lib/concurrency.ts +15 -3
- package/src/lib/conversation-api.ts +34 -31
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +54 -29
- package/src/lib/conversation-data/claude-code-adapter.ts +32 -6
- package/src/lib/conversation-data/codex-adapter.ts +91 -80
- package/src/lib/conversation-data/cursor-adapter.ts +48 -34
- package/src/lib/conversation-data/grok-adapter.ts +19 -6
- package/src/lib/conversation-data/index.ts +29 -10
- package/src/lib/conversation-data/kiro-adapter.ts +18 -5
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +43 -15
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +23 -18
- package/src/lib/conversation-zip-export.ts +20 -2
- package/src/lib/cursor-db.ts +34 -23
- package/src/lib/cursor-id.ts +17 -0
- package/src/lib/cursor-recovery.ts +146 -35
- package/src/lib/cursor-transcript-phase.ts +40 -0
- package/src/lib/cursor-transcript.ts +21 -5
- package/src/lib/grok-db.ts +76 -34
- package/src/lib/grok-transcript-phase.ts +11 -2
- package/src/lib/grok-transcript.ts +15 -2
- package/src/lib/kiro-db.ts +161 -68
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/opencode-db.ts +4 -8
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/portable-path.ts +9 -0
- package/src/lib/qoder-acp-client.ts +11 -2
- package/src/lib/qoder-db.ts +51 -17
- package/src/lib/qoder-transcript-phase.ts +64 -0
- package/src/lib/qoder-transcript.ts +30 -2
- package/src/lib/shared.ts +51 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +8 -3
- package/src/lib/ui-cache.ts +152 -17
- package/src/lib/ui-export-archive.ts +14 -55
- package/src/lib/ui-export-files.ts +75 -15
- package/src/lib/ui-export-zip.ts +40 -0
- package/apps/ui/package.json +0 -65
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { AntigravityConversation } from '@spiracha/lib/antigravity-exporter-types';
|
|
1
2
|
import { buildConversationExportBaseName } from '@spiracha/lib/ui-export-archive';
|
|
2
3
|
import { createServerFn } from '@tanstack/react-start';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
import { canExportAntigravityConversation, isAntigravityConversationLocked } from './antigravity-conversation-state';
|
|
6
|
+
import { runDeleteBatch } from './delete-batch';
|
|
5
7
|
|
|
6
8
|
const workspaceSchema = z.object({
|
|
7
9
|
workspaceKey: z.string().min(1),
|
|
@@ -13,10 +15,18 @@ const conversationSchema = z.object({
|
|
|
13
15
|
|
|
14
16
|
const exportSchema = z.object({
|
|
15
17
|
conversationId: z.string().min(1),
|
|
18
|
+
includeCommentary: z.boolean().default(false),
|
|
19
|
+
includeMetadata: z.boolean().default(true),
|
|
20
|
+
includeTools: z.boolean().default(true),
|
|
21
|
+
outputFormat: z.enum(['md', 'txt']).default('md'),
|
|
22
|
+
zipArchive: z.boolean().default(false),
|
|
16
23
|
});
|
|
17
24
|
|
|
18
25
|
const exportConversationsSchema = z.object({
|
|
19
26
|
conversationIds: z.array(z.string().min(1)).min(1),
|
|
27
|
+
includeCommentary: z.boolean().default(false),
|
|
28
|
+
includeMetadata: z.boolean().default(true),
|
|
29
|
+
includeTools: z.boolean().default(true),
|
|
20
30
|
outputFormat: z.enum(['md', 'txt']).default('md'),
|
|
21
31
|
zipArchive: z.boolean().default(true),
|
|
22
32
|
});
|
|
@@ -111,11 +121,22 @@ export const loadAntigravityConversationDetail = async (conversationId: string)
|
|
|
111
121
|
};
|
|
112
122
|
};
|
|
113
123
|
|
|
114
|
-
|
|
124
|
+
type AntigravityConversationExportOptions = {
|
|
125
|
+
includeCommentary?: boolean;
|
|
126
|
+
includeMetadata?: boolean;
|
|
127
|
+
includeTools?: boolean;
|
|
128
|
+
outputFormat?: 'md' | 'txt';
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const loadAntigravityConversationExport = async (
|
|
132
|
+
conversationId: string,
|
|
133
|
+
options: AntigravityConversationExportOptions = {},
|
|
134
|
+
loadedConversation?: AntigravityConversation,
|
|
135
|
+
) => {
|
|
115
136
|
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
116
137
|
const { renderAntigravityConversationMarkdown } = await import('@spiracha/lib/antigravity-db');
|
|
117
138
|
const { getCachedAntigravityKeychainSecret } = await import('@spiracha/lib/antigravity-keychain');
|
|
118
|
-
const conversation = await findAntigravityConversationById(conversationId);
|
|
139
|
+
const conversation = loadedConversation ?? (await findAntigravityConversationById(conversationId));
|
|
119
140
|
const keychainSecret = getCachedAntigravityKeychainSecret();
|
|
120
141
|
const hasKeychainSecret = Boolean(keychainSecret);
|
|
121
142
|
|
|
@@ -130,7 +151,11 @@ export const loadAntigravityConversationExport = async (conversationId: string)
|
|
|
130
151
|
const content = await runWithTranscriptLoadLimit(
|
|
131
152
|
() =>
|
|
132
153
|
renderAntigravityConversationMarkdown(conversation, {
|
|
154
|
+
includeCommentary: options.includeCommentary,
|
|
155
|
+
includeMetadata: options.includeMetadata,
|
|
156
|
+
includeTools: options.includeTools,
|
|
133
157
|
keychainSecret,
|
|
158
|
+
outputFormat: options.outputFormat,
|
|
134
159
|
}),
|
|
135
160
|
{
|
|
136
161
|
id: conversation.conversationId,
|
|
@@ -152,7 +177,7 @@ export const loadAntigravityConversationExport = async (conversationId: string)
|
|
|
152
177
|
updatedAtMs: conversation.lastUpdatedAtMs ?? conversation.conversationMtimeMs,
|
|
153
178
|
},
|
|
154
179
|
'antigravity-conversation',
|
|
155
|
-
)}.md`,
|
|
180
|
+
)}.${options.outputFormat ?? 'md'}`,
|
|
156
181
|
};
|
|
157
182
|
};
|
|
158
183
|
|
|
@@ -171,8 +196,8 @@ export const deleteAntigravityConversationsById = async (conversationIds: string
|
|
|
171
196
|
const { deleteAntigravityConversation } = await import('@spiracha/lib/antigravity-db');
|
|
172
197
|
const { resolveAntigravityRoots } = await import('@spiracha/lib/antigravity-exporter-types');
|
|
173
198
|
const roots = resolveAntigravityRoots();
|
|
174
|
-
const results = await
|
|
175
|
-
|
|
199
|
+
const results = await runDeleteBatch(conversationIds, (conversationId) =>
|
|
200
|
+
deleteAntigravityConversation(roots, conversationId),
|
|
176
201
|
);
|
|
177
202
|
const deletedConversationIds = results.flatMap((result) => result.deletedConversationIds);
|
|
178
203
|
if (deletedConversationIds.length === 0) {
|
|
@@ -192,7 +217,7 @@ export const getAntigravityConversationDetailFn = createServerFn({ method: 'GET'
|
|
|
192
217
|
});
|
|
193
218
|
|
|
194
219
|
export const exportAntigravityArtifactsFn = createServerFn({ method: 'POST' })
|
|
195
|
-
.validator(
|
|
220
|
+
.validator(conversationSchema)
|
|
196
221
|
.handler(async ({ data }) => {
|
|
197
222
|
const { renderAntigravityArtifactsMarkdown } = await import('@spiracha/lib/antigravity-db');
|
|
198
223
|
const conversation = await findAntigravityConversationById(data.conversationId);
|
|
@@ -211,22 +236,50 @@ export const exportAntigravityArtifactsFn = createServerFn({ method: 'POST' })
|
|
|
211
236
|
export const exportAntigravityConversationFn = createServerFn({ method: 'POST' })
|
|
212
237
|
.validator(exportSchema)
|
|
213
238
|
.handler(async ({ data }) => {
|
|
214
|
-
return
|
|
239
|
+
return exportAntigravityConversations({
|
|
240
|
+
conversationIds: [data.conversationId],
|
|
241
|
+
includeCommentary: data.includeCommentary,
|
|
242
|
+
includeMetadata: data.includeMetadata,
|
|
243
|
+
includeTools: data.includeTools,
|
|
244
|
+
outputFormat: data.outputFormat,
|
|
245
|
+
zipArchive: data.zipArchive,
|
|
246
|
+
});
|
|
215
247
|
});
|
|
216
248
|
|
|
217
|
-
export const exportAntigravityConversations = async (
|
|
249
|
+
export const exportAntigravityConversations = async (input: z.input<typeof exportConversationsSchema>) => {
|
|
250
|
+
const data = exportConversationsSchema.parse(input);
|
|
218
251
|
const { renderSourceSessionsDownload } = await import('./source-session-export-server');
|
|
219
|
-
const {
|
|
220
|
-
const
|
|
221
|
-
const
|
|
252
|
+
const { listAntigravityConversations } = await import('@spiracha/lib/antigravity-db');
|
|
253
|
+
const { resolveAntigravityProjectNames } = await import('@spiracha/lib/antigravity-projects');
|
|
254
|
+
const conversations = await listAntigravityConversations();
|
|
255
|
+
const conversationsById = new Map(conversations.map((conversation) => [conversation.conversationId, conversation]));
|
|
256
|
+
const selectedConversations = data.conversationIds.map((conversationId) => {
|
|
257
|
+
const conversation = conversationsById.get(conversationId);
|
|
258
|
+
if (!conversation) {
|
|
259
|
+
throw new Error(`Antigravity conversation not found: ${conversationId}`);
|
|
260
|
+
}
|
|
261
|
+
return conversation;
|
|
262
|
+
});
|
|
263
|
+
const projectNames = await resolveAntigravityProjectNames(
|
|
264
|
+
selectedConversations.flatMap((conversation) => (conversation.projectId ? [conversation.projectId] : [])),
|
|
265
|
+
);
|
|
222
266
|
const entries = await Promise.all(
|
|
223
|
-
|
|
224
|
-
const
|
|
267
|
+
selectedConversations.map(async (loadedConversation) => {
|
|
268
|
+
const conversationId = loadedConversation.conversationId;
|
|
269
|
+
const result = await loadAntigravityConversationExport(
|
|
270
|
+
conversationId,
|
|
271
|
+
{
|
|
272
|
+
includeCommentary: data.includeCommentary,
|
|
273
|
+
includeMetadata: data.includeMetadata,
|
|
274
|
+
includeTools: data.includeTools,
|
|
275
|
+
outputFormat: data.outputFormat,
|
|
276
|
+
},
|
|
277
|
+
loadedConversation,
|
|
278
|
+
);
|
|
225
279
|
const conversation = result.conversation;
|
|
226
|
-
const
|
|
227
|
-
?
|
|
228
|
-
|
|
229
|
-
const exportCwd = projectGroup?.label ?? conversation.workspaceFolder;
|
|
280
|
+
const exportCwd =
|
|
281
|
+
(conversation.projectId ? projectNames.get(conversation.projectId) : null) ??
|
|
282
|
+
conversation.workspaceFolder;
|
|
230
283
|
const updatedAtMs = conversation.lastUpdatedAtMs ?? conversation.conversationMtimeMs;
|
|
231
284
|
return {
|
|
232
285
|
content: result.content,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { getFinalAntigravityAssistantSequences } from '@spiracha/lib/antigravity-transcript-phase';
|
|
1
2
|
import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-types';
|
|
2
3
|
import type { JsonValue } from '@spiracha/lib/shared';
|
|
4
|
+
import { getThreadTranscriptStats } from './thread-transcript-stats';
|
|
3
5
|
|
|
4
6
|
type MarkdownSection = {
|
|
5
7
|
body: string;
|
|
@@ -23,6 +25,18 @@ const TIMESTAMP_PATTERN = /^_Timestamp:\s*(.+?)_$/u;
|
|
|
23
25
|
const TOOL_HEADING_PATTERN = /^tool:\s*(.+)$/iu;
|
|
24
26
|
const CONTROL_SUBHEADING_PATTERN = /^###\s+(Thinking|Tool Calls)\s*$/iu;
|
|
25
27
|
|
|
28
|
+
type MarkdownFence = { character: string; length: number };
|
|
29
|
+
|
|
30
|
+
const updateMarkdownFence = (fence: MarkdownFence | null, token: string | null): MarkdownFence | null => {
|
|
31
|
+
if (!token) {
|
|
32
|
+
return fence;
|
|
33
|
+
}
|
|
34
|
+
if (!fence) {
|
|
35
|
+
return { character: token[0]!, length: token.length };
|
|
36
|
+
}
|
|
37
|
+
return token[0] === fence.character && token.length >= fence.length ? null : fence;
|
|
38
|
+
};
|
|
39
|
+
|
|
26
40
|
const splitMarkdownSections = (markdown: string | null): MarkdownSection[] => {
|
|
27
41
|
if (!markdown?.trim()) {
|
|
28
42
|
return [];
|
|
@@ -32,6 +46,7 @@ const splitMarkdownSections = (markdown: string | null): MarkdownSection[] => {
|
|
|
32
46
|
const lines = markdown.split(/\r?\n/u);
|
|
33
47
|
let currentHeading: string | null = null;
|
|
34
48
|
let currentLines: string[] = [];
|
|
49
|
+
let fence: MarkdownFence | null = null;
|
|
35
50
|
|
|
36
51
|
const flush = () => {
|
|
37
52
|
if (!currentHeading) {
|
|
@@ -41,12 +56,13 @@ const splitMarkdownSections = (markdown: string | null): MarkdownSection[] => {
|
|
|
41
56
|
sections.push({
|
|
42
57
|
body: currentLines.join('\n').trim(),
|
|
43
58
|
heading: currentHeading,
|
|
44
|
-
sequence: sections.length *
|
|
59
|
+
sequence: sections.length * 1000,
|
|
45
60
|
});
|
|
46
61
|
};
|
|
47
62
|
|
|
48
63
|
for (const line of lines) {
|
|
49
|
-
const
|
|
64
|
+
const fenceToken = /^\s*(`{3,}|~{3,})/u.exec(line)?.[1] ?? null;
|
|
65
|
+
const heading = fence === null && fenceToken === null ? HEADING_PATTERN.exec(line) : null;
|
|
50
66
|
if (heading) {
|
|
51
67
|
flush();
|
|
52
68
|
currentHeading = heading[1]!.trim();
|
|
@@ -57,6 +73,8 @@ const splitMarkdownSections = (markdown: string | null): MarkdownSection[] => {
|
|
|
57
73
|
if (currentHeading) {
|
|
58
74
|
currentLines.push(line);
|
|
59
75
|
}
|
|
76
|
+
|
|
77
|
+
fence = updateMarkdownFence(fence, fenceToken);
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
flush();
|
|
@@ -195,38 +213,37 @@ const buildToolOutputEvent = (
|
|
|
195
213
|
});
|
|
196
214
|
|
|
197
215
|
const getFinalAssistantSectionSequences = (sections: MarkdownSection[]): Set<number> => {
|
|
198
|
-
const
|
|
199
|
-
let currentAssistantContentSequence: number | null = null;
|
|
200
|
-
|
|
201
|
-
const flushAssistantRun = () => {
|
|
202
|
-
if (currentAssistantContentSequence !== null) {
|
|
203
|
-
finalAssistantSectionSequences.add(currentAssistantContentSequence);
|
|
204
|
-
}
|
|
205
|
-
currentAssistantContentSequence = null;
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
for (const section of sections) {
|
|
216
|
+
const items = sections.map((section) => {
|
|
209
217
|
const heading = section.heading.toLowerCase();
|
|
210
218
|
if (heading === 'user') {
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
return {
|
|
220
|
+
hasContent: Boolean(section.body),
|
|
221
|
+
hasToolCalls: false,
|
|
222
|
+
role: 'user' as const,
|
|
223
|
+
sequence: section.sequence,
|
|
224
|
+
};
|
|
213
225
|
}
|
|
214
226
|
|
|
215
|
-
if (heading
|
|
216
|
-
|
|
227
|
+
if (heading === 'assistant') {
|
|
228
|
+
const { body } = extractTimestamp(section.body);
|
|
229
|
+
const parsed = parseAssistantSection(body);
|
|
230
|
+
return {
|
|
231
|
+
hasContent: Boolean(parsed.content),
|
|
232
|
+
hasToolCalls: parsed.toolCalls.length > 0,
|
|
233
|
+
role: 'assistant' as const,
|
|
234
|
+
sequence: section.sequence,
|
|
235
|
+
};
|
|
217
236
|
}
|
|
218
237
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
238
|
+
return {
|
|
239
|
+
hasContent: Boolean(section.body),
|
|
240
|
+
hasToolCalls: false,
|
|
241
|
+
role: 'other' as const,
|
|
242
|
+
sequence: section.sequence,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
227
245
|
|
|
228
|
-
|
|
229
|
-
return finalAssistantSectionSequences;
|
|
246
|
+
return getFinalAntigravityAssistantSequences(items);
|
|
230
247
|
};
|
|
231
248
|
|
|
232
249
|
const textSectionToEvents = (
|
|
@@ -298,46 +315,5 @@ export const antigravityMarkdownToThreadEvents = (markdown: string | null): Thre
|
|
|
298
315
|
return sections.flatMap((section) => sectionToEvents(section, finalAssistantSectionSequences));
|
|
299
316
|
};
|
|
300
317
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
if (event.role === 'assistant') {
|
|
304
|
-
stats.assistantMessageCount += 1;
|
|
305
|
-
}
|
|
306
|
-
if (event.role === 'user') {
|
|
307
|
-
stats.userMessageCount += 1;
|
|
308
|
-
}
|
|
309
|
-
if (event.phase === 'commentary') {
|
|
310
|
-
stats.commentaryCount += 1;
|
|
311
|
-
}
|
|
312
|
-
if (event.phase === 'final_answer') {
|
|
313
|
-
stats.finalAnswerCount += 1;
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
export const getAntigravityThreadTranscriptStats = (events: ThreadEvent[]): ThreadTranscriptStats => {
|
|
318
|
-
const stats: ThreadTranscriptStats = {
|
|
319
|
-
assistantMessageCount: 0,
|
|
320
|
-
commentaryCount: 0,
|
|
321
|
-
execCommandCount: 0,
|
|
322
|
-
finalAnswerCount: 0,
|
|
323
|
-
messageCount: 0,
|
|
324
|
-
toolCallCount: 0,
|
|
325
|
-
toolOutputCount: 0,
|
|
326
|
-
userMessageCount: 0,
|
|
327
|
-
webSearchEventCount: 0,
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
for (const event of events) {
|
|
331
|
-
if (event.kind === 'message') {
|
|
332
|
-
updateMessageStats(stats, event);
|
|
333
|
-
}
|
|
334
|
-
if (event.kind === 'tool_call') {
|
|
335
|
-
stats.toolCallCount += 1;
|
|
336
|
-
}
|
|
337
|
-
if (event.kind === 'tool_output') {
|
|
338
|
-
stats.toolOutputCount += 1;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return stats;
|
|
343
|
-
};
|
|
318
|
+
export const getAntigravityThreadTranscriptStats = (events: ThreadEvent[]): ThreadTranscriptStats =>
|
|
319
|
+
getThreadTranscriptStats(events);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServerFn } from '@tanstack/react-start';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { requireDeletedItems, runDeleteBatch } from './delete-batch';
|
|
3
4
|
import { renderSourceSessionDownload, renderSourceSessionsDownload } from './source-session-export-server';
|
|
4
5
|
|
|
5
6
|
const LARGE_CLAUDE_CODE_SESSION_SIZE_BYTES = 8 * 1024 * 1024;
|
|
@@ -56,7 +57,9 @@ const loadClaudeCodeSessionTranscript = async (sessionId: string) => {
|
|
|
56
57
|
const projectsDir = resolveClaudeCodeProjectsDir();
|
|
57
58
|
return runWithTranscriptLoadLimit(
|
|
58
59
|
async () => {
|
|
59
|
-
const transcript = await readClaudeCodeSessionTranscript(projectsDir, sessionId
|
|
60
|
+
const transcript = await readClaudeCodeSessionTranscript(projectsDir, sessionId, {
|
|
61
|
+
includeRawPayloads: false,
|
|
62
|
+
});
|
|
60
63
|
if (!transcript) {
|
|
61
64
|
throw new Error(`Claude Code session not found: ${sessionId}`);
|
|
62
65
|
}
|
|
@@ -225,7 +228,9 @@ export const deleteClaudeCodeSessionFn = createServerFn({ method: 'POST' })
|
|
|
225
228
|
.validator(sessionSchema)
|
|
226
229
|
.handler(async ({ data }) => {
|
|
227
230
|
const { deleteClaudeCodeSession, resolveClaudeCodeProjectsDir } = await import('@spiracha/lib/claude-code-db');
|
|
228
|
-
|
|
231
|
+
const result = await deleteClaudeCodeSession(resolveClaudeCodeProjectsDir(), data.sessionId);
|
|
232
|
+
requireDeletedItems(result.deletedSessionIds, 'Claude Code session', data.sessionId);
|
|
233
|
+
return result;
|
|
229
234
|
});
|
|
230
235
|
|
|
231
236
|
export const deleteClaudeCodeSessionsFn = createServerFn({ method: 'POST' })
|
|
@@ -233,8 +238,13 @@ export const deleteClaudeCodeSessionsFn = createServerFn({ method: 'POST' })
|
|
|
233
238
|
.handler(async ({ data }) => {
|
|
234
239
|
const { deleteClaudeCodeSession, resolveClaudeCodeProjectsDir } = await import('@spiracha/lib/claude-code-db');
|
|
235
240
|
const projectsDir = resolveClaudeCodeProjectsDir();
|
|
236
|
-
const results = await
|
|
237
|
-
|
|
241
|
+
const results = await runDeleteBatch(data.sessionIds, (sessionId) =>
|
|
242
|
+
deleteClaudeCodeSession(projectsDir, sessionId),
|
|
243
|
+
);
|
|
244
|
+
requireDeletedItems(
|
|
245
|
+
results.flatMap((result) => result.deletedSessionIds),
|
|
246
|
+
'Claude Code sessions',
|
|
247
|
+
'batch',
|
|
238
248
|
);
|
|
239
249
|
return {
|
|
240
250
|
deletedFiles: [...new Set(results.flatMap((result) => result.deletedFiles))],
|
|
@@ -49,10 +49,10 @@ export const projectThreadsQueryOptions = (project: string) =>
|
|
|
49
49
|
retryDelay,
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
export const threadSnapshotQueryOptions = (threadId: string
|
|
52
|
+
export const threadSnapshotQueryOptions = (threadId: string) =>
|
|
53
53
|
queryOptions({
|
|
54
|
-
queryFn: () => getThreadSnapshotFn({ data: {
|
|
55
|
-
queryKey: ['thread', threadId
|
|
54
|
+
queryFn: () => getThreadSnapshotFn({ data: { threadId } }),
|
|
55
|
+
queryKey: ['thread', threadId],
|
|
56
56
|
retry: retrySqliteQuery,
|
|
57
57
|
retryDelay,
|
|
58
58
|
});
|
|
@@ -22,7 +22,7 @@ const transcriptFiltersSchema = z.object({
|
|
|
22
22
|
showUserMessages: z.boolean(),
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const threadPreviewSchema = z.object({
|
|
26
26
|
filters: transcriptFiltersSchema.optional(),
|
|
27
27
|
threadId: z.string().min(1),
|
|
28
28
|
});
|
|
@@ -101,7 +101,7 @@ export const listProjectThreadsFn = createServerFn({ method: 'GET' })
|
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
export const getThreadSnapshotFn = createServerFn({ method: 'GET' })
|
|
104
|
-
.validator(
|
|
104
|
+
.validator(threadSchema)
|
|
105
105
|
.handler(async ({ data }) => {
|
|
106
106
|
const startedAt = Date.now();
|
|
107
107
|
const [{ getThreadBrowseData }, { getThreadRolloutLoadState }] = await Promise.all([
|
|
@@ -197,7 +197,7 @@ export const loadThreadTranscript = async (threadId: string) => {
|
|
|
197
197
|
};
|
|
198
198
|
|
|
199
199
|
export const getThreadTranscriptPreviewFn = createServerFn({ method: 'GET' })
|
|
200
|
-
.validator(
|
|
200
|
+
.validator(threadPreviewSchema)
|
|
201
201
|
.handler(async ({ data }) => {
|
|
202
202
|
return loadThreadTranscriptPreview(data.threadId, data.filters);
|
|
203
203
|
});
|
|
@@ -1,25 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
buildBatchExportBaseName,
|
|
7
|
-
buildConversationExportBaseName,
|
|
8
|
-
getExportMimeType,
|
|
9
|
-
resolveUniqueExportFileBaseName,
|
|
10
|
-
sanitizeExportFileName,
|
|
11
|
-
zipExportDirectory,
|
|
12
|
-
} from '@spiracha/lib/ui-export-archive';
|
|
13
|
-
import { buildUiExportDownloadUrl, ensureUiExportDir } from '@spiracha/lib/ui-export-files';
|
|
1
|
+
import { isSafeCursorComposerId } from '@spiracha/lib/cursor-id';
|
|
14
2
|
import { createServerFn } from '@tanstack/react-start';
|
|
15
3
|
import { z } from 'zod';
|
|
4
|
+
import { renderSourceSessionDownload, renderSourceSessionsDownload } from './source-session-export-server';
|
|
16
5
|
|
|
17
6
|
const workspaceSchema = z.object({
|
|
18
7
|
workspaceKey: z.string().min(1),
|
|
19
8
|
});
|
|
20
9
|
|
|
10
|
+
const composerIdSchema = z.string().refine(isSafeCursorComposerId, {
|
|
11
|
+
message: 'Invalid Cursor composer id.',
|
|
12
|
+
});
|
|
13
|
+
|
|
21
14
|
const threadSchema = z.object({
|
|
22
|
-
composerId:
|
|
15
|
+
composerId: composerIdSchema,
|
|
23
16
|
});
|
|
24
17
|
|
|
25
18
|
const recoverSchema = z.object({
|
|
@@ -28,7 +21,7 @@ const recoverSchema = z.object({
|
|
|
28
21
|
});
|
|
29
22
|
|
|
30
23
|
const exportSchema = z.object({
|
|
31
|
-
composerId:
|
|
24
|
+
composerId: composerIdSchema,
|
|
32
25
|
includeCommentary: z.boolean().default(true),
|
|
33
26
|
includeMetadata: z.boolean().default(true),
|
|
34
27
|
includeTools: z.boolean().default(true),
|
|
@@ -37,7 +30,7 @@ const exportSchema = z.object({
|
|
|
37
30
|
});
|
|
38
31
|
|
|
39
32
|
const exportThreadsSchema = z.object({
|
|
40
|
-
composerIds: z.array(
|
|
33
|
+
composerIds: z.array(composerIdSchema).min(1),
|
|
41
34
|
includeCommentary: z.boolean().default(true),
|
|
42
35
|
includeMetadata: z.boolean().default(true),
|
|
43
36
|
includeTools: z.boolean().default(true),
|
|
@@ -46,7 +39,7 @@ const exportThreadsSchema = z.object({
|
|
|
46
39
|
});
|
|
47
40
|
|
|
48
41
|
const deleteThreadsSchema = z.object({
|
|
49
|
-
composerIds: z.array(
|
|
42
|
+
composerIds: z.array(composerIdSchema).min(1),
|
|
50
43
|
});
|
|
51
44
|
|
|
52
45
|
const ensureCursorClosedForWrite = async () => {
|
|
@@ -69,48 +62,29 @@ const findGroupByKey = async (workspaceKey: string) => {
|
|
|
69
62
|
return group;
|
|
70
63
|
};
|
|
71
64
|
|
|
72
|
-
const toSafeExportName = (value: string) => {
|
|
73
|
-
return sanitizeExportFileName(value) || 'cursor-thread';
|
|
74
|
-
};
|
|
75
|
-
|
|
76
65
|
const renderCursorZipDownload = async (
|
|
77
|
-
rendered: Array<{
|
|
66
|
+
rendered: Array<{
|
|
67
|
+
composerId: string;
|
|
68
|
+
content: string;
|
|
69
|
+
cwd: string | null;
|
|
70
|
+
fileBaseName: string;
|
|
71
|
+
updatedAtMs: number | null;
|
|
72
|
+
}>,
|
|
78
73
|
outputFormat: 'md' | 'txt',
|
|
79
74
|
) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const zipPath = path.join(exportDir, `${exportBaseName}-${randomUUID()}.zip`);
|
|
94
|
-
const usedBaseNames = new Map<string, number>();
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
for (const entry of rendered) {
|
|
98
|
-
const baseName = toSafeExportName(entry.composerId);
|
|
99
|
-
const fileBaseName = resolveUniqueExportFileBaseName(baseName, usedBaseNames);
|
|
100
|
-
await Bun.write(path.join(workspaceDir, `${fileBaseName}.${outputFormat}`), entry.content);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
await zipExportDirectory(workspaceDir, zipPath);
|
|
104
|
-
} finally {
|
|
105
|
-
await rm(workspaceDir, { force: true, recursive: true });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
downloadUrl: buildUiExportDownloadUrl(zipPath),
|
|
110
|
-
fileName: `${exportBaseName}.zip`,
|
|
111
|
-
mimeType: 'application/zip',
|
|
112
|
-
mode: 'download_url' as const,
|
|
113
|
-
};
|
|
75
|
+
return renderSourceSessionsDownload({
|
|
76
|
+
entries: rendered.map((entry) => ({
|
|
77
|
+
content: entry.content,
|
|
78
|
+
cwd: entry.cwd,
|
|
79
|
+
fallbackBaseName: 'cursor-thread',
|
|
80
|
+
fileBaseName: entry.fileBaseName,
|
|
81
|
+
sessionId: entry.composerId,
|
|
82
|
+
updatedAtMs: entry.updatedAtMs,
|
|
83
|
+
})),
|
|
84
|
+
fallbackBaseName: 'cursor',
|
|
85
|
+
outputFormat,
|
|
86
|
+
zipArchive: true,
|
|
87
|
+
});
|
|
114
88
|
};
|
|
115
89
|
|
|
116
90
|
export const findCursorThreadByComposerId = async (composerId: string) => {
|
|
@@ -175,6 +149,7 @@ const renderCursorDownload = async (input: {
|
|
|
175
149
|
composerId,
|
|
176
150
|
content,
|
|
177
151
|
cwd: workspace?.folders[0] ?? null,
|
|
152
|
+
fileBaseName: transcript.head.name || composerId,
|
|
178
153
|
updatedAtMs: transcript.head.lastUpdatedAtMs,
|
|
179
154
|
};
|
|
180
155
|
}),
|
|
@@ -185,19 +160,16 @@ const renderCursorDownload = async (input: {
|
|
|
185
160
|
}
|
|
186
161
|
|
|
187
162
|
if (rendered.length === 1) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
mimeType: getExportMimeType(input.outputFormat),
|
|
199
|
-
mode: 'download' as const,
|
|
200
|
-
};
|
|
163
|
+
const entry = rendered[0]!;
|
|
164
|
+
return renderSourceSessionDownload({
|
|
165
|
+
content: entry.content,
|
|
166
|
+
cwd: entry.cwd,
|
|
167
|
+
fallbackBaseName: 'cursor-thread',
|
|
168
|
+
outputFormat: input.outputFormat,
|
|
169
|
+
sessionId: entry.composerId,
|
|
170
|
+
updatedAtMs: entry.updatedAtMs,
|
|
171
|
+
zipArchive: false,
|
|
172
|
+
});
|
|
201
173
|
}
|
|
202
174
|
|
|
203
175
|
throw new Error('No Cursor threads selected for export');
|