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,8 +1,21 @@
1
- import { listAntigravityConversations, readAntigravityConversationMessages } from '../antigravity-db';
1
+ import {
2
+ deleteAntigravityConversation,
3
+ listAntigravityConversations,
4
+ readAntigravityConversationMessages,
5
+ } from '../antigravity-db';
2
6
  import type { AntigravityConversation } from '../antigravity-exporter-types';
3
7
  import { resolveAntigravityRoots } from '../antigravity-exporter-types';
8
+ import { mapWithConcurrency } from '../concurrency';
4
9
  import { cleanInlineTitle } from '../shared';
5
- import { createDeepLinks, decodeFileUri, finalizeMessages } from './adapter-helpers';
10
+ import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
11
+ import { getFileFingerprint, hashCacheKeyPartsIterable, withCachedJson } from '../ui-cache';
12
+ import {
13
+ createConversationUiPath,
14
+ createDeepLinks,
15
+ decodeFileUri,
16
+ finalizeMessages,
17
+ isWithinUpdatedWindow,
18
+ } from './adapter-helpers';
6
19
  import { selectConversationMessages } from './message-selector';
7
20
  import { getConversationPathMatch, getFirstConversationPathMatch } from './path-match';
8
21
  import type {
@@ -10,6 +23,7 @@ import type {
10
23
  ConversationDetail,
11
24
  ConversationMessage,
12
25
  ConversationPathMatch,
26
+ DeleteConversationOptions,
13
27
  GetConversationOptions,
14
28
  ListConversationsForPathOptions,
15
29
  } from './types';
@@ -21,18 +35,33 @@ const getWorkspacePath = (conversation: AntigravityConversation) =>
21
35
  conversation.workspaceFolder ?? decodeFileUri(conversation.workspaceUri);
22
36
 
23
37
  const stripTrailingPathPunctuation = (value: string) => value.replace(/[),.;:\]`]+$/u, '');
38
+ const PATH_REFERENCE_FALLBACK_LIMIT = 100;
39
+ const PATH_REFERENCE_CONCURRENCY = 4;
24
40
 
25
41
  const extractAbsolutePathReferences = (text: string): string[] => {
26
42
  return [...new Set((text.match(/\/[^\s"'`)\]]+/gu) ?? []).map(stripTrailingPathPunctuation))];
27
43
  };
28
44
 
