spiracha 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/AGENTS.md +27 -12
  2. package/README.md +42 -19
  3. package/apps/ui/AGENTS.md +18 -10
  4. package/apps/ui/README.md +36 -11
  5. package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
  6. package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
  7. package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
  8. package/apps/ui/src/components/app-shell.tsx +46 -3
  9. package/apps/ui/src/components/breadcrumbs.tsx +21 -3
  10. package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
  11. package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
  12. package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
  13. package/apps/ui/src/components/data-table.tsx +23 -5
  14. package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
  15. package/apps/ui/src/components/export-dialog.tsx +79 -74
  16. package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
  17. package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
  18. package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
  19. package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
  20. package/apps/ui/src/components/page-header.tsx +15 -9
  21. package/apps/ui/src/components/projects-table.tsx +4 -1
  22. package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
  23. package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
  24. package/apps/ui/src/components/route-error-panel.tsx +13 -0
  25. package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
  26. package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
  27. package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
  28. package/apps/ui/src/components/threads-table.tsx +13 -47
  29. package/apps/ui/src/components/transcript-view.tsx +148 -52
  30. package/apps/ui/src/components/ui/select.tsx +1 -1
  31. package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
  32. package/apps/ui/src/lib/antigravity-server.ts +209 -11
  33. package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
  34. package/apps/ui/src/lib/claude-code-queries.ts +8 -0
  35. package/apps/ui/src/lib/claude-code-server.ts +185 -7
  36. package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
  37. package/apps/ui/src/lib/codex-queries.ts +16 -0
  38. package/apps/ui/src/lib/codex-server.ts +134 -52
  39. package/apps/ui/src/lib/cursor-server.ts +68 -53
  40. package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
  41. package/apps/ui/src/lib/delete-batch.ts +52 -0
  42. package/apps/ui/src/lib/error-presentation.ts +43 -0
  43. package/apps/ui/src/lib/export-mutation.ts +20 -0
  44. package/apps/ui/src/lib/export-options.ts +15 -0
  45. package/apps/ui/src/lib/formatters.ts +3 -5
  46. package/apps/ui/src/lib/grok-queries.ts +22 -0
  47. package/apps/ui/src/lib/grok-server.ts +168 -0
  48. package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
  49. package/apps/ui/src/lib/kiro-server.ts +97 -7
  50. package/apps/ui/src/lib/mutation-error.ts +15 -0
  51. package/apps/ui/src/lib/opencode-server.ts +96 -7
  52. package/apps/ui/src/lib/path-utils.ts +1 -1
  53. package/apps/ui/src/lib/qoder-queries.ts +15 -0
  54. package/apps/ui/src/lib/qoder-server.ts +67 -11
  55. package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
  56. package/apps/ui/src/lib/route-search.ts +114 -0
  57. package/apps/ui/src/lib/route-state-reset.tsx +10 -0
  58. package/apps/ui/src/lib/settings-server.ts +33 -0
  59. package/apps/ui/src/lib/settings-store.tsx +82 -38
  60. package/apps/ui/src/lib/settings.ts +65 -0
  61. package/apps/ui/src/lib/source-session-export-server.ts +86 -3
  62. package/apps/ui/src/lib/thread-metadata.ts +22 -0
  63. package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
  64. package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
  65. package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
  66. package/apps/ui/src/routeTree.gen.ts +341 -234
  67. package/apps/ui/src/routes/__root.tsx +12 -15
  68. package/apps/ui/src/routes/analytics.tsx +10 -61
  69. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +149 -29
  70. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
  71. package/apps/ui/src/routes/antigravity.index.tsx +2 -2
  72. package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
  73. package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
  74. package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
  75. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +168 -28
  76. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
  77. package/apps/ui/src/routes/claude-code.index.tsx +2 -2
  78. package/apps/ui/src/routes/codex.$project.tsx +33 -31
  79. package/apps/ui/src/routes/codex.index.tsx +5 -10
  80. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +31 -18
  81. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
  82. package/apps/ui/src/routes/cursor.index.tsx +5 -2
  83. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
  84. package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
  85. package/apps/ui/src/routes/grok.index.tsx +48 -0
  86. package/apps/ui/src/routes/index.tsx +2 -18
  87. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
  88. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
  89. package/apps/ui/src/routes/kiro.index.tsx +2 -2
  90. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
  91. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
  92. package/apps/ui/src/routes/opencode.index.tsx +6 -2
  93. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
  94. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
  95. package/apps/ui/src/routes/qoder.index.tsx +2 -2
  96. package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
  97. package/apps/ui/vite.config.ts +22 -12
  98. package/bin/spiracha.ts +28 -1
  99. package/package.json +37 -27
  100. package/src/client.ts +183 -5
  101. package/src/lib/antigravity-db.ts +424 -94
  102. package/src/lib/antigravity-exporter-types.ts +3 -0
  103. package/src/lib/antigravity-keychain.ts +7 -8
  104. package/src/lib/antigravity-projects.ts +201 -0
  105. package/src/lib/antigravity-transcript-phase.ts +47 -0
  106. package/src/lib/claude-code-db.ts +534 -42
  107. package/src/lib/claude-code-exporter-types.ts +5 -0
  108. package/src/lib/claude-code-transcript-phase.ts +60 -1
  109. package/src/lib/claude-code-transcript.ts +8 -5
  110. package/src/lib/codex-analytics.ts +32 -5
  111. package/src/lib/codex-browser-db.ts +540 -137
  112. package/src/lib/codex-browser-export.ts +18 -27
  113. package/src/lib/codex-browser-types.ts +21 -1
  114. package/src/lib/codex-thread-cache.ts +106 -23
  115. package/src/lib/codex-thread-parser.ts +149 -55
  116. package/src/lib/codex-thread-recovery.ts +38 -11
  117. package/src/lib/codex-transcript-filter.ts +31 -0
  118. package/src/lib/codex-transcript-renderer.ts +87 -79
  119. package/src/lib/concurrency.ts +56 -3
  120. package/src/lib/conversation-api.ts +410 -27
  121. package/src/lib/conversation-data/adapter-helpers.ts +25 -3
  122. package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
  123. package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
  124. package/src/lib/conversation-data/codex-adapter.ts +108 -73
  125. package/src/lib/conversation-data/cursor-adapter.ts +77 -29
  126. package/src/lib/conversation-data/grok-adapter.ts +223 -0
  127. package/src/lib/conversation-data/index.ts +105 -11
  128. package/src/lib/conversation-data/kiro-adapter.ts +59 -12
  129. package/src/lib/conversation-data/message-selector.ts +2 -5
  130. package/src/lib/conversation-data/opencode-adapter.ts +67 -17
  131. package/src/lib/conversation-data/path-match.ts +1 -13
  132. package/src/lib/conversation-data/qoder-adapter.ts +67 -37
  133. package/src/lib/conversation-data/types.ts +43 -0
  134. package/src/lib/conversation-zip-export.ts +75 -0
  135. package/src/lib/cursor-db.ts +68 -28
  136. package/src/lib/cursor-id.ts +17 -0
  137. package/src/lib/cursor-recovery.ts +146 -35
  138. package/src/lib/cursor-transcript-phase.ts +40 -0
  139. package/src/lib/cursor-transcript.ts +21 -5
  140. package/src/lib/grok-db.ts +1068 -0
  141. package/src/lib/grok-exporter-types.ts +110 -0
  142. package/src/lib/grok-transcript-phase.ts +61 -0
  143. package/src/lib/grok-transcript.ts +167 -0
  144. package/src/lib/kiro-db.ts +187 -43
  145. package/src/lib/kiro-transcript.ts +0 -4
  146. package/src/lib/model-label.ts +11 -3
  147. package/src/lib/opencode-db.ts +599 -60
  148. package/src/lib/opencode-think-tags.ts +17 -3
  149. package/src/lib/opencode-transcript.ts +8 -5
  150. package/src/lib/portable-path.ts +9 -0
  151. package/src/lib/qoder-acp-client.ts +11 -2
  152. package/src/lib/qoder-db.ts +51 -17
  153. package/src/lib/qoder-transcript-phase.ts +64 -0
  154. package/src/lib/qoder-transcript.ts +30 -2
  155. package/src/lib/shared.ts +63 -15
  156. package/src/lib/sqlite-error.ts +31 -5
  157. package/src/lib/transcript-load-limiter.ts +87 -0
  158. package/src/lib/ui-cache.ts +185 -24
  159. package/src/lib/ui-export-archive.ts +58 -39
  160. package/src/lib/ui-export-files.ts +75 -15
  161. package/src/lib/ui-export-zip.ts +40 -0
  162. package/apps/ui/package.json +0 -65
