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,16 +1,20 @@
1
1
  import type { ThreadEvent } from '@spiracha/lib/codex-browser-types';
2
+ import { shouldShowCodexTranscriptEvent } from '@spiracha/lib/codex-transcript-filter';
2
3
  import { useVirtualizer } from '@tanstack/react-virtual';
3
4
  import { Check, Copy } from 'lucide-react';
4
- import { useEffect, useMemo, useRef, useState } from 'react';
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
6
  import { Badge } from '#/components/ui/badge';
6
7
  import { Button } from '#/components/ui/button';
7
8
  import { Checkbox } from '#/components/ui/checkbox';
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '#/components/ui/select';
8
10
  import { formatDateTime, formatModelLabel } from '#/lib/formatters';
9
11
  import { applyPathTransforms } from '#/lib/path-utils';
10
12
  import { useSettings } from '#/lib/settings-store';
11
13
  import { cn } from '#/lib/utils';
12
14
 
13
15
  type TranscriptViewProps = {
16
+ activeEventJumpSignal?: number;
17
+ activeEventKey?: string | null;
14
18
  assistantModel: string | null;
15
19
  events: ThreadEvent[];
16
20
  projectPath: string | null;
@@ -19,36 +23,27 @@ type TranscriptViewProps = {
19
23
  showRawJson: boolean;
20
24
  showToolCalls: boolean;
21
25
  showUserMessages?: boolean;
26
+ sortOrder?: TranscriptSortOrder;
27
+ onSortOrderChange?: (value: TranscriptSortOrder) => void;
22
28
  };
23
29
 
24
- const isCommentaryMessage = (event: ThreadEvent) =>
25
- event.kind === 'message' && event.role === 'assistant' && event.phase === 'commentary';
30
+ export type TranscriptSortOrder = 'earliest' | 'latest';
26
31
 
27
- const shouldShowEvent = (
32
+ export const DEFAULT_SHOW_USER_MESSAGES = false;
33
+
34
+ export const shouldShowEvent = (
28
35
  event: ThreadEvent,
29
36
  showToolCalls: boolean,
30
37
  showExtraEvents: boolean,
31
38
  showCommentary: boolean,
32
39
  showUserMessages: boolean,
33
- ) => {
34
- if (isCommentaryMessage(event) && !showCommentary) {
35
- return false;
36
- }
37
-
38
- if (event.kind === 'message') {
39
- if (event.role === 'user' && !showUserMessages) {
40
- return false;
41
- }
42
-
43
- return !event.isHiddenByDefault || showExtraEvents;
44
- }
45
-
46
- if (event.kind === 'tool_call' || event.kind === 'tool_output') {
47
- return showToolCalls;
48
- }
49
-
50
- return showExtraEvents;
51
- };
40
+ ) =>
41
+ shouldShowCodexTranscriptEvent(event, {
42
+ showCommentary,
43
+ showExtraEvents,
44
+ showToolCalls,
45
+ showUserMessages,
46
+ });
52
47
 
53
48
  const getEventTone = (event: ThreadEvent) => {
54
49
  if (event.kind === 'message' && event.role === 'assistant') {
@@ -153,7 +148,7 @@ const getEventMarkdownBody = (event: ThreadEvent, transform: Transform) => {
153
148
  const getEventMarkdown = (event: ThreadEvent, assistantModel: string | null, transform: Transform) =>
154
149
  `## ${getEventTitle(event, assistantModel)}\n\n${getEventMarkdownBody(event, transform)}`;
155
150
 
156
- const buildEventKey = (event: ThreadEvent, index: number) => {
151
+ export const getTranscriptEventKey = (event: ThreadEvent, index: number) => {
157
152
  if (event.kind === 'tool_call') {
158
153
  return `${event.kind}-${event.sequence}-${event.callId ?? event.timestamp ?? event.name}-${index}`;
159
154
  }
@@ -248,36 +243,81 @@ type TranscriptEventCardProps = {
248
243
  assistantModel: string | null;
249
244
  copied: boolean;
250
245
  event: ThreadEvent;
246
+ eventKey: string;
247
+ isActive: boolean;
251
248
  isSelected: boolean;
252
249
  showRawJson: boolean;
253
250
  transform: Transform;
251
+ onCardElement: (eventKey: string, element: HTMLElement | null) => void;
254
252
  onCopy: (event: ThreadEvent) => void;
255
253
  onSelectionChange: (event: ThreadEvent, checked: boolean) => void;
256
254
  };
257
255
 
256
+ function TranscriptSortSelect({
257
+ sortOrder,
258
+ onSortOrderChange,
259
+ }: {
260
+ sortOrder: TranscriptSortOrder;
261
+ onSortOrderChange?: (value: TranscriptSortOrder) => void;
262
+ }) {
263
+ return (
264
+ <div className="flex items-center gap-2 text-sm">
265
+ <label className="text-[var(--muted-foreground)]" htmlFor="transcript-sort-order">
266
+ Sort by
267
+ </label>
268
+ <Select value={sortOrder} onValueChange={(value) => onSortOrderChange?.(value as TranscriptSortOrder)}>
269
+ <SelectTrigger
270
+ aria-label="Sort transcript messages"
271
+ className="h-8 w-[10.5rem] rounded-full border-[var(--border)] bg-[var(--panel)] text-xs"
272
+ id="transcript-sort-order"
273
+ >
274
+ <SelectValue />
275
+ </SelectTrigger>
276
+ <SelectContent className="border-[var(--border)] bg-[var(--panel)] text-[var(--foreground)]">
277
+ <SelectItem value="earliest">Earliest first</SelectItem>
278
+ <SelectItem value="latest">Latest first</SelectItem>
279
+ </SelectContent>
280
+ </Select>
281
+ </div>
282
+ );
283
+ }
284
+
258
285
  function TranscriptEventCard({
259
286
  assistantModel,
260
287
  copied,
261
288
  event,
289
+ eventKey,
290
+ isActive,
262
291
  isSelected,
263
292
  showRawJson,
264
293
  transform,
294
+ onCardElement,
265
295
  onCopy,
266
296
  onSelectionChange,
267
297
  }: TranscriptEventCardProps) {
298
+ const handleElement = useCallback(
299
+ (element: HTMLElement | null) => onCardElement(eventKey, element),
300
+ [eventKey, onCardElement],
301
+ );
302
+
268
303
  return (
269
304
  <article
305
+ ref={handleElement}
306
+ aria-current={isActive ? 'location' : undefined}
270
307
  className={cn(
271
- 'min-w-0 overflow-hidden rounded-xl border p-3.5 shadow-[var(--panel-shadow)]',
308
+ 'min-w-0 scroll-mt-24 overflow-hidden rounded-xl border p-3.5 shadow-[var(--panel-shadow)]',
272
309
  isSelected && 'ring-2 ring-[var(--accent)]/35',
310
+ isActive && 'ring-2 ring-[var(--accent)]',
273
311
  getEventTone(event),
274
312
  )}
313
+ data-transcript-event-key={eventKey}
275
314
  >
276
315
  <div className="flex flex-wrap items-start justify-between gap-2">
277
316
  <div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
278
317
  <Checkbox
279
318
  aria-label={`Select ${getEventTitle(event, assistantModel)}`}
280
319
  checked={isSelected}
320
+ className="size-5 cursor-pointer"
281
321
  onCheckedChange={(checked) => onSelectionChange(event, checked === true)}
282
322
  />
283
323
  <h3 className="min-w-0 break-words font-semibold text-sm [overflow-wrap:anywhere]">
@@ -316,6 +356,8 @@ function TranscriptEventCard({
316
356
  }
317
357
 
318
358
  export function TranscriptView({
359
+ activeEventJumpSignal = 0,
360
+ activeEventKey = null,
319
361
  assistantModel,
320
362
  events,
321
363
  projectPath,
@@ -324,33 +366,45 @@ export function TranscriptView({
324
366
  showRawJson,
325
367
  showToolCalls,
326
368
  showUserMessages = true,
369
+ sortOrder = 'earliest',
370
+ onSortOrderChange,
327
371
  }: TranscriptViewProps) {
328
372
  const { settings } = useSettings();
329
- const visibleEvents = useMemo(
373
+ const filteredEvents = useMemo(
330
374
  () =>
331
375
  events.filter((event) =>
332
376
  shouldShowEvent(event, showToolCalls, showExtraEvents, showCommentary, showUserMessages),
333
377
  ),
334
378
  [events, showCommentary, showExtraEvents, showToolCalls, showUserMessages],
335
379
  );
380
+ const visibleEvents = useMemo(
381
+ () => (sortOrder === 'latest' ? [...filteredEvents].reverse() : filteredEvents),
382
+ [filteredEvents, sortOrder],
383
+ );
336
384
  const [copiedEventKeys, setCopiedEventKeys] = useState<string[]>([]);
337
385
  const [copiedSelection, setCopiedSelection] = useState(false);
338
386
  const [copyErrorMessage, setCopyErrorMessage] = useState<string | null>(null);
339
387
  const [selectedEventKeys, setSelectedEventKeys] = useState<string[]>([]);
388
+ const eventElementByKeyRef = useRef(new Map<string, HTMLElement>());
389
+ const lastHandledJumpRef = useRef<{ eventKey: string; signal: number } | null>(null);
340
390
  const parentRef = useRef<HTMLDivElement | null>(null);
341
391
  const timeoutIdsRef = useRef<number[]>([]);
342
392
  const useVirtualList = visibleEvents.length > 40;
343
393
  const eventKeyByEvent = useMemo(
344
- () => new Map(events.map((event, index) => [event, buildEventKey(event, index)])),
394
+ () => new Map(events.map((event, index) => [event, getTranscriptEventKey(event, index)])),
345
395
  [events],
346
396
  );
347
- const getEventKey = (event: ThreadEvent) => eventKeyByEvent.get(event) ?? buildEventKey(event, -1);
397
+ const getEventKey = (event: ThreadEvent) => eventKeyByEvent.get(event) ?? getTranscriptEventKey(event, -1);
348
398
  const transform = (text: string) => applyPathTransforms(text, settings, projectPath);
349
399
  const visibleEventKeys = useMemo(
350
- () => visibleEvents.map((event) => eventKeyByEvent.get(event) ?? buildEventKey(event, -1)),
400
+ () => visibleEvents.map((event) => eventKeyByEvent.get(event) ?? getTranscriptEventKey(event, -1)),
351
401
  [visibleEvents, eventKeyByEvent],
352
402
  );
353
403
  const visibleEventKeySet = useMemo(() => new Set(visibleEventKeys), [visibleEventKeys]);
404
+ const activeVisibleEventIndex = useMemo(
405
+ () => (activeEventKey ? visibleEventKeys.indexOf(activeEventKey) : -1),
406
+ [activeEventKey, visibleEventKeys],
407
+ );
354
408
  const selectedEventKeySet = useMemo(() => new Set(selectedEventKeys), [selectedEventKeys]);
355
409
  const virtualizer = useVirtualizer({
356
410
  count: useVirtualList ? visibleEvents.length : 0,
@@ -361,6 +415,15 @@ export function TranscriptView({
361
415
  overscan: 8,
362
416
  });
363
417
 
418
+ const handleCardElement = useCallback((eventKey: string, element: HTMLElement | null) => {
419
+ if (element) {
420
+ eventElementByKeyRef.current.set(eventKey, element);
421
+ return;
422
+ }
423
+
424
+ eventElementByKeyRef.current.delete(eventKey);
425
+ }, []);
426
+
364
427
  useEffect(() => {
365
428
  return () => {
366
429
  for (const timeoutId of timeoutIdsRef.current) {
@@ -377,6 +440,27 @@ export function TranscriptView({
377
440
  });
378
441
  }, [visibleEventKeySet]);
379
442
 
443
+ useEffect(() => {
444
+ if (!Number.isFinite(activeEventJumpSignal) || !activeEventKey || activeVisibleEventIndex < 0) {
445
+ return;
446
+ }
447
+
448
+ if (
449
+ lastHandledJumpRef.current?.signal === activeEventJumpSignal &&
450
+ lastHandledJumpRef.current.eventKey === activeEventKey
451
+ ) {
452
+ return;
453
+ }
454
+ lastHandledJumpRef.current = { eventKey: activeEventKey, signal: activeEventJumpSignal };
455
+
456
+ if (useVirtualList) {
457
+ virtualizer.scrollToIndex(activeVisibleEventIndex, { align: 'start' });
458
+ return;
459
+ }
460
+
461
+ eventElementByKeyRef.current.get(activeEventKey)?.scrollIntoView({ behavior: 'auto', block: 'start' });
462
+ }, [activeEventJumpSignal, activeEventKey, activeVisibleEventIndex, useVirtualList, virtualizer]);
463
+
380
464
  const scheduleTimeout = (callback: () => void, delayMs: number) => {
381
465
  const timeoutId = window.setTimeout(() => {
382
466
  timeoutIdsRef.current = timeoutIdsRef.current.filter((entry) => entry !== timeoutId);
@@ -468,17 +552,20 @@ export function TranscriptView({
468
552
  <span className="text-[var(--destructive)] text-sm">{copyErrorMessage}</span>
469
553
  ) : null}
470
554
  </div>
471
- <Button
472
- aria-label="Copy selected messages"
473
- className="hover:bg-[var(--panel-secondary)] hover:text-[var(--foreground)]"
474
- disabled={visibleSelectedKeys.length === 0}
475
- size="sm"
476
- variant="outline"
477
- onClick={() => void handleCopySelected()}
478
- >
479
- {copiedSelection ? <Check className="text-[var(--accent)]" /> : <Copy />}
480
- {copiedSelection ? 'Copied' : 'Copy'}
481
- </Button>
555
+ <div className="flex items-center gap-2">
556
+ <TranscriptSortSelect sortOrder={sortOrder} onSortOrderChange={onSortOrderChange} />
557
+ <Button
558
+ aria-label="Copy selected messages"
559
+ className="hover:bg-[var(--panel-secondary)] hover:text-[var(--foreground)]"
560
+ disabled={visibleSelectedKeys.length === 0}
561
+ size="sm"
562
+ variant="outline"
563
+ onClick={() => void handleCopySelected()}
564
+ >
565
+ {copiedSelection ? <Check className="text-[var(--accent)]" /> : <Copy />}
566
+ {copiedSelection ? 'Copied' : 'Copy'}
567
+ </Button>
568
+ </div>
482
569
  </div>
483
570
  <div className="relative w-full" style={{ height: `${virtualizer.getTotalSize()}px` }}>
484
571
  {virtualizer.getVirtualItems().map((item) => (
@@ -493,9 +580,12 @@ export function TranscriptView({
493
580
  assistantModel={assistantModel}
494
581
  copied={copiedEventKeys.includes(getEventKey(visibleEvents[item.index]!))}
495
582
  event={visibleEvents[item.index]!}
583
+ eventKey={getEventKey(visibleEvents[item.index]!)}
584
+ isActive={activeEventKey === getEventKey(visibleEvents[item.index]!)}
496
585
  isSelected={selectedEventKeySet.has(getEventKey(visibleEvents[item.index]!))}
497
586
  showRawJson={showRawJson}
498
587
  transform={transform}
588
+ onCardElement={handleCardElement}
499
589
  onCopy={(event) => void handleCopyEvent(event)}
500
590
  onSelectionChange={handleSelectionChange}
501
591
  />
@@ -522,27 +612,33 @@ export function TranscriptView({
522
612
  <span className="text-[var(--destructive)] text-sm">{copyErrorMessage}</span>
523
613
  ) : null}
524
614
  </div>
525
- <Button
526
- aria-label="Copy selected messages"
527
- className="hover:bg-[var(--panel-secondary)] hover:text-[var(--foreground)]"
528
- disabled={visibleSelectedKeys.length === 0}
529
- size="sm"
530
- variant="outline"
531
- onClick={() => void handleCopySelected()}
532
- >
533
- {copiedSelection ? <Check className="text-[var(--accent)]" /> : <Copy />}
534
- {copiedSelection ? 'Copied' : 'Copy'}
535
- </Button>
615
+ <div className="flex items-center gap-2">
616
+ <TranscriptSortSelect sortOrder={sortOrder} onSortOrderChange={onSortOrderChange} />
617
+ <Button
618
+ aria-label="Copy selected messages"
619
+ className="hover:bg-[var(--panel-secondary)] hover:text-[var(--foreground)]"
620
+ disabled={visibleSelectedKeys.length === 0}
621
+ size="sm"
622
+ variant="outline"
623
+ onClick={() => void handleCopySelected()}
624
+ >
625
+ {copiedSelection ? <Check className="text-[var(--accent)]" /> : <Copy />}
626
+ {copiedSelection ? 'Copied' : 'Copy'}
627
+ </Button>
628
+ </div>
536
629
  </div>
537
630
  {visibleEvents.map((event) => (
538
631
  <TranscriptEventCard
539
632
  assistantModel={assistantModel}
540
633
  copied={copiedEventKeys.includes(getEventKey(event))}
541
- key={getEventKey(event)}
542
634
  event={event}
635
+ eventKey={getEventKey(event)}
636
+ key={getEventKey(event)}
637
+ isActive={activeEventKey === getEventKey(event)}
543
638
  isSelected={selectedEventKeySet.has(getEventKey(event))}
544
639
  showRawJson={showRawJson}
545
640
  transform={transform}
641
+ onCardElement={handleCardElement}
546
642
  onCopy={(selectedEvent) => void handleCopyEvent(selectedEvent)}
547
643
  onSelectionChange={handleSelectionChange}
548
644
  />
@@ -94,7 +94,7 @@ function SelectItem({ className, children, ...props }: React.ComponentProps<type
94
94
  <SelectPrimitive.Item
95
95
  data-slot="select-item"
96
96
  className={cn(
97
- "relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
97
+ "relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden focus:bg-[var(--accent-muted)] focus:text-[var(--foreground)] data-[disabled]:pointer-events-none data-[highlighted]:bg-[var(--accent-muted)] data-[highlighted]:text-[var(--foreground)] data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-[var(--muted-foreground)] [&_svg]:pointer-events-none [&_svg]:shrink-0 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
98
98
  className,
99
99
  )}
100
100
  {...props}
@@ -10,6 +10,14 @@ export const hasEncryptedAntigravityConversation = (conversation: AntigravityCon
10
10
  return conversation.transcriptSource === 'safe-storage';
11
11
  };
12
12
 
13
+ export const hasSummaryAntigravityConversation = (conversation: AntigravityConversation): boolean => {
14
+ return (
15
+ conversation.artifactCount === 0 &&
16
+ conversation.transcriptSource === null &&
17
+ (conversation.summaryPath !== null || conversation.indexedItemCount !== null)
18
+ );
19
+ };
20
+
13
21
  export const isAntigravityConversationLocked = (
14
22
  conversation: AntigravityConversation,
15
23
  hasKeychainSecret: boolean,
@@ -21,8 +29,13 @@ export const canExportAntigravityConversation = (
21
29
  conversation: AntigravityConversation,
22
30
  hasKeychainSecret: boolean,
23
31
  ): boolean => {
24
- return (
25
- hasReadableAntigravityConversation(conversation) ||
26
- (hasEncryptedAntigravityConversation(conversation) && hasKeychainSecret)
27
- );
32
+ if (hasReadableAntigravityConversation(conversation)) {
33
+ return true;
34
+ }
35
+
36
+ if (hasEncryptedAntigravityConversation(conversation)) {
37
+ return hasKeychainSecret;
38
+ }
39
+
40
+ return hasSummaryAntigravityConversation(conversation);
28
41
  };