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,4 +1,5 @@
1
- import { readdir, stat } from 'node:fs/promises';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, readdir, rename, rm, stat } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import {
4
5
  type AntigravityArtifact,
@@ -11,7 +12,18 @@ import {
11
12
  resolveAntigravityRoots,
12
13
  } from './antigravity-exporter-types';
13
14
  import { decryptAntigravitySafeStoragePayload } from './antigravity-keychain';
15
+ import { resolveAntigravityProjectNames } from './antigravity-projects';
16
+ import { getAntigravityAssistantPhase, getFinalAntigravityAssistantSequences } from './antigravity-transcript-phase';
14
17
  import { mapWithConcurrency } from './concurrency';
18
+ import {
19
+ type ExportFormat,
20
+ formatInlineLiteral,
21
+ type MetadataEntry,
22
+ renderCodeBlock,
23
+ renderDocumentTitle,
24
+ renderMetadataBlock,
25
+ renderSection,
26
+ } from './shared';
15
27
 
16
28
  type ProtoField = {
17
29
  bytes?: Uint8Array;
@@ -25,6 +37,7 @@ type SummaryEntry = {
25
37
  createdAtMs: number | null;
26
38
  indexedItemCount: number | null;
27
39
  lastUpdatedAtMs: number | null;
40
+ projectId: string | null;
28
41
  summaryPath: string;
29
42
  title: string;
30
43
  workspaceFolder: string | null;
@@ -33,6 +46,11 @@ type SummaryEntry = {
33
46
  workspaceUri: string | null;
34
47
  };
35
48
 
49
+ export type DeleteAntigravityConversationResult = {
50
+ deletedConversationIds: string[];
51
+ deletedPaths: string[];
52
+ };
53
+
36
54
  type ConversationFile = {
37
55
  bytes: number;
38
56
  mtimeMs: number;
@@ -48,7 +66,6 @@ const isSafeConversationId = (value: string) => SAFE_CONVERSATION_ID_PATTERN.tes
48
66
  type TranscriptFile = {
49
67
  bytes: number;
50
68
  entryCount: number;
51
- fullPath: string | null;
52
69
  mtimeMs: number;
53
70
  model: string | null;
54
71
  path: string;
@@ -60,11 +77,19 @@ export type AntigravityConversationMessage = {
60
77
  createdAtMs: number | null;
61
78
  metadata: Record<string, unknown>;
62
79
  order: number;
63
- phase: 'final_answer' | 'reasoning' | 'tool_call' | 'tool_output' | 'unknown';
80
+ phase: 'commentary' | 'final_answer' | 'reasoning' | 'tool_call' | 'tool_output' | 'unknown';
64
81
  role: 'assistant' | 'system' | 'tool' | 'unknown' | 'user';
65
82
  text: string;
66
83
  };
67
84
 
85
+ export type AntigravityConversationRenderOptions = {
86
+ includeCommentary?: boolean;
87
+ includeMetadata?: boolean;
88
+ includeTools?: boolean;
89
+ keychainSecret?: string | null;
90
+ outputFormat?: ExportFormat;
91
+ };
92
+
68
93
  type WorkspaceInfo = Pick<SummaryEntry, 'workspaceFolder' | 'workspaceKey' | 'workspaceLabel' | 'workspaceUri'>;
69
94
 
70
95
  const UNKNOWN_WORKSPACE: WorkspaceInfo = {
@@ -271,8 +296,8 @@ const parseContextWorkspaceInfo = (field: ProtoField | null): WorkspaceInfo | nu
271
296
  // Antigravity summary parsing is reverse-engineered from agyhub_summaries_proto.pb:
272
297
  // entry field 1 = conversation id, entry field 2 = summary message. Inside that summary,
273
298
  // field 1 = title, 2 = indexed item count, 3 = last-updated timestamp, 7 = created timestamp,
274
- // 9 = workspace info, and 17 = context workspace info. parseWorkspaceInfo uses nested fields
275
- // 1/2 for URI variants; parseContextWorkspaceInfo uses field 7 or nested workspace field 1.
299
+ // 9 = workspace info, and 17 = context metadata. Context field 18 is the Antigravity
300
+ // project id; workspace parsing uses context field 7 or nested workspace field 1.
276
301
  const parseSummaryEntry = (entryField: ProtoField, summaryPath: string): SummaryEntry | null => {
277
302
  try {
278
303
  const entryFields = nestedFields(entryField);
@@ -283,6 +308,7 @@ const parseSummaryEntry = (entryField: ProtoField, summaryPath: string): Summary
283
308
  }
284
309
 
285
310
  const summaryFields = nestedFields(summaryBytes);
311
+ const contextFields = nestedFields(firstField(summaryFields, 17));
286
312
  const workspace =
287
313
  parseWorkspaceInfo(firstField(summaryFields, 9)) ??
288
314
  parseContextWorkspaceInfo(firstField(summaryFields, 17)) ??
@@ -294,6 +320,7 @@ const parseSummaryEntry = (entryField: ProtoField, summaryPath: string): Summary
294
320
  createdAtMs: parseTimestampMs(firstField(summaryFields, 7)),
295
321
  indexedItemCount: fieldNumberValue(summaryFields, 2),
296
322
  lastUpdatedAtMs: parseTimestampMs(firstField(summaryFields, 3)),
323
+ projectId: fieldString(contextFields, 18),
297
324
  summaryPath,
298
325
  title: cleanTitle(fieldString(summaryFields, 1), conversationId),
299
326
  };
@@ -318,6 +345,92 @@ export const readAntigravitySummaryIndex = async (summaryPath: string): Promise<
318
345
  }
319
346
  };
320
347
 
348
+ const getFieldBounds = (
349
+ buffer: Uint8Array,
350
+ start: number,
351
+ ): {
352
+ end: number;
353
+ fieldNumber: number;
354
+ payloadEnd: number;
355
+ payloadStart: number;
356
+ wireType: number;
357
+ } => {
358
+ const key = readVarint(buffer, start, buffer.length);
359
+ const fieldNumber = key.value >> 3;
360
+ const wireType = key.value & 7;
361
+ let index = key.next;
362
+
363
+ if (wireType === 0) {
364
+ const value = readVarint(buffer, index, buffer.length);
365
+ return { end: value.next, fieldNumber, payloadEnd: value.next, payloadStart: index, wireType };
366
+ }
367
+
368
+ if (wireType === 1) {
369
+ return { end: index + 8, fieldNumber, payloadEnd: index + 8, payloadStart: index, wireType };
370
+ }
371
+
372
+ if (wireType === 2) {
373
+ const length = readVarint(buffer, index, buffer.length);
374
+ index = length.next;
375
+ return {
376
+ end: index + length.value,
377
+ fieldNumber,
378
+ payloadEnd: index + length.value,
379
+ payloadStart: index,
380
+ wireType,
381
+ };
382
+ }
383
+
384
+ if (wireType === 5) {
385
+ return { end: index + 4, fieldNumber, payloadEnd: index + 4, payloadStart: index, wireType };
386
+ }
387
+
388
+ throw new Error(`Unsupported protobuf wire type: ${wireType}`);
389
+ };
390
+
391
+ const removeConversationFromSummaryIndex = async (summaryPath: string, conversationId: string): Promise<boolean> => {
392
+ if (!(await pathExists(summaryPath))) {
393
+ return false;
394
+ }
395
+
396
+ const buffer = new Uint8Array(await Bun.file(summaryPath).arrayBuffer());
397
+ const retained: Uint8Array[] = [];
398
+ let removed = false;
399
+ let index = 0;
400
+
401
+ while (index < buffer.length) {
402
+ const bounds = getFieldBounds(buffer, index);
403
+ const fieldBytes = buffer.slice(index, bounds.end);
404
+ const shouldRemove =
405
+ bounds.fieldNumber === 1 &&
406
+ bounds.wireType === 2 &&
407
+ fieldString(parseProtoFields(buffer, bounds.payloadStart, bounds.payloadEnd), 1) === conversationId;
408
+
409
+ if (shouldRemove) {
410
+ removed = true;
411
+ } else {
412
+ retained.push(fieldBytes);
413
+ }
414
+
415
+ index = bounds.end;
416
+ }
417
+
418
+ if (!removed) {
419
+ return false;
420
+ }
421
+
422
+ const tempPath = `${summaryPath}.${randomUUID()}.tmp`;
423
+ const mode = (await stat(summaryPath)).mode & 0o777;
424
+ try {
425
+ await Bun.write(tempPath, Buffer.concat(retained));
426
+ await chmod(tempPath, mode);
427
+ await rename(tempPath, summaryPath);
428
+ } finally {
429
+ await rm(tempPath, { force: true });
430
+ }
431
+ return true;
432
+ };
433
+
321
434
  const preferConversationFile = (
322
435
  current: ConversationFile | undefined,
323
436
  candidate: ConversationFile,
@@ -491,15 +604,6 @@ const mergeArtifactMaps = async (roots: string[]): Promise<Map<string, Antigravi
491
604
  return merged;
492
605
  };
493
606
 
494
- const countJsonlEntries = async (filePath: string): Promise<number> => {
495
- try {
496
- const text = await Bun.file(filePath).text();
497
- return text.split(/\r?\n/u).filter((line) => line.trim().length > 0).length;
498
- } catch {
499
- return 0;
500
- }
501
- };
502
-
503
607
  const stripModelQualifier = (value: string): string => value.replace(/\s*\([^)]*\)\s*$/u, '').trim();
504
608
 
505
609
  const extractModelSelection = (content: string): string | null => {
@@ -524,18 +628,31 @@ const extractModelSelection = (content: string): string | null => {
524
628
  return rawModel ? stripModelQualifier(rawModel) : null;
525
629
  };
526
630
 
527
- const extractTranscriptModel = async (filePath: string): Promise<string | null> => {
528
- const text = await Bun.file(filePath)
529
- .text()
530
- .catch(() => '');
631
+ const transcriptAnalysisCache = new Map<string, { entryCount: number; model: string | null }>();
632
+
633
+ const analyzeTranscriptFile = async (filePath: string, size: number, mtimeMs: number) => {
634
+ const cacheKey = `${filePath}:${size}:${mtimeMs}`;
635
+ const cached = transcriptAnalysisCache.get(cacheKey);
636
+ if (cached) {
637
+ return cached;
638
+ }
639
+ const text = await Bun.file(filePath).text();
640
+ let model: string | null = null;
531
641
  for (const entry of parseLogEntries(text)) {
532
- const model = extractModelSelection(getString(entry.content) ?? '');
533
- if (model) {
534
- return model;
642
+ model = extractModelSelection(getString(entry.content) ?? '') ?? model;
643
+ if (model !== null) {
644
+ break;
535
645
  }
536
646
  }
537
-
538
- return null;
647
+ const analysis = {
648
+ entryCount: text.split(/\r?\n/u).filter((line) => line.trim().length > 0).length,
649
+ model,
650
+ };
651
+ if (transcriptAnalysisCache.size >= 256) {
652
+ transcriptAnalysisCache.delete(transcriptAnalysisCache.keys().next().value ?? '');
653
+ }
654
+ transcriptAnalysisCache.set(cacheKey, analysis);
655
+ return analysis;
539
656
  };
540
657
 
541
658
  const preferTranscriptFile = (current: TranscriptFile | undefined, candidate: TranscriptFile): TranscriptFile => {
@@ -543,15 +660,22 @@ const preferTranscriptFile = (current: TranscriptFile | undefined, candidate: Tr
543
660
  return candidate;
544
661
  }
545
662
 
663
+ const sourceRank = (source: TranscriptFile['source']) => (source === 'transcript' ? 2 : 1);
664
+ const candidateRank = sourceRank(candidate.source);
665
+ const currentRank = sourceRank(current.source);
666
+ if (candidateRank !== currentRank) {
667
+ return candidateRank > currentRank ? candidate : current;
668
+ }
669
+
546
670
  if (candidate.mtimeMs !== current.mtimeMs) {
547
671
  return candidate.mtimeMs > current.mtimeMs ? candidate : current;
548
672
  }
549
673
 
550
- if (candidate.source !== current.source) {
551
- return candidate.source === 'overview' ? candidate : current;
674
+ if (candidate.entryCount !== current.entryCount) {
675
+ return candidate.entryCount > current.entryCount ? candidate : current;
552
676
  }
553
677
 
554
- return candidate.entryCount > current.entryCount ? candidate : current;
678
+ return candidate.bytes > current.bytes ? candidate : current;
555
679
  };
556
680
 
557
681
  const readTranscriptFileCandidate = async (
@@ -565,15 +689,12 @@ const readTranscriptFileCandidate = async (
565
689
  if (!info.isFile()) {
566
690
  return null;
567
691
  }
692
+ const analysis = await analyzeTranscriptFile(transcriptPath, info.size, info.mtimeMs);
568
693
 
569
- const fullPath = path.join(logsDir, 'transcript_full.jsonl');
570
- const hasFullTranscript = await pathExists(fullPath);
571
- const modelSourcePath = hasFullTranscript ? fullPath : transcriptPath;
572
694
  return {
573
695
  bytes: info.size,
574
- entryCount: await countJsonlEntries(modelSourcePath),
575
- fullPath: hasFullTranscript ? fullPath : null,
576
- model: await extractTranscriptModel(modelSourcePath),
696
+ entryCount: analysis.entryCount,
697
+ model: analysis.model,
577
698
  mtimeMs: info.mtimeMs,
578
699
  path: transcriptPath,
579
700
  root,
@@ -601,8 +722,9 @@ const readTranscriptFilesForRoot = async (root: string): Promise<Map<string, Tra
601
722
 
602
723
  const logsDir = path.join(brainDir, entry.name, '.system_generated', 'logs');
603
724
  for (const candidate of [
604
- { name: 'overview.txt', source: 'overview' as const },
725
+ { name: 'transcript_full.jsonl', source: 'transcript' as const },
605
726
  { name: 'transcript.jsonl', source: 'transcript' as const },
727
+ { name: 'overview.txt', source: 'overview' as const },
606
728
  ]) {
607
729
  const transcript = await readTranscriptFileCandidate(root, logsDir, candidate);
608
730
  if (transcript) {
@@ -689,6 +811,8 @@ const toConversation = (
689
811
  ): AntigravityConversation => {
690
812
  const fallbackTitle = artifacts[0]?.summary ?? conversationId;
691
813
  const artifactBytes = artifacts.reduce((total, artifact) => total + artifact.bytes, 0);
814
+ const conversationBytes = file?.bytes ?? 0;
815
+ const transcriptBytes = transcript?.bytes ?? 0;
692
816
  const workspace = resolveConversationWorkspace(summary, file, transcript, artifacts);
693
817
  const lastUpdatedAtMs = resolveConversationLastUpdatedAt(artifacts, file, summary, transcript);
694
818
  const sourceRoot = resolveConversationSourceRoot(file, transcript, artifacts);
@@ -697,7 +821,7 @@ const toConversation = (
697
821
  artifactBytes,
698
822
  artifactCount: artifacts.length,
699
823
  artifacts,
700
- conversationBytes: file?.bytes ?? 0,
824
+ conversationBytes,
701
825
  conversationId,
702
826
  conversationMtimeMs: file?.mtimeMs ?? null,
703
827
  conversationPath: file?.path ?? null,
@@ -705,10 +829,12 @@ const toConversation = (
705
829
  indexedItemCount: summary?.indexedItemCount ?? null,
706
830
  lastUpdatedAtMs,
707
831
  model: transcript?.model ?? null,
832
+ projectId: summary?.projectId ?? null,
708
833
  sourceRoot,
709
834
  summaryPath: summary?.summaryPath ?? null,
710
835
  title: summary?.title ?? cleanTitle(fallbackTitle, conversationId),
711
- transcriptBytes: transcript?.bytes ?? 0,
836
+ totalBytes: conversationBytes + transcriptBytes + artifactBytes,
837
+ transcriptBytes,
712
838
  transcriptEntryCount: transcript?.entryCount ?? 0,
713
839
  transcriptPath: transcript?.path ?? null,
714
840
  transcriptSource: resolveConversationTranscriptSource(file, transcript),
@@ -750,25 +876,30 @@ export const listAntigravityConversations = async (
750
876
 
751
877
  export const groupAntigravityConversations = (
752
878
  conversations: AntigravityConversation[],
879
+ projectNames: ReadonlyMap<string, string> = new Map(),
753
880
  ): AntigravityWorkspaceGroup[] => {
754
881
  const groups = new Map<string, AntigravityWorkspaceGroup>();
755
882
  for (const conversation of conversations) {
756
- const current = groups.get(conversation.workspaceKey) ?? {
883
+ const projectName = conversation.projectId ? projectNames.get(conversation.projectId) : null;
884
+ const groupKey = conversation.projectId ? `project:${conversation.projectId}` : conversation.workspaceKey;
885
+ const current = groups.get(groupKey) ?? {
757
886
  artifactCount: 0,
758
887
  conversationBytes: 0,
759
888
  conversationCount: 0,
760
- key: conversation.workspaceKey,
761
- label: conversation.workspaceLabel,
889
+ key: groupKey,
890
+ label: projectName ?? conversation.workspaceLabel,
762
891
  lastActiveMs: 0,
892
+ totalBytes: 0,
763
893
  transcriptCount: 0,
764
- uri: conversation.workspaceUri,
894
+ uri: conversation.projectId ? null : conversation.workspaceUri,
765
895
  };
766
896
  current.artifactCount += conversation.artifactCount;
767
897
  current.conversationBytes += conversation.conversationBytes;
768
898
  current.conversationCount += 1;
769
899
  current.lastActiveMs = Math.max(current.lastActiveMs, conversation.lastUpdatedAtMs ?? 0);
770
900
  current.transcriptCount += conversation.transcriptEntryCount > 0 ? 1 : 0;
771
- groups.set(conversation.workspaceKey, current);
901
+ current.totalBytes += conversation.totalBytes;
902
+ groups.set(groupKey, current);
772
903
  }
773
904
 
774
905
  return [...groups.values()].sort((a, b) => b.lastActiveMs - a.lastActiveMs || a.label.localeCompare(b.label));
@@ -777,16 +908,67 @@ export const groupAntigravityConversations = (
777
908
  export const listAntigravityWorkspaceGroups = async (
778
909
  roots = resolveAntigravityRoots(),
779
910
  ): Promise<AntigravityWorkspaceGroup[]> => {
780
- return groupAntigravityConversations(await listAntigravityConversations(roots));
911
+ const conversations = await listAntigravityConversations(roots);
912
+ const projectNames = await resolveAntigravityProjectNames(
913
+ conversations.flatMap((conversation) => (conversation.projectId ? [conversation.projectId] : [])),
914
+ );
915
+ return groupAntigravityConversations(conversations, projectNames);
781
916
  };
782
917
 
783
918
  export const listAntigravityConversationsForGroup = async (
784
919
  workspaceKey: string,
785
920
  roots = resolveAntigravityRoots(),
786
921
  ): Promise<AntigravityConversation[]> => {
787
- return (await listAntigravityConversations(roots)).filter(
788
- (conversation) => conversation.workspaceKey === workspaceKey,
789
- );
922
+ return (await listAntigravityConversations(roots)).filter((conversation) => {
923
+ const conversationGroupKey = conversation.projectId
924
+ ? `project:${conversation.projectId}`
925
+ : conversation.workspaceKey;
926
+ return conversationGroupKey === workspaceKey;
927
+ });
928
+ };
929
+
930
+ const existingAntigravityDeletePaths = async (root: string, conversationId: string): Promise<string[]> => {
931
+ const conversationPath = path.join(getAntigravityConversationDir(root), `${conversationId}.pb`);
932
+ const artifactDir = path.join(getAntigravityBrainDir(root), conversationId);
933
+ const logsDir = path.join(artifactDir, '.system_generated', 'logs');
934
+ const candidates = [
935
+ conversationPath,
936
+ path.join(logsDir, 'overview.txt'),
937
+ path.join(logsDir, 'transcript.jsonl'),
938
+ path.join(logsDir, 'transcript_full.jsonl'),
939
+ artifactDir,
940
+ ];
941
+ const exists = await Promise.all(candidates.map(pathExists));
942
+ return candidates.filter((_, index) => exists[index]);
943
+ };
944
+
945
+ export const deleteAntigravityConversation = async (
946
+ roots: string[],
947
+ conversationId: string,
948
+ ): Promise<DeleteAntigravityConversationResult> => {
949
+ if (!isSafeConversationId(conversationId)) {
950
+ return { deletedConversationIds: [], deletedPaths: [] };
951
+ }
952
+
953
+ const deletedPaths: string[] = [];
954
+ let deletedSummary = false;
955
+
956
+ for (const root of roots) {
957
+ deletedSummary =
958
+ (await removeConversationFromSummaryIndex(getAntigravitySummaryIndexPath(root), conversationId)) ||
959
+ deletedSummary;
960
+
961
+ const rootPaths = await existingAntigravityDeletePaths(root, conversationId);
962
+ deletedPaths.push(...rootPaths);
963
+
964
+ await rm(path.join(getAntigravityConversationDir(root), `${conversationId}.pb`), { force: true });
965
+ await rm(path.join(getAntigravityBrainDir(root), conversationId), { force: true, recursive: true });
966
+ }
967
+
968
+ return {
969
+ deletedConversationIds: deletedSummary || deletedPaths.length > 0 ? [conversationId] : [],
970
+ deletedPaths: [...new Set(deletedPaths)],
971
+ };
790
972
  };
791
973
 
792
974
  export const renderAntigravityArtifactsMarkdown = async (
@@ -893,48 +1075,99 @@ const logEntryHeading = (entry: AntigravityLogEntry): string => {
893
1075
  return type ? `Tool: ${type}` : 'Event';
894
1076
  };
895
1077
 
896
- const renderToolCalls = (toolCalls: unknown): string[] => {
1078
+ type ResolvedAntigravityConversationRenderOptions = {
1079
+ includeCommentary: boolean;
1080
+ includeMetadata: boolean;
1081
+ includeTools: boolean;
1082
+ keychainSecret: string | null;
1083
+ outputFormat: ExportFormat;
1084
+ };
1085
+
1086
+ const resolveAntigravityRenderOptions = (
1087
+ options: AntigravityConversationRenderOptions,
1088
+ ): ResolvedAntigravityConversationRenderOptions => ({
1089
+ includeCommentary: options.includeCommentary ?? true,
1090
+ includeMetadata: options.includeMetadata ?? true,
1091
+ includeTools: options.includeTools ?? true,
1092
+ keychainSecret: options.keychainSecret ?? null,
1093
+ outputFormat: options.outputFormat ?? 'md',
1094
+ });
1095
+
1096
+ const renderNestedHeading = (title: string, outputFormat: ExportFormat) => {
1097
+ return outputFormat === 'md' ? `### ${title}` : `${title}\n${'~'.repeat(Math.max(title.length, 3))}`;
1098
+ };
1099
+
1100
+ const renderToolCalls = (toolCalls: unknown, outputFormat: ExportFormat): string => {
897
1101
  if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
898
- return [];
1102
+ return '';
899
1103
  }
900
1104
 
901
- const parts = ['### Tool Calls', ''];
1105
+ const parts: string[] = [];
902
1106
  for (const call of toolCalls) {
903
1107
  if (!call || typeof call !== 'object') {
904
1108
  continue;
905
1109
  }
906
1110
 
907
1111
  const { args, name } = call as { args?: unknown; name?: unknown };
908
- parts.push(`- \`${typeof name === 'string' ? name : 'unknown'}\``);
1112
+ parts.push(`- ${formatInlineLiteral(typeof name === 'string' ? name : 'unknown', outputFormat)}`);
909
1113
  if (args !== undefined) {
910
- parts.push('', '```json', JSON.stringify(args, null, 2), '```', '');
1114
+ parts.push('', 'Input:', '', renderCodeBlock(JSON.stringify(args, null, 2), outputFormat), '');
911
1115
  }
912
1116
  }
913
1117
 
914
- return parts;
1118
+ return parts.length > 0 ? [renderNestedHeading('Tool Calls', outputFormat), '', ...parts].join('\n').trimEnd() : '';
915
1119
  };
916
1120
 
917
- const renderLogEntry = (entry: AntigravityLogEntry): string[] => {
918
- const heading = logEntryHeading(entry);
919
- const timestamp = getString(entry.created_at);
1121
+ const renderLogEntryBodyParts = (
1122
+ entry: AntigravityLogEntry,
1123
+ role: AntigravityConversationMessage['role'],
1124
+ sequence: number,
1125
+ finalAssistantSequences: Set<number>,
1126
+ options: ResolvedAntigravityConversationRenderOptions,
1127
+ ): string[] => {
1128
+ const bodyParts: string[] = [];
1129
+ const thinking = getString(entry.thinking) ?? '';
1130
+ if (options.includeCommentary && thinking) {
1131
+ bodyParts.push(renderNestedHeading('Thinking', options.outputFormat), '', thinking.trim(), '');
1132
+ }
1133
+
920
1134
  const content = cleanLogContent(entry);
921
- const thinking = getString(entry.thinking);
922
- const parts = [`## ${heading}`, ''];
1135
+ const phase = role === 'assistant' ? getAntigravityAssistantPhase(sequence, finalAssistantSequences) : 'unknown';
1136
+ if (content && (role !== 'assistant' || phase !== 'commentary' || options.includeCommentary)) {
1137
+ bodyParts.push(content, '');
1138
+ }
923
1139
 
924
- if (timestamp) {
925
- parts.push(`_Timestamp: ${timestamp}_`, '');
1140
+ const toolCalls = options.includeTools ? renderToolCalls(entry.tool_calls, options.outputFormat) : '';
1141
+ if (toolCalls) {
1142
+ bodyParts.push(toolCalls);
926
1143
  }
927
1144
 
928
- if (thinking) {
929
- parts.push('### Thinking', '', thinking.trim(), '');
1145
+ return bodyParts;
1146
+ };
1147
+
1148
+ const renderLogEntry = (
1149
+ entry: AntigravityLogEntry,
1150
+ sequence: number,
1151
+ finalAssistantSequences: Set<number>,
1152
+ options: ResolvedAntigravityConversationRenderOptions,
1153
+ ): string => {
1154
+ const heading = logEntryHeading(entry);
1155
+ const role = logEntryRole(entry);
1156
+ if (!options.includeTools && (role === 'tool' || heading.startsWith('Tool: '))) {
1157
+ return '';
930
1158
  }
931
1159
 
932
- if (content) {
933
- parts.push(content, '');
1160
+ const timestamp = getString(entry.created_at);
1161
+ const bodyParts = renderLogEntryBodyParts(entry, role, sequence, finalAssistantSequences, options);
1162
+
1163
+ if (bodyParts.length === 0) {
1164
+ return '';
934
1165
  }
935
1166
 
936
- parts.push(...renderToolCalls(entry.tool_calls));
937
- return parts;
1167
+ const parts = timestamp
1168
+ ? [options.outputFormat === 'md' ? `_Timestamp: ${timestamp}_` : `Timestamp: ${timestamp}`, '', ...bodyParts]
1169
+ : bodyParts;
1170
+ return renderSection(heading, parts.join('\n').trimEnd(), options.outputFormat);
938
1171
  };
939
1172
 
940
1173
  const logEntryCreatedAtMs = (entry: AntigravityLogEntry): number | null => {
@@ -972,10 +1205,14 @@ const logEntryRole = (entry: AntigravityLogEntry): AntigravityConversationMessag
972
1205
  return 'unknown';
973
1206
  };
974
1207
 
975
- const logEntryPhase = (entry: AntigravityLogEntry): AntigravityConversationMessage['phase'] => {
1208
+ const logEntryPhase = (
1209
+ entry: AntigravityLogEntry,
1210
+ sequence: number,
1211
+ finalAssistantSequences: Set<number>,
1212
+ ): AntigravityConversationMessage['phase'] => {
976
1213
  const role = logEntryRole(entry);
977
1214
  if (role === 'assistant') {
978
- return 'final_answer';
1215
+ return getAntigravityAssistantPhase(sequence, finalAssistantSequences);
979
1216
  }
980
1217
  if (role === 'tool') {
981
1218
  return 'tool_output';
@@ -1005,11 +1242,27 @@ const toolCallsText = (toolCalls: unknown): string => {
1005
1242
  .join('\n');
1006
1243
  };
1007
1244
 
1008
- const logEntryToMessages = (entry: AntigravityLogEntry, index: number): AntigravityConversationMessage[] => {
1245
+ const getAntigravityPhaseItems = (entries: AntigravityLogEntry[]) => {
1246
+ return entries.map((entry, sequence) => {
1247
+ const role = logEntryRole(entry);
1248
+ return {
1249
+ hasContent: Boolean(cleanLogContent(entry)),
1250
+ hasToolCalls: Array.isArray(entry.tool_calls) && entry.tool_calls.length > 0,
1251
+ role: role === 'assistant' || role === 'user' ? role : ('other' as const),
1252
+ sequence,
1253
+ };
1254
+ });
1255
+ };
1256
+
1257
+ const logEntryToMessages = (
1258
+ entry: AntigravityLogEntry,
1259
+ index: number,
1260
+ finalAssistantSequences: Set<number>,
1261
+ ): AntigravityConversationMessage[] => {
1009
1262
  const order = logEntryOrder(entry, index);
1010
1263
  const createdAtMs = logEntryCreatedAtMs(entry);
1011
1264
  const role = logEntryRole(entry);
1012
- const phase = logEntryPhase(entry);
1265
+ const phase = logEntryPhase(entry, index, finalAssistantSequences);
1013
1266
  const metadata = logEntryMetadata(entry);
1014
1267
  const messages: AntigravityConversationMessage[] = [];
1015
1268
  const thinking = getString(entry.thinking)?.trim();
@@ -1058,15 +1311,39 @@ export const readAntigravityConversationMessages = async (
1058
1311
  return [];
1059
1312
  }
1060
1313
 
1061
- try {
1062
- const entries = parseLogEntries(await Bun.file(conversation.transcriptPath).text());
1063
- return entries.flatMap(logEntryToMessages);
1064
- } catch {
1065
- return [];
1314
+ const content = await Bun.file(conversation.transcriptPath).text();
1315
+ const entries = parseLogEntries(content);
1316
+ if (content.trim() && entries.length === 0) {
1317
+ throw new Error(`Antigravity transcript is corrupt: ${conversation.transcriptPath}`);
1318
+ }
1319
+ const finalAssistantSequences = getFinalAntigravityAssistantSequences(getAntigravityPhaseItems(entries));
1320
+ return entries.flatMap((entry, index) => logEntryToMessages(entry, index, finalAssistantSequences));
1321
+ };
1322
+
1323
+ const renderAntigravityMetadata = (entries: MetadataEntry[], outputFormat: ExportFormat): string => {
1324
+ if (outputFormat === 'txt') {
1325
+ return renderMetadataBlock(entries, outputFormat);
1066
1326
  }
1327
+
1328
+ return entries
1329
+ .filter((entry) => entry.value !== null && entry.value !== undefined && entry.value !== '')
1330
+ .map((entry) => `- ${entry.key}: ${formatInlineLiteral(String(entry.value), outputFormat)}`)
1331
+ .join('\n');
1067
1332
  };
1068
1333
 
1069
- const renderAntigravityTranscriptMarkdown = async (conversation: AntigravityConversation): Promise<string | null> => {
1334
+ const buildAntigravityIdentityMetadata = (
1335
+ conversation: AntigravityConversation,
1336
+ exportedFrom: string,
1337
+ ): MetadataEntry[] => [
1338
+ { key: 'exported_from', value: exportedFrom },
1339
+ { key: 'conversation_id', value: conversation.conversationId },
1340
+ { key: 'workspace', value: conversation.workspaceUri },
1341
+ ];
1342
+
1343
+ const renderAntigravityTranscript = async (
1344
+ conversation: AntigravityConversation,
1345
+ options: ResolvedAntigravityConversationRenderOptions,
1346
+ ): Promise<string | null> => {
1070
1347
  if (!conversation.transcriptPath || !conversation.transcriptSource) {
1071
1348
  return null;
1072
1349
  }
@@ -1080,35 +1357,44 @@ const renderAntigravityTranscriptMarkdown = async (conversation: AntigravityConv
1080
1357
  conversation.transcriptSource === 'overview'
1081
1358
  ? 'antigravity_overview_transcript'
1082
1359
  : 'antigravity_jsonl_transcript';
1360
+ const finalAssistantSequences = getFinalAntigravityAssistantSequences(getAntigravityPhaseItems(entries));
1361
+ const sections = entries
1362
+ .map((entry, sequence) => renderLogEntry(entry, sequence, finalAssistantSequences, options))
1363
+ .filter(Boolean);
1083
1364
  const parts = [
1084
- `# ${conversation.title}`,
1085
- '',
1086
- `- exported_from: \`${exportedFrom}\``,
1087
- `- conversation_id: \`${conversation.conversationId}\``,
1088
- conversation.workspaceUri ? `- workspace: \`${conversation.workspaceUri}\`` : '',
1365
+ renderDocumentTitle(conversation.title, options.outputFormat),
1089
1366
  '',
1367
+ options.includeMetadata
1368
+ ? renderAntigravityMetadata(
1369
+ buildAntigravityIdentityMetadata(conversation, exportedFrom),
1370
+ options.outputFormat,
1371
+ )
1372
+ : '',
1373
+ ...sections,
1090
1374
  ].filter(Boolean);
1091
1375
 
1092
- for (const entry of entries) {
1093
- parts.push(...renderLogEntry(entry));
1094
- }
1095
-
1096
1376
  return `${parts.join('\n').trimEnd()}\n`;
1097
1377
  };
1098
1378
 
1099
- const renderDecryptedSafeStorageMarkdown = (conversation: AntigravityConversation, content: string): string | null => {
1379
+ const renderDecryptedSafeStorage = (
1380
+ conversation: AntigravityConversation,
1381
+ content: string,
1382
+ options: ResolvedAntigravityConversationRenderOptions,
1383
+ ): string | null => {
1100
1384
  const trimmed = content.trim();
1101
1385
  if (!trimmed) {
1102
1386
  return null;
1103
1387
  }
1104
1388
 
1105
1389
  return [
1106
- `# ${conversation.title}`,
1107
- '',
1108
- '- exported_from: `antigravity_safe_storage_payload`',
1109
- `- conversation_id: \`${conversation.conversationId}\``,
1110
- conversation.workspaceUri ? `- workspace: \`${conversation.workspaceUri}\`` : '',
1390
+ renderDocumentTitle(conversation.title, options.outputFormat),
1111
1391
  '',
1392
+ options.includeMetadata
1393
+ ? renderAntigravityMetadata(
1394
+ buildAntigravityIdentityMetadata(conversation, 'antigravity_safe_storage_payload'),
1395
+ options.outputFormat,
1396
+ )
1397
+ : '',
1112
1398
  trimmed,
1113
1399
  '',
1114
1400
  ]
@@ -1116,22 +1402,66 @@ const renderDecryptedSafeStorageMarkdown = (conversation: AntigravityConversatio
1116
1402
  .join('\n');
1117
1403
  };
1118
1404
 
1405
+ const renderAntigravitySummary = (
1406
+ conversation: AntigravityConversation,
1407
+ options: ResolvedAntigravityConversationRenderOptions,
1408
+ ): string | null => {
1409
+ if (conversation.artifactCount > 0) {
1410
+ return null;
1411
+ }
1412
+
1413
+ if (!conversation.summaryPath && conversation.indexedItemCount === null) {
1414
+ return null;
1415
+ }
1416
+
1417
+ return [
1418
+ renderDocumentTitle(conversation.title, options.outputFormat),
1419
+ '',
1420
+ options.includeMetadata
1421
+ ? renderAntigravityMetadata(
1422
+ [
1423
+ ...buildAntigravityIdentityMetadata(conversation, 'antigravity_summary_index'),
1424
+ { key: 'indexed_items', value: conversation.indexedItemCount },
1425
+ {
1426
+ key: 'created_at',
1427
+ value: conversation.createdAtMs ? new Date(conversation.createdAtMs).toISOString() : null,
1428
+ },
1429
+ {
1430
+ key: 'updated_at',
1431
+ value: conversation.lastUpdatedAtMs
1432
+ ? new Date(conversation.lastUpdatedAtMs).toISOString()
1433
+ : null,
1434
+ },
1435
+ ],
1436
+ options.outputFormat,
1437
+ )
1438
+ : '',
1439
+ options.includeMetadata
1440
+ ? 'No local transcript log was found for this Antigravity conversation. The export contains the summary index metadata that Antigravity retained.'
1441
+ : 'No local transcript log was found for this Antigravity conversation.',
1442
+ '',
1443
+ ]
1444
+ .filter(Boolean)
1445
+ .join('\n');
1446
+ };
1447
+
1119
1448
  export const renderAntigravityConversationMarkdown = async (
1120
1449
  conversation: AntigravityConversation,
1121
- options: { keychainSecret?: string | null } = {},
1450
+ options: AntigravityConversationRenderOptions = {},
1122
1451
  ): Promise<string | null> => {
1123
- const transcript = await renderAntigravityTranscriptMarkdown(conversation);
1452
+ const resolvedOptions = resolveAntigravityRenderOptions(options);
1453
+ const transcript = await renderAntigravityTranscript(conversation, resolvedOptions);
1124
1454
  if (transcript) {
1125
1455
  return transcript;
1126
1456
  }
1127
1457
 
1128
- if (options.keychainSecret && conversation.conversationPath) {
1458
+ if (resolvedOptions.keychainSecret && conversation.conversationPath) {
1129
1459
  const encrypted = Buffer.from(await Bun.file(conversation.conversationPath).arrayBuffer());
1130
- const decrypted = decryptAntigravitySafeStoragePayload(encrypted, options.keychainSecret);
1460
+ const decrypted = decryptAntigravitySafeStoragePayload(encrypted, resolvedOptions.keychainSecret);
1131
1461
  if (decrypted) {
1132
- return renderDecryptedSafeStorageMarkdown(conversation, decrypted);
1462
+ return renderDecryptedSafeStorage(conversation, decrypted, resolvedOptions);
1133
1463
  }
1134
1464
  }
1135
1465
 
1136
- return null;
1466
+ return renderAntigravitySummary(conversation, resolvedOptions);
1137
1467
  };