@@ -1,6 +1,11 @@
1
1
  import { createServerFn } from '@tanstack/react-start';
2
2
  import { z } from 'zod';
3
- import { renderSourceSessionDownload } from './source-session-export-server';
3
+ import { requireDeletedItems, runDeleteBatch } from './delete-batch';
4
+ import { renderSourceSessionDownload, renderSourceSessionsDownload } from './source-session-export-server';
5
+
6
+ const LARGE_CLAUDE_CODE_SESSION_SIZE_BYTES = 8 * 1024 * 1024;
7
+ const CLAUDE_CODE_DETAIL_PREVIEW_ENTRY_LIMIT = 400;
8
+ const CLAUDE_CODE_DETAIL_PREVIEW_LEADING_ENTRY_LIMIT = 100;
4
9
 
5
10
  const workspaceSchema = z.object({
6
11
  workspaceKey: z.string().min(1),
@@ -19,6 +24,19 @@ const exportSessionSchema = z.object({
19
24
  zipArchive: z.boolean().default(false),
20
25
  });
21
26
 
27
+ const exportSessionsSchema = z.object({
28
+ includeCommentary: z.boolean().default(true),
29
+ includeMetadata: z.boolean().default(true),
30
+ includeTools: z.boolean().default(true),
31
+ outputFormat: z.enum(['md', 'txt']).default('md'),
32
+ sessionIds: z.array(z.string().min(1)).min(1),
33
+ zipArchive: z.boolean().default(true),
34
+ });
35
+
36
+ const deleteSessionsSchema = z.object({
37
+ sessionIds: z.array(z.string().min(1)).min(1),
38
+ });
39
+
22
40
  export const listClaudeCodeWorkspacesFn = createServerFn({ method: 'GET' }).handler(async () => {
23
41
  const { listClaudeCodeWorkspaceGroups } = await import('@spiracha/lib/claude-code-db');
24
42
  return listClaudeCodeWorkspaceGroups();
@@ -32,21 +50,114 @@ export const listClaudeCodeSessionsFn = createServerFn({ method: 'GET' })
32
50
  });
33
51
 
34
52
  const loadClaudeCodeSessionTranscript = async (sessionId: string) => {
53
+ const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
35
54
  const { readClaudeCodeSessionTranscript, resolveClaudeCodeProjectsDir } = await import(
36
55
  '@spiracha/lib/claude-code-db'
37
56
  );
38
- const transcript = await readClaudeCodeSessionTranscript(resolveClaudeCodeProjectsDir(), sessionId);
39
- if (!transcript) {
40
- throw new Error(`Claude Code session not found: ${sessionId}`);
57
+ const projectsDir = resolveClaudeCodeProjectsDir();
58
+ return runWithTranscriptLoadLimit(
59
+ async () => {
60
+ const transcript = await readClaudeCodeSessionTranscript(projectsDir, sessionId, {
61
+ includeRawPayloads: false,
62
+ });
63
+ if (!transcript) {
64
+ throw new Error(`Claude Code session not found: ${sessionId}`);
65
+ }
66
+
67
+ return transcript;
68
+ },
69
+ {
70
+ id: sessionId,
71
+ path: projectsDir,
72
+ source: 'claude-code-ui',
73
+ },
74
+ );
75
+ };
76
+
77
+ export const buildClaudeCodeSessionDetailPreview = (
78
+ transcript: Awaited<ReturnType<typeof loadClaudeCodeSessionTranscript>>,
79
+ ) => {
80
+ if (transcript.entries.length <= CLAUDE_CODE_DETAIL_PREVIEW_ENTRY_LIMIT) {
81
+ return transcript;
41
82
  }
42
83
 
43
- return transcript;
84
+ const trailingEntryLimit = CLAUDE_CODE_DETAIL_PREVIEW_ENTRY_LIMIT - CLAUDE_CODE_DETAIL_PREVIEW_LEADING_ENTRY_LIMIT;
85
+ return {
86
+ ...transcript,
87
+ entries: [
88
+ ...transcript.entries.slice(0, CLAUDE_CODE_DETAIL_PREVIEW_LEADING_ENTRY_LIMIT),
89
+ ...transcript.entries.slice(-trailingEntryLimit),
90
+ ].map((entry) => ({
91
+ ...entry,
92
+ parts: entry.parts.map((part) => ({ ...part, raw: {} })),
93
+ raw: {},
94
+ })),
95
+ isPartial: true,
96
+ omittedEntryCount: transcript.entries.length - CLAUDE_CODE_DETAIL_PREVIEW_ENTRY_LIMIT,
97
+ rawEvents: [],
98
+ rawPayloadsOmitted: true,
99
+ };
100
+ };
101
+
102
+ export const loadClaudeCodeSessionDetail = async (sessionId: string) => {
103
+ const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
104
+ const { readClaudeCodeSessionTranscript, resolveClaudeCodeProjectsDir } = await import(
105
+ '@spiracha/lib/claude-code-db'
106
+ );
107
+ const projectsDir = resolveClaudeCodeProjectsDir();
108
+ return runWithTranscriptLoadLimit(
109
+ async () => {
110
+ const transcript = await readClaudeCodeSessionTranscript(projectsDir, sessionId, {
111
+ maxRawPayloadFileSizeBytes: LARGE_CLAUDE_CODE_SESSION_SIZE_BYTES,
112
+ });
113
+ if (!transcript) {
114
+ throw new Error(`Claude Code session not found: ${sessionId}`);
115
+ }
116
+
117
+ return buildClaudeCodeSessionDetailPreview(transcript);
118
+ },
119
+ {
120
+ id: sessionId,
121
+ path: projectsDir,
122
+ source: 'claude-code-ui-detail',
123
+ },
124
+ );
125
+ };
126
+
127
+ export const loadClaudeCodeSessionFullDetail = async (sessionId: string) => {
128
+ const { runWithTranscriptLoadLimit } = await import('@spiracha/lib/transcript-load-limiter');
129
+ const { readClaudeCodeSessionTranscript, resolveClaudeCodeProjectsDir } = await import(
130
+ '@spiracha/lib/claude-code-db'
131
+ );
132
+ const projectsDir = resolveClaudeCodeProjectsDir();
133
+ return runWithTranscriptLoadLimit(
134
+ async () => {
135
+ const transcript = await readClaudeCodeSessionTranscript(projectsDir, sessionId, {
136
+ includeRawPayloads: false,
137
+ });
138
+ if (!transcript) {
139
+ throw new Error(`Claude Code session not found: ${sessionId}`);
140
+ }
141
+ return transcript;
142
+ },
143
+ {
144
+ id: sessionId,
145
+ path: projectsDir,
146
+ source: 'claude-code-ui-full-detail',
147
+ },
148
+ );
44
149
  };
45
150
 
46
151
  export const getClaudeCodeSessionDetailFn = createServerFn({ method: 'GET' })
47
152
  .validator(sessionSchema)
48
153
  .handler(async ({ data }) => {
49
- return loadClaudeCodeSessionTranscript(data.sessionId);
154
+ return loadClaudeCodeSessionDetail(data.sessionId);
155
+ });
156
+
157
+ export const getClaudeCodeSessionTranscriptFn = createServerFn({ method: 'GET' })
158
+ .validator(sessionSchema)
159
+ .handler(async ({ data }) => {
160
+ return loadClaudeCodeSessionFullDetail(data.sessionId);
50
161
  });
51
162
 
52
163
  export const exportClaudeCodeSessionFn = createServerFn({ method: 'POST' })
@@ -67,9 +178,76 @@ export const exportClaudeCodeSessionFn = createServerFn({ method: 'POST' })
67
178
 
68
179
  return renderSourceSessionDownload({
69
180
  content,
181
+ cwd: transcript.session.cwd ?? transcript.session.worktree,
70
182
  fallbackBaseName: 'claude-code-session',
71
- fileBaseName: transcript.session.title || transcript.session.sessionId,
72
183
  outputFormat: data.outputFormat,
184
+ sessionId: transcript.session.sessionId,
185
+ updatedAtMs: transcript.session.lastActiveAtMs,
73
186
  zipArchive: data.zipArchive,
74
187
  });
75
188
  });
189
+
190
+ export const exportClaudeCodeSessionsFn = createServerFn({ method: 'POST' })
191
+ .validator(exportSessionsSchema)
192
+ .handler(async ({ data }) => {
193
+ const { renderClaudeCodeTranscript } = await import('@spiracha/lib/claude-code-transcript');
194
+ const entries = await Promise.all(
195
+ data.sessionIds.map(async (sessionId) => {
196
+ const transcript = await loadClaudeCodeSessionTranscript(sessionId);
197
+ const content = renderClaudeCodeTranscript(transcript, {
198
+ includeCommentary: data.includeCommentary,
199
+ includeMetadata: data.includeMetadata,
200
+ includeTools: data.includeTools,
201
+ outputFormat: data.outputFormat,
202
+ });
203
+
204
+ if (!content) {
205
+ throw new Error(`Claude Code session has no exportable content: ${sessionId}`);
206
+ }
207
+
208
+ return {
209
+ content,
210
+ cwd: transcript.session.cwd ?? transcript.session.worktree,
211
+ fallbackBaseName: 'claude-code-session',
212
+ fileBaseName: transcript.session.title || transcript.session.sessionId,
213
+ sessionId: transcript.session.sessionId,
214
+ updatedAtMs: transcript.session.lastActiveAtMs,
215
+ };
216
+ }),
217
+ );
218
+
219
+ return renderSourceSessionsDownload({
220
+ entries,
221
+ fallbackBaseName: 'claude-code-sessions',
222
+ outputFormat: data.outputFormat,
223
+ zipArchive: data.zipArchive,
224
+ });
225
+ });
226
+
227
+ export const deleteClaudeCodeSessionFn = createServerFn({ method: 'POST' })
228
+ .validator(sessionSchema)
229
+ .handler(async ({ data }) => {
230
+ const { deleteClaudeCodeSession, resolveClaudeCodeProjectsDir } = await import('@spiracha/lib/claude-code-db');
231
+ const result = await deleteClaudeCodeSession(resolveClaudeCodeProjectsDir(), data.sessionId);
232
+ requireDeletedItems(result.deletedSessionIds, 'Claude Code session', data.sessionId);
233
+ return result;
234
+ });
235
+
236
+ export const deleteClaudeCodeSessionsFn = createServerFn({ method: 'POST' })
237
+ .validator(deleteSessionsSchema)
238
+ .handler(async ({ data }) => {
239
+ const { deleteClaudeCodeSession, resolveClaudeCodeProjectsDir } = await import('@spiracha/lib/claude-code-db');
240
+ const projectsDir = resolveClaudeCodeProjectsDir();
241
+ const results = await runDeleteBatch(data.sessionIds, (sessionId) =>
242
+ deleteClaudeCodeSession(projectsDir, sessionId),
243
+ );
244
+ requireDeletedItems(
245
+ results.flatMap((result) => result.deletedSessionIds),
246
+ 'Claude Code sessions',
247
+ 'batch',
248
+ );
249
+ return {
250
+ deletedFiles: [...new Set(results.flatMap((result) => result.deletedFiles))],
251
+ deletedSessionIds: [...new Set(results.flatMap((result) => result.deletedSessionIds))],
252
+ };
253
+ });
@@ -3,7 +3,10 @@ import type {
3
3
  ClaudeCodeTranscriptEntry,
4
4
  ClaudeCodeTranscriptPart,
5
5
  } from '@spiracha/lib/claude-code-exporter-types';
6
- import { getClaudeCodeAssistantMessagePhase } from '@spiracha/lib/claude-code-transcript-phase';
6
+ import {
7
+ getClaudeCodeAssistantMessagePhase,
8
+ isClaudeCodeSyntheticTranscriptEntry,
9
+ } from '@spiracha/lib/claude-code-transcript-phase';
7
10
  import type { ThreadEvent, ThreadTranscriptStats } from '@spiracha/lib/codex-browser-types';
8
11
  import type { JsonValue } from '@spiracha/lib/shared';
9
12
  import { getThreadTranscriptStats } from './thread-transcript-stats';
@@ -141,6 +144,10 @@ export const claudeCodeTranscriptToThreadEvents = (transcript: ClaudeCodeSession
141
144
  const events: ThreadEvent[] = [];
142
145
  let sequence = 0;
143
146
  for (const entry of transcript.entries) {
147
+ if (isClaudeCodeSyntheticTranscriptEntry(entry)) {
148
+ continue;
149
+ }
150
+
144
151
  for (const part of entry.parts) {
145
152
  events.push(...partToEvents(entry, part, sequence));
146
153
  sequence += 1;
@@ -5,10 +5,18 @@ import {
5
5
  getDashboardSummaryFn,
6
6
  getThreadSnapshotFn,
7
7
  getThreadTranscriptFn,
8
+ getThreadTranscriptPreviewFn,
8
9
  listProjectsFn,
9
10
  listProjectThreadsFn,
10
11
  } from './codex-server';
11
12
 
13
+ type ThreadTranscriptFilters = {
14
+ showCommentary: boolean;
15
+ showExtraEvents: boolean;
16
+ showToolCalls: boolean;
17
+ showUserMessages: boolean;
18
+ };
19
+
12
20
  const retrySqliteQuery = (failureCount: number, error: unknown) => {
13
21
  return failureCount < 3 && isRetryableSqliteError(error);
14
22
  };
@@ -49,6 +57,14 @@ export const threadSnapshotQueryOptions = (threadId: string) =>
49
57
  retryDelay,
50
58
  });
51
59
 
60
+ export const threadTranscriptPreviewQueryOptions = (threadId: string, filters?: ThreadTranscriptFilters) =>
61
+ queryOptions({
62
+ queryFn: () => getThreadTranscriptPreviewFn({ data: { filters, threadId } }),
63
+ queryKey: ['thread-transcript-preview', threadId, filters ?? 'all'],
64
+ retry: retrySqliteQuery,
65
+ retryDelay,
66
+ });
67
+
52
68
  export const threadTranscriptQueryOptions = (threadId: string) =>
53
69
  queryOptions({
54
70
  queryFn: () => getThreadTranscriptFn({ data: { threadId } }),
@@ -1,21 +1,4 @@
1
- import { getCodexAnalytics } from '@spiracha/lib/codex-analytics';
2
- import {
3
- deleteCodexProject,
4
- deleteCodexThread,
5
- deleteCodexThreads,
6
- getCodexDashboardSummary,
7
- getThreadBrowseData,
8
- listCodexProjects,
9
- listProjectThreads,
10
- resolveCodexThreadDbPath,
11
- } from '@spiracha/lib/codex-browser-db';
12
- import { renderCodexThreadDownload, renderCodexThreadsDownload } from '@spiracha/lib/codex-browser-export';
13
- import {
14
- getCachedParsedCodexTranscript,
15
- getCachedThreadTranscriptPreview,
16
- getThreadRolloutLoadState,
17
- } from '@spiracha/lib/codex-thread-cache';
18
- import { recoverCodexProjectThreads } from '@spiracha/lib/codex-thread-recovery';
1
+ import type { ParsedCodexTranscript } from '@spiracha/lib/codex-browser-types';
19
2
  import { createServerFn } from '@tanstack/react-start';
20
3
  import { z } from 'zod';
21
4
 
@@ -32,6 +15,18 @@ const threadSchema = z.object({
32
15
  threadId: z.string().min(1),
33
16
  });
34
17
 
18
+ const transcriptFiltersSchema = z.object({
19
+ showCommentary: z.boolean(),
20
+ showExtraEvents: z.boolean(),
21
+ showToolCalls: z.boolean(),
22
+ showUserMessages: z.boolean(),
23
+ });
24
+
25
+ const threadPreviewSchema = z.object({
26
+ filters: transcriptFiltersSchema.optional(),
27
+ threadId: z.string().min(1),
28
+ });
29
+
35
30
  const deleteThreadSchema = z.object({
36
31
  deleteSessionFiles: z.boolean().default(false),
37
32
  threadId: z.string().min(1),
@@ -68,24 +63,39 @@ const exportThreadsSchema = z.object({
68
63
  zipArchive: z.boolean().default(true),
69
64
  });
70
65
 
71
- const getDbPath = () => process.env.SPIRACHA_CODEX_DB?.trim() || resolveCodexThreadDbPath();
66
+ const getDbPath = async () => {
67
+ const configuredDbPath = process.env.SPIRACHA_CODEX_DB?.trim();
68
+ if (configuredDbPath) {
69
+ return configuredDbPath;
70
+ }
71
+
72
+ const { resolveCodexThreadDbPath } = await import('@spiracha/lib/codex-browser-db');
73
+ return resolveCodexThreadDbPath();
74
+ };
72
75
 
73
76
  const isMissingFileError = (error: unknown) => {
74
77
  return error instanceof Error && /ENOENT|no such file/i.test(error.message);
75
78
  };
76
79
 
80
+ const logCodexThreadLoad = (event: string, details: Record<string, unknown>) => {
81
+ console.info(`[spiracha:codex-thread] ${event}`, details);
82
+ };
83
+
77
84
  export const getDashboardSummaryFn = createServerFn({ method: 'GET' }).handler(async () => {
78
- return getCodexDashboardSummary(getDbPath());
85
+ const { getCodexDashboardSummary } = await import('@spiracha/lib/codex-browser-db');
86
+ return getCodexDashboardSummary(await getDbPath());
79
87
  });
80
88
 
81
89
  export const listProjectsFn = createServerFn({ method: 'GET' }).handler(async () => {
82
- return listCodexProjects(getDbPath());
90
+ const { listCodexProjects } = await import('@spiracha/lib/codex-browser-db');
91
+ return listCodexProjects(await getDbPath());
83
92
  });
84
93
 
85
94
  export const listProjectThreadsFn = createServerFn({ method: 'GET' })
86
95
  .validator(projectSchema)
87
96
  .handler(async ({ data }) => {
88
- return listProjectThreads(getDbPath(), data.project, {
97
+ const { listProjectThreads } = await import('@spiracha/lib/codex-browser-db');
98
+ return listProjectThreads(await getDbPath(), data.project, {
89
99
  includeTranscriptStats: false,
90
100
  });
91
101
  });
@@ -93,51 +103,117 @@ export const listProjectThreadsFn = createServerFn({ method: 'GET' })
93
103
  export const getThreadSnapshotFn = createServerFn({ method: 'GET' })
94
104
  .validator(threadSchema)
95
105
  .handler(async ({ data }) => {
96
- const dbPath = getDbPath();
106
+ const startedAt = Date.now();
107
+ const [{ getThreadBrowseData }, { getThreadRolloutLoadState }] = await Promise.all([
108
+ import('@spiracha/lib/codex-browser-db'),
109
+ import('@spiracha/lib/codex-thread-cache'),
110
+ ]);
111
+ const dbPath = await getDbPath();
112
+ logCodexThreadLoad('snapshot_start', {
113
+ threadId: data.threadId,
114
+ });
97
115
  const browseData = getThreadBrowseData(dbPath, data.threadId);
98
- const rollout = await getThreadRolloutLoadState(browseData.thread.rollout_path);
99
- let transcript = null;
100
- let transcriptState: 'available' | 'deferred' | 'missing' = rollout.shouldDeferTranscriptLoad
101
- ? 'deferred'
102
- : 'available';
103
-
104
- if (!rollout.shouldDeferTranscriptLoad) {
105
- try {
106
- transcript = await getCachedParsedCodexTranscript(browseData.thread.rollout_path);
107
- } catch (error) {
108
- if (!isMissingFileError(error)) {
109
- throw error;
110
- }
111
-
112
- transcriptState = 'missing';
116
+ const transcript: ParsedCodexTranscript | null = null;
117
+ let rollout: Awaited<ReturnType<typeof getThreadRolloutLoadState>>;
118
+
119
+ try {
120
+ rollout = await getThreadRolloutLoadState(browseData.thread.rollout_path);
121
+ } catch (error) {
122
+ if (!isMissingFileError(error)) {
123
+ throw error;
113
124
  }
125
+
126
+ rollout = {
127
+ fileSizeBytes: null,
128
+ shouldDeferTranscriptLoad: false,
129
+ };
114
130
  }
115
131
 
132
+ const transcriptState: 'available' | 'deferred' | 'missing' =
133
+ rollout.fileSizeBytes === null ? 'missing' : rollout.shouldDeferTranscriptLoad ? 'deferred' : 'available';
134
+ logCodexThreadLoad('snapshot_ready', {
135
+ durationMs: Date.now() - startedAt,
136
+ fileSizeBytes: rollout.fileSizeBytes,
137
+ shouldDeferTranscriptLoad: rollout.shouldDeferTranscriptLoad,
138
+ threadId: data.threadId,
139
+ transcriptState,
140
+ });
141
+
116
142
  return {
117
143
  ...browseData,
118
- availableTools:
119
- browseData.dynamicTools.length > 0
120
- ? browseData.dynamicTools
121
- : (transcript?.sessionMeta.dynamicTools ?? []),
144
+ availableTools: browseData.dynamicTools,
122
145
  rollout,
123
146
  transcript,
124
147
  transcriptState,
125
148
  };
126
149
  });
127
150
 
151
+ export const loadThreadTranscriptPreview = async (
152
+ threadId: string,
153
+ filters?: z.infer<typeof transcriptFiltersSchema>,
154
+ ) => {
155
+ const startedAt = Date.now();
156
+ const [{ getThreadBrowseData }, { getCachedThreadTranscriptPreview }] = await Promise.all([
157
+ import('@spiracha/lib/codex-browser-db'),
158
+ import('@spiracha/lib/codex-thread-cache'),
159
+ ]);
160
+ const dbPath = await getDbPath();
161
+ const browseData = getThreadBrowseData(dbPath, threadId);
162
+ logCodexThreadLoad('preview_start', {
163
+ rolloutPath: browseData.thread.rollout_path,
164
+ threadId,
165
+ });
166
+ const transcript = await getCachedThreadTranscriptPreview(browseData.thread.rollout_path, {
167
+ filters,
168
+ });
169
+ logCodexThreadLoad('preview_ready', {
170
+ durationMs: Date.now() - startedAt,
171
+ eventCount: transcript.events.length,
172
+ isPartial: transcript.isPartial,
173
+ threadId,
174
+ });
175
+ return transcript;
176
+ };
177
+
178
+ export const loadThreadTranscript = async (threadId: string) => {
179
+ const [{ getThreadBrowseData }, { getCachedParsedCodexTranscript }] = await Promise.all([
180
+ import('@spiracha/lib/codex-browser-db'),
181
+ import('@spiracha/lib/codex-thread-cache'),
182
+ ]);
183
+ const dbPath = await getDbPath();
184
+ const browseData = getThreadBrowseData(dbPath, threadId);
185
+ const startedAt = Date.now();
186
+ logCodexThreadLoad('full_start', {
187
+ rolloutPath: browseData.thread.rollout_path,
188
+ threadId,
189
+ });
190
+ const transcript = await getCachedParsedCodexTranscript(browseData.thread.rollout_path);
191
+ logCodexThreadLoad('full_ready', {
192
+ durationMs: Date.now() - startedAt,
193
+ eventCount: transcript.events.length,
194
+ threadId,
195
+ });
196
+ return transcript;
197
+ };
198
+
199
+ export const getThreadTranscriptPreviewFn = createServerFn({ method: 'GET' })
200
+ .validator(threadPreviewSchema)
201
+ .handler(async ({ data }) => {
202
+ return loadThreadTranscriptPreview(data.threadId, data.filters);
203
+ });
204
+
128
205
  export const getThreadTranscriptFn = createServerFn({ method: 'GET' })
129
206
  .validator(threadSchema)
130
207
  .handler(async ({ data }) => {
131
- const dbPath = getDbPath();
132
- const browseData = getThreadBrowseData(dbPath, data.threadId);
133
- return getCachedThreadTranscriptPreview(browseData.thread.rollout_path);
208
+ return loadThreadTranscript(data.threadId);
134
209
  });
135
210
 
136
211
  export const getAnalyticsFn = createServerFn({ method: 'GET' })
137
212
  .validator(analyticsSchema)
138
213
  .handler(async ({ data }) => {
214
+ const { getCodexAnalytics } = await import('@spiracha/lib/codex-analytics');
139
215
  return getCodexAnalytics({
140
- dbPath: getDbPath(),
216
+ dbPath: await getDbPath(),
141
217
  project: data.project,
142
218
  });
143
219
  });
@@ -145,8 +221,9 @@ export const getAnalyticsFn = createServerFn({ method: 'GET' })
145
221
  export const exportThreadFn = createServerFn({ method: 'POST' })
146
222
  .validator(exportSchema)
147
223
  .handler(async ({ data }) => {
224
+ const { renderCodexThreadDownload } = await import('@spiracha/lib/codex-browser-export');
148
225
  return renderCodexThreadDownload({
149
- dbPath: getDbPath(),
226
+ dbPath: await getDbPath(),
150
227
  includeCommentary: data.includeCommentary,
151
228
  includeMetadata: data.includeMetadata,
152
229
  includeTools: data.includeTools,
@@ -163,8 +240,9 @@ export const exportThreadFn = createServerFn({ method: 'POST' })
163
240
  export const exportThreadsFn = createServerFn({ method: 'POST' })
164
241
  .validator(exportThreadsSchema)
165
242
  .handler(async ({ data }) => {
243
+ const { renderCodexThreadsDownload } = await import('@spiracha/lib/codex-browser-export');
166
244
  return renderCodexThreadsDownload({
167
- dbPath: getDbPath(),
245
+ dbPath: await getDbPath(),
168
246
  includeCommentary: data.includeCommentary,
169
247
  includeMetadata: data.includeMetadata,
170
248
  includeTools: data.includeTools,
@@ -181,7 +259,8 @@ export const exportThreadsFn = createServerFn({ method: 'POST' })
181
259
  export const deleteThreadFn = createServerFn({ method: 'POST' })
182
260
  .validator(deleteThreadSchema)
183
261
  .handler(async ({ data }) => {
184
- return deleteCodexThread(getDbPath(), data.threadId, {
262
+ const { deleteCodexThread } = await import('@spiracha/lib/codex-browser-db');
263
+ return deleteCodexThread(await getDbPath(), data.threadId, {
185
264
  deleteSessionFiles: data.deleteSessionFiles,
186
265
  });
187
266
  });
@@ -189,7 +268,8 @@ export const deleteThreadFn = createServerFn({ method: 'POST' })
189
268
  export const deleteThreadsFn = createServerFn({ method: 'POST' })
190
269
  .validator(deleteThreadsSchema)
191
270
  .handler(async ({ data }) => {
192
- return deleteCodexThreads(getDbPath(), data.threadIds, {
271
+ const { deleteCodexThreads } = await import('@spiracha/lib/codex-browser-db');
272
+ return deleteCodexThreads(await getDbPath(), data.threadIds, {
193
273
  deleteSessionFiles: data.deleteSessionFiles,
194
274
  });
195
275
  });
@@ -197,7 +277,8 @@ export const deleteThreadsFn = createServerFn({ method: 'POST' })
197
277
  export const deleteProjectFn = createServerFn({ method: 'POST' })
198
278
  .validator(deleteProjectSchema)
199
279
  .handler(async ({ data }) => {
200
- return deleteCodexProject(getDbPath(), data.project, {
280
+ const { deleteCodexProject } = await import('@spiracha/lib/codex-browser-db');
281
+ return deleteCodexProject(await getDbPath(), data.project, {
201
282
  deleteSessionFiles: data.deleteSessionFiles,
202
283
  });
203
284
  });
@@ -205,5 +286,6 @@ export const deleteProjectFn = createServerFn({ method: 'POST' })
205
286
  export const recoverProjectThreadsFn = createServerFn({ method: 'POST' })
206
287
  .validator(projectSchema)
207
288
  .handler(async ({ data }) => {
208
- return recoverCodexProjectThreads(getDbPath(), data.project);
289
+ const { recoverCodexProjectThreads } = await import('@spiracha/lib/codex-thread-recovery');
290
+ return recoverCodexProjectThreads(await getDbPath(), data.project);
209
291
  });