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.
Files changed (162) hide show
  1. package/AGENTS.md +27 -12
  2. package/README.md +42 -19
  3. package/apps/ui/AGENTS.md +18 -10
  4. package/apps/ui/README.md +36 -11
  5. package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
  6. package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
  7. package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
  8. package/apps/ui/src/components/app-shell.tsx +46 -3
  9. package/apps/ui/src/components/breadcrumbs.tsx +21 -3
  10. package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
  11. package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
  12. package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
  13. package/apps/ui/src/components/data-table.tsx +23 -5
  14. package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
  15. package/apps/ui/src/components/export-dialog.tsx +79 -74
  16. package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
  17. package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
  18. package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
  19. package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
  20. package/apps/ui/src/components/page-header.tsx +15 -9
  21. package/apps/ui/src/components/projects-table.tsx +4 -1
  22. package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
  23. package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
  24. package/apps/ui/src/components/route-error-panel.tsx +13 -0
  25. package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
  26. package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
  27. package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
  28. package/apps/ui/src/components/threads-table.tsx +13 -47
  29. package/apps/ui/src/components/transcript-view.tsx +148 -52
  30. package/apps/ui/src/components/ui/select.tsx +1 -1
  31. package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
  32. package/apps/ui/src/lib/antigravity-server.ts +209 -11
  33. package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
  34. package/apps/ui/src/lib/claude-code-queries.ts +8 -0
  35. package/apps/ui/src/lib/claude-code-server.ts +185 -7
  36. package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
  37. package/apps/ui/src/lib/codex-queries.ts +16 -0
  38. package/apps/ui/src/lib/codex-server.ts +134 -52
  39. package/apps/ui/src/lib/cursor-server.ts +68 -53
  40. package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
  41. package/apps/ui/src/lib/delete-batch.ts +52 -0
  42. package/apps/ui/src/lib/error-presentation.ts +43 -0
  43. package/apps/ui/src/lib/export-mutation.ts +20 -0
  44. package/apps/ui/src/lib/export-options.ts +15 -0
  45. package/apps/ui/src/lib/formatters.ts +3 -5
  46. package/apps/ui/src/lib/grok-queries.ts +22 -0
  47. package/apps/ui/src/lib/grok-server.ts +168 -0
  48. package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
  49. package/apps/ui/src/lib/kiro-server.ts +97 -7
  50. package/apps/ui/src/lib/mutation-error.ts +15 -0
  51. package/apps/ui/src/lib/opencode-server.ts +96 -7
  52. package/apps/ui/src/lib/path-utils.ts +1 -1
  53. package/apps/ui/src/lib/qoder-queries.ts +15 -0
  54. package/apps/ui/src/lib/qoder-server.ts +67 -11
  55. package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
  56. package/apps/ui/src/lib/route-search.ts +114 -0
  57. package/apps/ui/src/lib/route-state-reset.tsx +10 -0
  58. package/apps/ui/src/lib/settings-server.ts +33 -0
  59. package/apps/ui/src/lib/settings-store.tsx +82 -38
  60. package/apps/ui/src/lib/settings.ts +65 -0
  61. package/apps/ui/src/lib/source-session-export-server.ts +86 -3
  62. package/apps/ui/src/lib/thread-metadata.ts +22 -0
  63. package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
  64. package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
  65. package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
  66. package/apps/ui/src/routeTree.gen.ts +341 -234
  67. package/apps/ui/src/routes/__root.tsx +12 -15
  68. package/apps/ui/src/routes/analytics.tsx +10 -61
  69. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +149 -29
  70. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
  71. package/apps/ui/src/routes/antigravity.index.tsx +2 -2
  72. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
  73. package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
  74. package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
  75. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +168 -28
  76. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
  77. package/apps/ui/src/routes/claude-code.index.tsx +2 -2
  78. package/apps/ui/src/routes/codex.$project.tsx +33 -31
  79. package/apps/ui/src/routes/codex.index.tsx +5 -10
  80. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +31 -18
  81. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
  82. package/apps/ui/src/routes/cursor.index.tsx +5 -2
  83. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
  84. package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
  85. package/apps/ui/src/routes/grok.index.tsx +48 -0
  86. package/apps/ui/src/routes/index.tsx +2 -18
  87. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
  88. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
  89. package/apps/ui/src/routes/kiro.index.tsx +2 -2
  90. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
  91. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
  92. package/apps/ui/src/routes/opencode.index.tsx +6 -2
  93. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
  94. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
  95. package/apps/ui/src/routes/qoder.index.tsx +2 -2
  96. package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
  97. package/apps/ui/vite.config.ts +22 -12
  98. package/bin/spiracha.ts +28 -1
  99. package/package.json +37 -27
  100. package/src/client.ts +183 -5
  101. package/src/lib/antigravity-db.ts +424 -94
  102. package/src/lib/antigravity-exporter-types.ts +3 -0
  103. package/src/lib/antigravity-keychain.ts +7 -8
  104. package/src/lib/antigravity-projects.ts +201 -0
  105. package/src/lib/antigravity-transcript-phase.ts +47 -0
  106. package/src/lib/claude-code-db.ts +534 -42
  107. package/src/lib/claude-code-exporter-types.ts +5 -0
  108. package/src/lib/claude-code-transcript-phase.ts +60 -1
  109. package/src/lib/claude-code-transcript.ts +8 -5
  110. package/src/lib/codex-analytics.ts +32 -5
  111. package/src/lib/codex-browser-db.ts +540 -137
  112. package/src/lib/codex-browser-export.ts +18 -27
  113. package/src/lib/codex-browser-types.ts +21 -1
  114. package/src/lib/codex-thread-cache.ts +106 -23
  115. package/src/lib/codex-thread-parser.ts +149 -55
  116. package/src/lib/codex-thread-recovery.ts +38 -11
  117. package/src/lib/codex-transcript-filter.ts +31 -0
  118. package/src/lib/codex-transcript-renderer.ts +87 -79
  119. package/src/lib/concurrency.ts +56 -3
  120. package/src/lib/conversation-api.ts +410 -27
  121. package/src/lib/conversation-data/adapter-helpers.ts +25 -3
  122. package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
  123. package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
  124. package/src/lib/conversation-data/codex-adapter.ts +108 -73
  125. package/src/lib/conversation-data/cursor-adapter.ts +77 -29
  126. package/src/lib/conversation-data/grok-adapter.ts +223 -0
  127. package/src/lib/conversation-data/index.ts +105 -11
  128. package/src/lib/conversation-data/kiro-adapter.ts +59 -12
  129. package/src/lib/conversation-data/message-selector.ts +2 -5
  130. package/src/lib/conversation-data/opencode-adapter.ts +67 -17
  131. package/src/lib/conversation-data/path-match.ts +1 -13
  132. package/src/lib/conversation-data/qoder-adapter.ts +67 -37
  133. package/src/lib/conversation-data/types.ts +43 -0
  134. package/src/lib/conversation-zip-export.ts +75 -0
  135. package/src/lib/cursor-db.ts +68 -28
  136. package/src/lib/cursor-id.ts +17 -0
  137. package/src/lib/cursor-recovery.ts +146 -35
  138. package/src/lib/cursor-transcript-phase.ts +40 -0
  139. package/src/lib/cursor-transcript.ts +21 -5
  140. package/src/lib/grok-db.ts +1068 -0
  141. package/src/lib/grok-exporter-types.ts +110 -0
  142. package/src/lib/grok-transcript-phase.ts +61 -0
  143. package/src/lib/grok-transcript.ts +167 -0
  144. package/src/lib/kiro-db.ts +187 -43
  145. package/src/lib/kiro-transcript.ts +0 -4
  146. package/src/lib/model-label.ts +11 -3
  147. package/src/lib/opencode-db.ts +599 -60
  148. package/src/lib/opencode-think-tags.ts +17 -3
  149. package/src/lib/opencode-transcript.ts +8 -5
  150. package/src/lib/portable-path.ts +9 -0
  151. package/src/lib/qoder-acp-client.ts +11 -2
  152. package/src/lib/qoder-db.ts +51 -17
  153. package/src/lib/qoder-transcript-phase.ts +64 -0
  154. package/src/lib/qoder-transcript.ts +30 -2
  155. package/src/lib/shared.ts +63 -15
  156. package/src/lib/sqlite-error.ts +31 -5
  157. package/src/lib/transcript-load-limiter.ts +87 -0
  158. package/src/lib/ui-cache.ts +185 -24
  159. package/src/lib/ui-export-archive.ts +58 -39
  160. package/src/lib/ui-export-files.ts +75 -15
  161. package/src/lib/ui-export-zip.ts +40 -0
  162. 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 = transcriptLocked
