spiracha 2.0.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 +42 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +36 -11
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +46 -3
- 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 +23 -5
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +79 -74
- 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/projects-table.tsx +4 -1
- 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/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -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 +13 -47
- 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 +209 -11
- package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +185 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +16 -0
- package/apps/ui/src/lib/codex-server.ts +134 -52
- package/apps/ui/src/lib/cursor-server.ts +68 -53
- 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/formatters.ts +3 -5
- package/apps/ui/src/lib/grok-queries.ts +22 -0
- package/apps/ui/src/lib/grok-server.ts +168 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +97 -7
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +96 -7
- 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-server.ts +67 -11
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-search.ts +114 -0
- 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 +86 -3
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
- package/apps/ui/src/routeTree.gen.ts +341 -234
- 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 +149 -29
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- 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 +168 -28
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
- 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 +31 -18
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
- package/apps/ui/src/routes/opencode.index.tsx +6 -2
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +37 -27
- package/src/client.ts +183 -5
- package/src/lib/antigravity-db.ts +424 -94
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +534 -42
- 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-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +18 -27
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +106 -23
- package/src/lib/codex-thread-parser.ts +149 -55
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +87 -79
- package/src/lib/concurrency.ts +56 -3
- package/src/lib/conversation-api.ts +410 -27
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
- package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
- package/src/lib/conversation-data/codex-adapter.ts +108 -73
- package/src/lib/conversation-data/cursor-adapter.ts +77 -29
- package/src/lib/conversation-data/grok-adapter.ts +223 -0
- package/src/lib/conversation-data/index.ts +105 -11
- package/src/lib/conversation-data/kiro-adapter.ts +59 -12
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +67 -17
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +67 -37
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +75 -0
- package/src/lib/cursor-db.ts +68 -28
- 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 +1068 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +61 -0
- package/src/lib/grok-transcript.ts +167 -0
- package/src/lib/kiro-db.ts +187 -43
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +599 -60
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/opencode-transcript.ts +8 -5
- 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 +63 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +87 -0
- package/src/lib/ui-cache.ts +185 -24
- package/src/lib/ui-export-archive.ts +58 -39
- 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,6 +1,9 @@
|
|
|
1
|
+
import type { AntigravityConversation } from '@spiracha/lib/antigravity-exporter-types';
|
|
2
|
+
import { buildConversationExportBaseName } from '@spiracha/lib/ui-export-archive';
|
|
1
3
|
import { createServerFn } from '@tanstack/react-start';
|
|
2
4
|
import { z } from 'zod';
|
|
3
5
|
import { canExportAntigravityConversation, isAntigravityConversationLocked } from './antigravity-conversation-state';
|
|
6
|
+
import { runDeleteBatch } from './delete-batch';
|
|
4
7
|
|
|
5
8
|
const workspaceSchema = z.object({
|
|
6
9
|
workspaceKey: z.string().min(1),
|
|
@@ -12,6 +15,24 @@ const conversationSchema = z.object({
|
|
|
12
15
|
|
|
13
16
|
const exportSchema = z.object({
|
|
14
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),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const exportConversationsSchema = z.object({
|
|
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),
|
|
30
|
+
outputFormat: z.enum(['md', 'txt']).default('md'),
|
|
31
|
+
zipArchive: z.boolean().default(true),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const deleteConversationsSchema = z.object({
|
|
35
|
+
conversationIds: z.array(z.string().min(1)).min(1),
|
|
15
36
|
});
|
|
16
37
|
|
|
17
38
|
export const listAntigravityWorkspacesFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
@@ -48,7 +69,24 @@ const findAntigravityConversationById = async (conversationId: string) => {
|
|
|
48
69
|
return conversation;
|
|
49
70
|
};
|
|
50
71
|
|
|
72
|
+
const resolveAntigravityConversationGroup = async (
|
|
73
|
+
conversation: Awaited<ReturnType<typeof findAntigravityConversationById>>,
|
|
74
|
+
) => {
|
|
75
|
+
const fallback = {
|
|
76
|
+
key: conversation.projectId ? `project:${conversation.projectId}` : conversation.workspaceKey,
|
|
77
|
+
label: conversation.workspaceLabel,
|
|
78
|
+
};
|
|
79
|
+
if (!conversation.projectId) {
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { listAntigravityWorkspaceGroups } = await import('@spiracha/lib/antigravity-db');
|
|
84
|
+
const group = (await listAntigravityWorkspaceGroups()).find((candidate) => candidate.key === fallback.key);
|
|
85
|
+
return group ? { key: group.key, label: group.label } : fallback;
|
|
86
|
+
};
|
|
87
|
+
|
|
51
88
|
export const loadAntigravityConversationDetail = async (conversationId: string) => {
|
|
89
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
52
90
|
const { renderAntigravityArtifactsMarkdown, renderAntigravityConversationMarkdown } = await import(
|
|
53
91
|
'@spiracha/lib/antigravity-db'
|
|
54
92
|
);
|
|
@@ -57,25 +95,48 @@ export const loadAntigravityConversationDetail = async (conversationId: string)
|
|
|
57
95
|
const keychainSecret = getCachedAntigravityKeychainSecret();
|
|
58
96
|
const hasKeychainSecret = Boolean(keychainSecret);
|
|
59
97
|
const transcriptLocked = isAntigravityConversationLocked(conversation, hasKeychainSecret);
|
|
60
|
-
const conversationMarkdown =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
98
|
+
const [[conversationMarkdown, artifactsMarkdown], conversationGroup] = await Promise.all([
|
|
99
|
+
runWithTranscriptLoadLimit(
|
|
100
|
+
async () =>
|
|
101
|
+
Promise.all([
|
|
102
|
+
transcriptLocked ? null : renderAntigravityConversationMarkdown(conversation, { keychainSecret }),
|
|
103
|
+
conversation.artifactCount > 0 ? renderAntigravityArtifactsMarkdown(conversation) : null,
|
|
104
|
+
]),
|
|
105
|
+
{
|
|
106
|
+
id: conversation.conversationId,
|
|
107
|
+
path: conversation.transcriptPath ?? conversation.conversationPath ?? undefined,
|
|
108
|
+
source: 'antigravity-ui-detail',
|
|
109
|
+
},
|
|
110
|
+
),
|
|
111
|
+
resolveAntigravityConversationGroup(conversation),
|
|
112
|
+
]);
|
|
65
113
|
|
|
66
114
|
return {
|
|
67
115
|
artifactsMarkdown,
|
|
68
116
|
conversation,
|
|
117
|
+
conversationGroup,
|
|
69
118
|
// Suppress the duplicate panel when artifactsMarkdown and conversationMarkdown are identical.
|
|
70
119
|
conversationMarkdown: conversationMarkdown === artifactsMarkdown ? null : conversationMarkdown,
|
|
71
120
|
transcriptLocked,
|
|
72
121
|
};
|
|
73
122
|
};
|
|
74
123
|
|
|
75
|
-
|
|
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
|
+
) => {
|
|
136
|
+
const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
|
|
76
137
|
const { renderAntigravityConversationMarkdown } = await import('@spiracha/lib/antigravity-db');
|
|
77
138
|
const { getCachedAntigravityKeychainSecret } = await import('@spiracha/lib/antigravity-keychain');
|
|
78
|
-
const conversation = await findAntigravityConversationById(conversationId);
|
|
139
|
+
const conversation = loadedConversation ?? (await findAntigravityConversationById(conversationId));
|
|
79
140
|
const keychainSecret = getCachedAntigravityKeychainSecret();
|
|
80
141
|
const hasKeychainSecret = Boolean(keychainSecret);
|
|
81
142
|
|
|
@@ -87,14 +148,65 @@ export const loadAntigravityConversationExport = async (conversationId: string)
|
|
|
87
148
|
throw new Error(`No exportable Antigravity transcript found for conversation: ${conversationId}`);
|
|
88
149
|
}
|
|
89
150
|
|
|
90
|
-
const content = await
|
|
151
|
+
const content = await runWithTranscriptLoadLimit(
|
|
152
|
+
() =>
|
|
153
|
+
renderAntigravityConversationMarkdown(conversation, {
|
|
154
|
+
includeCommentary: options.includeCommentary,
|
|
155
|
+
includeMetadata: options.includeMetadata,
|
|
156
|
+
includeTools: options.includeTools,
|
|
157
|
+
keychainSecret,
|
|
158
|
+
outputFormat: options.outputFormat,
|
|
159
|
+
}),
|
|
160
|
+
{
|
|
161
|
+
id: conversation.conversationId,
|
|
162
|
+
path: conversation.transcriptPath ?? conversation.conversationPath ?? undefined,
|
|
163
|
+
source: 'antigravity-ui-export',
|
|
164
|
+
},
|
|
165
|
+
);
|
|
91
166
|
if (!content) {
|
|
92
167
|
throw new Error(`No exportable Antigravity transcript found for conversation: ${conversationId}`);
|
|
93
168
|
}
|
|
94
169
|
|
|
95
170
|
return {
|
|
96
171
|
content,
|
|
97
|
-
|
|
172
|
+
conversation,
|
|
173
|
+
filename: `${buildConversationExportBaseName(
|
|
174
|
+
{
|
|
175
|
+
cwd: conversation.workspaceFolder,
|
|
176
|
+
id: conversation.conversationId,
|
|
177
|
+
updatedAtMs: conversation.lastUpdatedAtMs ?? conversation.conversationMtimeMs,
|
|
178
|
+
},
|
|
179
|
+
'antigravity-conversation',
|
|
180
|
+
)}.${options.outputFormat ?? 'md'}`,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const deleteAntigravityConversationById = async (conversationId: string) => {
|
|
185
|
+
const { deleteAntigravityConversation } = await import('@spiracha/lib/antigravity-db');
|
|
186
|
+
const { resolveAntigravityRoots } = await import('@spiracha/lib/antigravity-exporter-types');
|
|
187
|
+
const result = await deleteAntigravityConversation(resolveAntigravityRoots(), conversationId);
|
|
188
|
+
if (result.deletedConversationIds.length === 0) {
|
|
189
|
+
throw new Error(`Antigravity conversation not found: ${conversationId}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const deleteAntigravityConversationsById = async (conversationIds: string[]) => {
|
|
196
|
+
const { deleteAntigravityConversation } = await import('@spiracha/lib/antigravity-db');
|
|
197
|
+
const { resolveAntigravityRoots } = await import('@spiracha/lib/antigravity-exporter-types');
|
|
198
|
+
const roots = resolveAntigravityRoots();
|
|
199
|
+
const results = await runDeleteBatch(conversationIds, (conversationId) =>
|
|
200
|
+
deleteAntigravityConversation(roots, conversationId),
|
|
201
|
+
);
|
|
202
|
+
const deletedConversationIds = results.flatMap((result) => result.deletedConversationIds);
|
|
203
|
+
if (deletedConversationIds.length === 0) {
|
|
204
|
+
throw new Error('No Antigravity conversations were deleted');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
deletedConversationIds,
|
|
209
|
+
deletedPaths: results.flatMap((result) => result.deletedPaths),
|
|
98
210
|
};
|
|
99
211
|
};
|
|
100
212
|
|
|
@@ -105,7 +217,7 @@ export const getAntigravityConversationDetailFn = createServerFn({ method: 'GET'
|
|
|
105
217
|
});
|
|
106
218
|
|
|
107
219
|
export const exportAntigravityArtifactsFn = createServerFn({ method: 'POST' })
|
|
108
|
-
.validator(
|
|
220
|
+
.validator(conversationSchema)
|
|
109
221
|
.handler(async ({ data }) => {
|
|
110
222
|
const { renderAntigravityArtifactsMarkdown } = await import('@spiracha/lib/antigravity-db');
|
|
111
223
|
const conversation = await findAntigravityConversationById(data.conversationId);
|
|
@@ -124,5 +236,91 @@ export const exportAntigravityArtifactsFn = createServerFn({ method: 'POST' })
|
|
|
124
236
|
export const exportAntigravityConversationFn = createServerFn({ method: 'POST' })
|
|
125
237
|
.validator(exportSchema)
|
|
126
238
|
.handler(async ({ data }) => {
|
|
127
|
-
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
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
export const exportAntigravityConversations = async (input: z.input<typeof exportConversationsSchema>) => {
|
|
250
|
+
const data = exportConversationsSchema.parse(input);
|
|
251
|
+
const { renderSourceSessionsDownload } = await import('./source-session-export-server');
|
|
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
|
+
);
|
|
266
|
+
const entries = await Promise.all(
|
|
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
|
+
);
|
|
279
|
+
const conversation = result.conversation;
|
|
280
|
+
const exportCwd =
|
|
281
|
+
(conversation.projectId ? projectNames.get(conversation.projectId) : null) ??
|
|
282
|
+
conversation.workspaceFolder;
|
|
283
|
+
const updatedAtMs = conversation.lastUpdatedAtMs ?? conversation.conversationMtimeMs;
|
|
284
|
+
return {
|
|
285
|
+
content: result.content,
|
|
286
|
+
cwd: exportCwd,
|
|
287
|
+
fallbackBaseName: 'antigravity-conversation',
|
|
288
|
+
fileBaseName: buildConversationExportBaseName(
|
|
289
|
+
{
|
|
290
|
+
cwd: exportCwd,
|
|
291
|
+
id: conversation.conversationId,
|
|
292
|
+
updatedAtMs,
|
|
293
|
+
},
|
|
294
|
+
'antigravity-conversation',
|
|
295
|
+
),
|
|
296
|
+
sessionId: conversation.conversationId,
|
|
297
|
+
updatedAtMs,
|
|
298
|
+
};
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
return renderSourceSessionsDownload({
|
|
303
|
+
entries,
|
|
304
|
+
fallbackBaseName: 'antigravity-conversations',
|
|
305
|
+
outputFormat: data.outputFormat,
|
|
306
|
+
zipArchive: data.zipArchive,
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export const exportAntigravityConversationsFn = createServerFn({ method: 'POST' })
|
|
311
|
+
.validator(exportConversationsSchema)
|
|
312
|
+
.handler(async ({ data }) => {
|
|
313
|
+
return exportAntigravityConversations(data);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
export const deleteAntigravityConversationFn = createServerFn({ method: 'POST' })
|
|
317
|
+
.validator(conversationSchema)
|
|
318
|
+
.handler(async ({ data }) => {
|
|
319
|
+
return deleteAntigravityConversationById(data.conversationId);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
export const deleteAntigravityConversationsFn = createServerFn({ method: 'POST' })
|
|
323
|
+
.validator(deleteConversationsSchema)
|
|
324
|
+
.handler(async ({ data }) => {
|
|
325
|
+
return deleteAntigravityConversationsById(data.conversationIds);
|
|
128
326
|
});
|
|
@@ -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;
|
|
@@ -18,9 +20,22 @@ type ParsedToolCall = {
|
|
|
18
20
|
name: string;
|
|
19
21
|
};
|
|
20
22
|
|
|
21
|
-
const HEADING_PATTERN = /^##\s+(
|
|
23
|
+
const HEADING_PATTERN = /^##\s+((?:User|Assistant|System|Event)|Tool:\s*.+)$/iu;
|
|
22
24
|
const TIMESTAMP_PATTERN = /^_Timestamp:\s*(.+?)_$/u;
|
|
23
25
|
const TOOL_HEADING_PATTERN = /^tool:\s*(.+)$/iu;
|
|
26
|
+
const CONTROL_SUBHEADING_PATTERN = /^###\s+(Thinking|Tool Calls)\s*$/iu;
|
|
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
|
+
};
|
|
24
39
|
|
|
25
40
|
const splitMarkdownSections = (markdown: string | null): MarkdownSection[] => {
|
|
26
41
|
if (!markdown?.trim()) {
|
|
@@ -31,6 +46,7 @@ const splitMarkdownSections = (markdown: string | null): MarkdownSection[] => {
|
|
|
31
46
|
const lines = markdown.split(/\r?\n/u);
|
|
32
47
|
let currentHeading: string | null = null;
|
|
33
48
|
let currentLines: string[] = [];
|
|
49
|
+
let fence: MarkdownFence | null = null;
|
|
34
50
|
|
|
35
51
|
const flush = () => {
|
|
36
52
|
if (!currentHeading) {
|
|
@@ -40,12 +56,13 @@ const splitMarkdownSections = (markdown: string | null): MarkdownSection[] => {
|
|
|
40
56
|
sections.push({
|
|
41
57
|
body: currentLines.join('\n').trim(),
|
|
42
58
|
heading: currentHeading,
|
|
43
|
-
sequence: sections.length *
|
|
59
|
+
sequence: sections.length * 1000,
|
|
44
60
|
});
|
|
45
61
|
};
|
|
46
62
|
|
|
47
63
|
for (const line of lines) {
|
|
48
|
-
const
|
|
64
|
+
const fenceToken = /^\s*(`{3,}|~{3,})/u.exec(line)?.[1] ?? null;
|
|
65
|
+
const heading = fence === null && fenceToken === null ? HEADING_PATTERN.exec(line) : null;
|
|
49
66
|
if (heading) {
|
|
50
67
|
flush();
|
|
51
68
|
currentHeading = heading[1]!.trim();
|
|
@@ -56,6 +73,8 @@ const splitMarkdownSections = (markdown: string | null): MarkdownSection[] => {
|
|
|
56
73
|
if (currentHeading) {
|
|
57
74
|
currentLines.push(line);
|
|
58
75
|
}
|
|
76
|
+
|
|
77
|
+
fence = updateMarkdownFence(fence, fenceToken);
|
|
59
78
|
}
|
|
60
79
|
|
|
61
80
|
flush();
|
|
@@ -72,7 +91,12 @@ const extractTimestamp = (body: string): { body: string; timestamp: string | nul
|
|
|
72
91
|
};
|
|
73
92
|
|
|
74
93
|
const sectionBodyBeforeSubheading = (body: string): string => {
|
|
75
|
-
|
|
94
|
+
const lines = body.split(/\r?\n/u);
|
|
95
|
+
const controlIndex = lines.findIndex((line) => CONTROL_SUBHEADING_PATTERN.test(line.trim()));
|
|
96
|
+
return lines
|
|
97
|
+
.slice(0, controlIndex === -1 ? undefined : controlIndex)
|
|
98
|
+
.join('\n')
|
|
99
|
+
.trim();
|
|
76
100
|
};
|
|
77
101
|
|
|
78
102
|
const extractSubheadingBlock = (body: string, title: string): string => {
|
|
@@ -82,7 +106,9 @@ const extractSubheadingBlock = (body: string, title: string): string => {
|
|
|
82
106
|
return '';
|
|
83
107
|
}
|
|
84
108
|
|
|
85
|
-
const nextSubheadingIndex = lines.findIndex(
|
|
109
|
+
const nextSubheadingIndex = lines.findIndex(
|
|
110
|
+
(line, index) => index > startIndex && CONTROL_SUBHEADING_PATTERN.test(line.trim()),
|
|
111
|
+
);
|
|
86
112
|
return lines
|
|
87
113
|
.slice(startIndex + 1, nextSubheadingIndex === -1 ? undefined : nextSubheadingIndex)
|
|
88
114
|
.join('\n')
|
|
@@ -187,38 +213,37 @@ const buildToolOutputEvent = (
|
|
|
187
213
|
});
|
|
188
214
|
|
|
189
215
|
const getFinalAssistantSectionSequences = (sections: MarkdownSection[]): Set<number> => {
|
|
190
|
-
const
|
|
191
|
-
let currentAssistantContentSequence: number | null = null;
|
|
192
|
-
|
|
193
|
-
const flushAssistantRun = () => {
|
|
194
|
-
if (currentAssistantContentSequence !== null) {
|
|
195
|
-
finalAssistantSectionSequences.add(currentAssistantContentSequence);
|
|
196
|
-
}
|
|
197
|
-
currentAssistantContentSequence = null;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
for (const section of sections) {
|
|
216
|
+
const items = sections.map((section) => {
|
|
201
217
|
const heading = section.heading.toLowerCase();
|
|
202
218
|
if (heading === 'user') {
|
|
203
|
-
|
|
204
|
-
|
|
219
|
+
return {
|
|
220
|
+
hasContent: Boolean(section.body),
|
|
221
|
+
hasToolCalls: false,
|
|
222
|
+
role: 'user' as const,
|
|
223
|
+
sequence: section.sequence,
|
|
224
|
+
};
|
|
205
225
|
}
|
|
206
226
|
|
|
207
|
-
if (heading
|
|
208
|
-
|
|
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
|
+
};
|
|
209
236
|
}
|
|
210
237
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
238
|
+
return {
|
|
239
|
+
hasContent: Boolean(section.body),
|
|
240
|
+
hasToolCalls: false,
|
|
241
|
+
role: 'other' as const,
|
|
242
|
+
sequence: section.sequence,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
219
245
|
|
|
220
|
-
|
|
221
|
-
return finalAssistantSectionSequences;
|
|
246
|
+
return getFinalAntigravityAssistantSequences(items);
|
|
222
247
|
};
|
|
223
248
|
|
|
224
249
|
const textSectionToEvents = (
|
|
@@ -290,46 +315,5 @@ export const antigravityMarkdownToThreadEvents = (markdown: string | null): Thre
|
|
|
290
315
|
return sections.flatMap((section) => sectionToEvents(section, finalAssistantSectionSequences));
|
|
291
316
|
};
|
|
292
317
|
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
if (event.role === 'assistant') {
|
|
296
|
-
stats.assistantMessageCount += 1;
|
|
297
|
-
}
|
|
298
|
-
if (event.role === 'user') {
|
|
299
|
-
stats.userMessageCount += 1;
|
|
300
|
-
}
|
|
301
|
-
if (event.phase === 'commentary') {
|
|
302
|
-
stats.commentaryCount += 1;
|
|
303
|
-
}
|
|
304
|
-
if (event.phase === 'final_answer') {
|
|
305
|
-
stats.finalAnswerCount += 1;
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
export const getAntigravityThreadTranscriptStats = (events: ThreadEvent[]): ThreadTranscriptStats => {
|
|
310
|
-
const stats: ThreadTranscriptStats = {
|
|
311
|
-
assistantMessageCount: 0,
|
|
312
|
-
commentaryCount: 0,
|
|
313
|
-
execCommandCount: 0,
|
|
314
|
-
finalAnswerCount: 0,
|
|
315
|
-
messageCount: 0,
|
|
316
|
-
toolCallCount: 0,
|
|
317
|
-
toolOutputCount: 0,
|
|
318
|
-
userMessageCount: 0,
|
|
319
|
-
webSearchEventCount: 0,
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
for (const event of events) {
|
|
323
|
-
if (event.kind === 'message') {
|
|
324
|
-
updateMessageStats(stats, event);
|
|
325
|
-
}
|
|
326
|
-
if (event.kind === 'tool_call') {
|
|
327
|
-
stats.toolCallCount += 1;
|
|
328
|
-
}
|
|
329
|
-
if (event.kind === 'tool_output') {
|
|
330
|
-
stats.toolOutputCount += 1;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return stats;
|
|
335
|
-
};
|
|
318
|
+
export const getAntigravityThreadTranscriptStats = (events: ThreadEvent[]): ThreadTranscriptStats =>
|
|
319
|
+
getThreadTranscriptStats(events);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { queryOptions } from '@tanstack/react-query';
|
|
2
2
|
import {
|
|
3
3
|
getClaudeCodeSessionDetailFn,
|
|
4
|
+
getClaudeCodeSessionTranscriptFn,
|
|
4
5
|
listClaudeCodeSessionsFn,
|
|
5
6
|
listClaudeCodeWorkspacesFn,
|
|
6
7
|
} from './claude-code-server';
|
|
@@ -24,3 +25,10 @@ export const claudeCodeSessionDetailQueryOptions = (sessionId: string | null) =>
|
|
|
24
25
|
queryFn: () => getClaudeCodeSessionDetailFn({ data: { sessionId: sessionId ?? '' } }),
|
|
25
26
|
queryKey: ['claude-code-session', sessionId ?? 'none'],
|
|
26
27
|
});
|
|
28
|
+
|
|
29
|
+
export const claudeCodeSessionTranscriptQueryOptions = (sessionId: string | null) =>
|
|
30
|
+
queryOptions({
|
|
31
|
+
enabled: sessionId !== null,
|
|
32
|
+
queryFn: () => getClaudeCodeSessionTranscriptFn({ data: { sessionId: sessionId ?? '' } }),
|
|
33
|
+
queryKey: ['claude-code-session-transcript', sessionId ?? 'none'],
|
|
34
|
+
});
|