29
45
  const readMessages = async (conversation: AntigravityConversation) => {
30
- const messages = await readAntigravityConversationMessages(conversation);
46
+ const load = () =>
47
+ runWithTranscriptLoadLimit(() => readAntigravityConversationMessages(conversation), {
48
+ id: conversation.conversationId,
49
+ path: conversation.transcriptPath ?? conversation.conversationPath ?? undefined,
50
+ source: 'antigravity-api',
51
+ });
52
+ const transcriptPath = conversation.transcriptPath;
53
+ const messages = transcriptPath
54
+ ? await getFileFingerprint(transcriptPath)
55
+ .then((fingerprint) =>
56
+ withCachedJson(`antigravity-api-messages-${hashCacheKeyPartsIterable([fingerprint])}`, load),
57
+ )
58
+ .catch(load)
59
+ : await load();
31
60
  return finalizeMessages(
32
61
  messages.map(
33
- (message): ConversationMessage => ({
62
+ (message, entryIndex): ConversationMessage => ({
34
63
  ...message,
35
- id: `${conversation.conversationId}:${message.order}:${message.role}:${message.phase}`,
64
+ id: `${conversation.conversationId}:${message.order}:${message.role}:${message.phase}:${entryIndex}`,
36
65
  metadata: {
37
66
  ...message.metadata,
38
67
  model: conversation.model,
@@ -60,7 +89,7 @@ const buildConversation = async (
60
89
  deepLinks: createDeepLinks(
61
90
  'antigravity',
62
91
  conversation.conversationId,
63
- `/antigravity-conversations/${conversation.conversationId}`,
92
+ createConversationUiPath('antigravity-conversations', conversation.conversationId),
64
93
  ),
65
94
  id: conversation.conversationId,
66
95
  matches,
@@ -80,20 +109,6 @@ const buildConversation = async (
80
109
  };
81
110
  };
82
111
 
83
- const isWithinUpdatedWindow = (
84
- conversation: AntigravityConversation,
85
- options: Pick<ListConversationsForPathOptions, 'updatedAfterMs' | 'updatedBeforeMs'>,
86
- ) => {
87
- const updatedAtMs = conversation.lastUpdatedAtMs ?? conversation.conversationMtimeMs ?? 0;
88
- if (options.updatedAfterMs !== undefined && updatedAtMs < options.updatedAfterMs) {
89
- return false;
90
- }
91
- if (options.updatedBeforeMs !== undefined && updatedAtMs > options.updatedBeforeMs) {
92
- return false;
93
- }
94
- return true;
95
- };
96
-
97
112
  const getReferencedPathMatch = async (
98
113
  requestedPath: string,
99
114
  messages: ConversationMessage[],
@@ -106,9 +121,10 @@ const listAntigravityConversationsForPath = async (options: ListConversationsFor
106
121
  const roots = getRoots(options);
107
122
  const conversations = await listAntigravityConversations(roots);
108
123
  const result: ConversationDetail[] = [];
124
+ const pathReferenceCandidates: AntigravityConversation[] = [];
109
125
 
110
126
  for (const conversation of conversations) {
111
- if (!isWithinUpdatedWindow(conversation, options)) {
127
+ if (!isWithinUpdatedWindow(conversation.lastUpdatedAtMs ?? conversation.conversationMtimeMs, options)) {
112
128
  continue;
113
129
  }
114
130
 
@@ -119,13 +135,32 @@ const listAntigravityConversationsForPath = async (options: ListConversationsFor
119
135
  continue;
120
136
  }
121
137
 
122
- const messages = await readMessages(conversation);
123
- const referencedPathMatch = await getReferencedPathMatch(options.cwd, messages);
124
- if (referencedPathMatch) {
125
- result.push(await buildConversation(conversation, [referencedPathMatch], options, messages));
126
- }
138
+ pathReferenceCandidates.push(conversation);
127
139
  }
128
140
 
141
+ const referencedConversations = await mapWithConcurrency(
142
+ pathReferenceCandidates.slice(0, PATH_REFERENCE_FALLBACK_LIMIT),
143
+ PATH_REFERENCE_CONCURRENCY,
144
+ async (conversation) => {
145
+ let messages: ConversationMessage[];
146
+ try {
147
+ messages = await readMessages(conversation);
148
+ } catch (error) {
149
+ console.warn('[spiracha:antigravity] skipped unreadable path-reference transcript', {
150
+ conversationId: conversation.conversationId,
151
+ error: error instanceof Error ? error.message : String(error),
152
+ });
153
+ return null;
154
+ }
155
+ const referencedPathMatch = await getReferencedPathMatch(options.cwd, messages);
156
+ if (referencedPathMatch) {
157
+ return buildConversation(conversation, [referencedPathMatch], options, messages);
158
+ }
159
+ return null;
160
+ },
161
+ );
162
+ result.push(...referencedConversations.flatMap((conversation) => (conversation ? [conversation] : [])));
163
+
129
164
  return result;
130
165
  };
131
166
 
@@ -141,7 +176,16 @@ const getAntigravityConversation = async (options: GetConversationOptions): Prom
141
176
  : null;
142
177
  };
143
178
 
179
+ const deleteAntigravityConversationById = async (options: DeleteConversationOptions) => {
180
+ const result = await deleteAntigravityConversation(getRoots(options), options.id);
181
+ return {
182
+ deletedFiles: result.deletedPaths,
183
+ deletedIds: result.deletedConversationIds,
184
+ };
185
+ };
186
+
144
187
  export const antigravityConversationAdapter: ConversationAdapter = {
188
+ deleteConversation: deleteAntigravityConversationById,
145
189
  getConversation: getAntigravityConversation,
146
190
  listConversationsForPath: listAntigravityConversationsForPath,
147
191
  source: 'antigravity',
@@ -1,5 +1,6 @@
1
1
  import {
2
- listClaudeCodeSessionsForGroup,
2
+ deleteClaudeCodeSession,
3
+ listClaudeCodeSessionTranscriptsForGroup,
3
4
  listClaudeCodeWorkspaceGroups,
4
5
  readClaudeCodeSessionTranscript,
5
6
  } from '../claude-code-db';
@@ -10,11 +11,15 @@ import type {
10
11
  ClaudeCodeTranscriptPart,
11
12
  } from '../claude-code-exporter-types';
12
13
  import { getClaudeCodeAssistantMessagePhase, resolveClaudeCodeProjectsDir } from '../claude-code-exporter-types';
14
+ import { mapWithConcurrency } from '../concurrency';
13
15
  import { cleanInlineTitle } from '../shared';
16
+ import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
14
17
  import {
18
+ createConversationUiPath,
15
19
  createDeepLinks,
16
20
  createTextMessage,
17
21
  finalizeMessages,
22
+ isWithinUpdatedWindow,
18
23
  normalizeAssistantPhase,
19
24
  normalizeRole,
20
25
  toDateMs,
@@ -26,10 +31,13 @@ import type {
26
31
  ConversationDetail,
27
32
  ConversationMessage,
28
33
  ConversationPathMatch,
34
+ DeleteConversationOptions,
29
35
  GetConversationOptions,
30
36
  ListConversationsForPathOptions,
31
37
  } from './types';
32
38
 
39
+ const CLAUDE_CONVERSATION_HYDRATION_CONCURRENCY = 4;
40
+
33
41
  const getProjectsDir = (options: { locations?: { claudeCodeProjectsDir?: string } }) =>
34
42
  options.locations?.claudeCodeProjectsDir ?? resolveClaudeCodeProjectsDir();
35
43
 
@@ -86,6 +94,19 @@ const partToMessages = (
86
94
  });
87
95
  }
88
96
 
97
+ if (part.type === 'attachment') {
98
+ const attachmentLabel = part.attachmentType?.trim() || 'file';
99
+ return createTextMessage({
100
+ createdAtMs,
101
+ id: baseId,
102
+ metadata: { attachmentType: part.attachmentType ?? null },
103
+ order: partIndex,
104
+ phase: 'unknown',
105
+ role: normalizeRole(entry.role),
106
+ text: part.text?.trim() || `[Attachment: ${attachmentLabel}]`,
107
+ });
108
+ }
109
+
89
110
  return [];
90
111
  };
91
112
 
@@ -102,9 +123,15 @@ const buildConversation = async (
102
123
  projectsDir: string,
103
124
  matches: ConversationPathMatch[],
104
125
  options: Pick<ListConversationsForPathOptions, 'includeMessages' | 'messageSelector'>,
126
+ loadedTranscript: ClaudeCodeSessionTranscript | null = null,
105
127
  ): Promise<ConversationDetail> => {
106
128
  const transcript = options.includeMessages
107
- ? await readClaudeCodeSessionTranscript(projectsDir, session.sessionId)
129
+ ? (loadedTranscript ??
130
+ (await runWithTranscriptLoadLimit(() => readClaudeCodeSessionTranscript(projectsDir, session.sessionId), {
131
+ id: session.sessionId,
132
+ path: session.filePath,
133
+ source: 'claude-code-api',
134
+ })))
108
135
  : null;
109
136
  const allMessages = transcript ? transcriptToMessages(transcript) : [];
110
137
  const messages = options.includeMessages
@@ -113,7 +140,11 @@ const buildConversation = async (
113
140
 
114
141
  return {
115
142
  createdAtMs: session.createdAtMs,
116
- deepLinks: createDeepLinks('claude-code', session.sessionId, `/claude-code-sessions/${session.sessionId}`),
143
+ deepLinks: createDeepLinks(
144
+ 'claude-code',
145
+ session.sessionId,
146
+ createConversationUiPath('claude-code-sessions', session.sessionId),
147
+ ),
117
148
  id: session.sessionId,
118
149
  matches,
119
150
  messageCount: options.includeMessages ? allMessages.length : session.messageCount,
@@ -146,10 +177,14 @@ const listClaudeConversationsForPath = async (
146
177
  continue;
147
178
  }
148
179
 
149
- const sessions = await listClaudeCodeSessionsForGroup(group.key, projectsDir);
150
- for (const session of sessions) {
151
- conversations.push(await buildConversation(session, projectsDir, [match], options));
152
- }
180
+ const transcripts = (await listClaudeCodeSessionTranscriptsForGroup(group.key, projectsDir)).filter(
181
+ (transcript) => isWithinUpdatedWindow(transcript.session.lastActiveAtMs, options),
182
+ );
183
+ conversations.push(
184
+ ...(await mapWithConcurrency(transcripts, CLAUDE_CONVERSATION_HYDRATION_CONCURRENCY, (transcript) =>
185
+ buildConversation(transcript.session, projectsDir, [match], options, transcript),
186
+ )),
187
+ );
153
188
  }
154
189
 
155
190
  return conversations;
@@ -157,18 +192,40 @@ const listClaudeConversationsForPath = async (
157
192
 
158
193
  const getClaudeConversation = async (options: GetConversationOptions): Promise<ConversationDetail | null> => {
159
194
  const projectsDir = getProjectsDir(options);
160
- const transcript = await readClaudeCodeSessionTranscript(projectsDir, options.id);
195
+ const transcript = await runWithTranscriptLoadLimit(
196
+ () => readClaudeCodeSessionTranscript(projectsDir, options.id),
197
+ {
198
+ id: options.id,
199
+ path: projectsDir,
200
+ source: 'claude-code-api',
201
+ },
202
+ );
161
203
  if (!transcript) {
162
204
  return null;
163
205
  }
164
206
 
165
- return buildConversation(transcript.session, projectsDir, [], {
166
- includeMessages: true,
167
- messageSelector: options.messageSelector ?? 'all',
168
- });
207
+ return buildConversation(
208
+ transcript.session,
209
+ projectsDir,
210
+ [],
211
+ {
212
+ includeMessages: true,
213
+ messageSelector: options.messageSelector ?? 'all',
214
+ },
215
+ transcript,
216
+ );
217
+ };
218
+
219
+ const deleteClaudeConversation = async (options: DeleteConversationOptions) => {
220
+ const result = await deleteClaudeCodeSession(getProjectsDir(options), options.id);
221
+ return {
222
+ deletedFiles: result.deletedFiles,
223
+ deletedIds: result.deletedSessionIds,
224
+ };
169
225
  };
170
226
 
171
227
  export const claudeCodeConversationAdapter: ConversationAdapter = {
228
+ deleteConversation: deleteClaudeConversation,
172
229
  getConversation: getClaudeConversation,
173
230
  listConversationsForPath: listClaudeConversationsForPath,
174
231
  source: 'claude-code',
@@ -1,17 +1,33 @@
1
- import { getThreadBrowseData, listScopedThreads, resolveCodexThreadDbPath } from '../codex-browser-db';
1
+ import {
2
+ CodexThreadNotFoundError,
3
+ deleteCodexThread,
4
+ getThreadBrowseData,
5
+ listScopedThreads,
6
+ resolveCodexThreadDbPath,
7
+ } from '../codex-browser-db';
2
8
  import type { MessageEvent, ThreadBrowseData, ThreadEvent } from '../codex-browser-types';
3
9
  import { parseCodexTranscriptFile } from '../codex-thread-parser';
4
10
  import type { ThreadRow } from '../codex-thread-types';
5
11
  import { cleanInlineTitle } from '../shared';
12
+ import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
13
+ import {
14
+ createConversationUiPath,
15
+ createDeepLinks,
16
+ createTextMessage,
17
+ finalizeMessages,
18
+ isWithinUpdatedWindow,
19
+ normalizeAssistantPhase,
20
+ normalizeRole,
21
+ toDateMs,
22
+ } from './adapter-helpers';
6
23
  import { selectConversationMessages } from './message-selector';
7
24
  import { getConversationPathMatch } from './path-match';
8
25
  import type {
9
26
  ConversationAdapter,
10
27
  ConversationDetail,
11
28
  ConversationMessage,
12
- ConversationMessagePhase,
13
- ConversationMessageRole,
14
29
  ConversationPathMatch,
30
+ DeleteConversationOptions,
15
31
  GetConversationOptions,
16
32
  ListConversationsForPathOptions,
17
33
  } from './types';
@@ -28,36 +44,6 @@ const toCreatedAtMs = (thread: ThreadRow) => {
28
44
  return thread.created_at_ms ?? thread.created_at * 1000;
29
45
  };
30
46
 
31
- const buildCodexDeepLinks = (id: string) => ({
32
- native: `codex://threads/${id}`,
33
- spiracha: `spiracha://conversation/codex/${id}`,
34
- ui: `/threads/${id}`,
35
- });
36
-
37
- const normalizeRole = (role: string): ConversationMessageRole => {
38
- if (role === 'assistant' || role === 'user' || role === 'system') {
39
- return role;
40
- }
41
-
42
- return 'unknown';
43
- };
44
-
45
- const normalizeMessagePhase = (event: MessageEvent): ConversationMessagePhase => {
46
- if (event.role !== 'assistant') {
47
- return 'unknown';
48
- }
49
-
50
- if (event.phase === 'final_answer' || event.phase === 'final') {
51
- return 'final_answer';
52
- }
53
-
54
- if (event.phase === 'commentary' || event.isHiddenByDefault) {
55
- return 'commentary';
56
- }
57
-
58
- return 'final_answer';
59
- };
60
-
61
47
  const toMessageEventMessage = (event: MessageEvent): ConversationMessage | null => {
62
48
  const text = event.text.trim();
63
49
  if (!text) {
@@ -65,14 +51,17 @@ const toMessageEventMessage = (event: MessageEvent): ConversationMessage | null
65
51
  }
66
52
 
67
53
  return {
68
- createdAtMs: event.timestamp ? Date.parse(event.timestamp) : null,
54
+ createdAtMs: toDateMs(event.timestamp),
69
55
  id: `codex:${event.sequence}`,
70
56
  metadata: {
71
57
  model: event.model,
72
58
  variant: event.variant,
73
59
  },
74
60
  order: event.sequence,
75
- phase: normalizeMessagePhase(event),
61
+ phase:
62
+ event.role === 'assistant'
63
+ ? normalizeAssistantPhase(event.isHiddenByDefault ? 'commentary' : event.phase, 'unknown')
64
+ : 'unknown',
76
65
  role: normalizeRole(event.role),
77
66
  text,
78
67
  };
@@ -80,37 +69,41 @@ const toMessageEventMessage = (event: MessageEvent): ConversationMessage | null
80
69
 
81
70
  const toToolMessage = (event: ThreadEvent): ConversationMessage | null => {
82
71
  if (event.kind === 'tool_call') {
83
- return {
84
- createdAtMs: event.timestamp ? Date.parse(event.timestamp) : null,
85
- id: `codex:${event.sequence}`,
86
- metadata: {
87
- callId: event.callId,
88
- command: event.command,
89
- name: event.name,
90
- workdir: event.workdir,
91
- },
92
- order: event.sequence,
93
- phase: 'tool_call',
94
- role: 'tool',
95
- text: event.command ?? event.name,
96
- };
72
+ return (
73
+ createTextMessage({
74
+ createdAtMs: toDateMs(event.timestamp),
75
+ id: `codex:${event.sequence}`,
76
+ metadata: {
77
+ callId: event.callId,
78
+ command: event.command,
79
+ name: event.name,
80
+ workdir: event.workdir,
81
+ },
82
+ order: event.sequence,
83
+ phase: 'tool_call',
84
+ role: 'tool',
85
+ text: event.command || event.name,
86
+ })[0] ?? null
87
+ );
97
88
  }
98
89
 
99
90
  if (event.kind === 'tool_output') {
100
91
  const text = event.summary || event.outputText;
101
- return {
102
- createdAtMs: event.timestamp ? Date.parse(event.timestamp) : null,
103
- id: `codex:${event.sequence}`,
104
- metadata: {
105
- callId: event.callId,
106
- exitCode: event.exitCode,
107
- wallTime: event.wallTime,
108
- },
109
- order: event.sequence,
110
- phase: 'tool_output',
111
- role: 'tool',
112
- text,
113
- };
92
+ return (
93
+ createTextMessage({
94
+ createdAtMs: toDateMs(event.timestamp),
95
+ id: `codex:${event.sequence}`,
96
+ metadata: {
97
+ callId: event.callId,
98
+ exitCode: event.exitCode,
99
+ wallTime: event.wallTime,
100
+ },
101
+ order: event.sequence,
102
+ phase: 'tool_output',
103
+ role: 'tool',
104
+ text,
105
+ })[0] ?? null
106
+ );
114
107
  }
115
108
 
116
109
  return null;
@@ -118,6 +111,10 @@ const toToolMessage = (event: ThreadEvent): ConversationMessage | null => {
118
111
 
119
112
  const toConversationMessage = (event: ThreadEvent): ConversationMessage | null => {
120
113
  if (event.kind === 'message') {
114
+ if (event.isHiddenByDefault) {
115
+ return null;
116
+ }
117
+
121
118
  return toMessageEventMessage(event);
122
119
  }
123
120
 
@@ -125,7 +122,7 @@ const toConversationMessage = (event: ThreadEvent): ConversationMessage | null =
125
122
  const text = event.summary.join('\n').trim();
126
123
  return text
127
124
  ? {
128
- createdAtMs: event.timestamp ? Date.parse(event.timestamp) : null,
125
+ createdAtMs: toDateMs(event.timestamp),
129
126
  id: `codex:${event.sequence}`,
130
127
  metadata: {
131
128
  hasEncryptedContent: event.hasEncryptedContent,
@@ -142,14 +139,33 @@ const toConversationMessage = (event: ThreadEvent): ConversationMessage | null =
142
139
  };
143
140
 
144
141
  const readCodexMessages = async (thread: ThreadRow): Promise<ConversationMessage[]> => {
145
- const transcript = await parseCodexTranscriptFile(thread.rollout_path, {
146
- includeRaw: false,
147
- });
142
+ let transcript: Awaited<ReturnType<typeof parseCodexTranscriptFile>>;
143
+ try {
144
+ transcript = await runWithTranscriptLoadLimit(
145
+ () =>
146
+ parseCodexTranscriptFile(thread.rollout_path, {
147
+ includeRaw: false,
148
+ }),
149
+ {
150
+ id: thread.id,
151
+ path: thread.rollout_path,
152
+ source: 'codex-api',
153
+ },
154
+ );
155
+ } catch (error) {
156
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
157
+ return [];
158
+ }
148
159
 
149
- return transcript.events.flatMap((event) => {
150
- const message = toConversationMessage(event);
151
- return message ? [message] : [];
152
- });
160
+ throw error;
161
+ }
162
+
163
+ return finalizeMessages(
164
+ transcript.events.flatMap((event) => {
165
+ const message = toConversationMessage(event);
166
+ return message ? [message] : [];
167
+ }),
168
+ );
153
169
  };
154
170
 
155
171
  const buildCodexConversation = async (
@@ -164,7 +180,12 @@ const buildCodexConversation = async (
164
180
 
165
181
  return {
166
182
  createdAtMs: toCreatedAtMs(thread),
167
- deepLinks: buildCodexDeepLinks(thread.id),
183
+ deepLinks: createDeepLinks(
184
+ 'codex',
185
+ thread.id,
186
+ createConversationUiPath('threads', thread.id),
187
+ `codex://threads/${encodeURIComponent(thread.id)}`,
188
+ ),
168
189
  id: thread.id,
169
190
  matches,
170
191
  messageCount: options.includeMessages ? allMessages.length : null,
@@ -206,8 +227,13 @@ const listCodexConversationsForPath = async (
206
227
  options: ListConversationsForPathOptions,
207
228
  ): Promise<ConversationDetail[]> => {
208
229
  const dbPath = getCodexDbPath(options);
230
+ if (!(await Bun.file(dbPath).exists())) {
231
+ return [];
232
+ }
209
233
  const threads = listScopedThreads(dbPath, null);
210
- const matchedThreads = await filterThreadsForPath(threads, options.cwd);
234
+ const matchedThreads = (await filterThreadsForPath(threads, options.cwd)).filter(({ thread }) =>
235
+ isWithinUpdatedWindow(toTimestampMs(thread), options),
236
+ );
211
237
 
212
238
  return Promise.all(
213
239
  matchedThreads.map(({ matches, thread }) =>
@@ -225,7 +251,7 @@ const getCodexConversation = async (options: GetConversationOptions): Promise<Co
225
251
  try {
226
252
  browseData = getThreadBrowseData(dbPath, options.id);
227
253
  } catch (error) {
228
- if (error instanceof Error && /not found/i.test(error.message)) {
254
+ if (error instanceof CodexThreadNotFoundError) {
229
255
  return null;
230
256
  }
231
257
 
@@ -238,7 +264,16 @@ const getCodexConversation = async (options: GetConversationOptions): Promise<Co
238
264
  });
239
265
  };
240
266
 
267
+ const deleteCodexConversation = async (options: DeleteConversationOptions) => {
268
+ const result = await deleteCodexThread(getCodexDbPath(options), options.id, { deleteSessionFiles: true });
269
+ return {
270
+ deletedFiles: result.deletedSessionFiles,
271
+ deletedIds: result.deletedThreadIds,
272
+ };
273
+ };
274
+
241
275
  export const codexConversationAdapter: ConversationAdapter = {
276
+ deleteConversation: deleteCodexConversation,
242
277
  getConversation: getCodexConversation,
243
278
  listConversationsForPath: listCodexConversationsForPath,
244
279
  source: 'codex',