61
- ? null
62
- : await renderAntigravityConversationMarkdown(conversation, { keychainSecret });
63
- const artifactsMarkdown =
64
- conversation.artifactCount > 0 ? await renderAntigravityArtifactsMarkdown(conversation) : null;
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
- export const loadAntigravityConversationExport = async (conversationId: string) => {
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 renderAntigravityConversationMarkdown(conversation, { keychainSecret });
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
- filename: `${conversationId}.md`,
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(exportSchema)
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 loadAntigravityConversationExport(data.conversationId);
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+(.+)$/u;
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 * 10,
59
+ sequence: sections.length * 1000,
44
60
  });
45
61
  };
46
62
 
47
63
  for (const line of lines) {
48
- const heading = HEADING_PATTERN.exec(line);
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
- return body.split(/^###\s+/mu)[0]?.trim() ?? '';
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((line, index) => index > startIndex && line.startsWith('### '));
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 finalAssistantSectionSequences = new Set<number>();
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
- flushAssistantRun();
204
- continue;
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 !== 'assistant') {
208
- continue;
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
- const { body } = extractTimestamp(section.body);
212
- const parsed = parseAssistantSection(body);
213
- if (parsed.toolCalls.length > 0) {
214
- currentAssistantContentSequence = null;
215
- } else if (parsed.content) {
216
- currentAssistantContentSequence = section.sequence;
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
- flushAssistantRun();
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 updateMessageStats = (stats: ThreadTranscriptStats, event: Extract<ThreadEvent, { kind: 'message' }>) => {
294
- stats.messageCount += 1;
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
+ });