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,5 +1,6 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
+ import type { ClaudeCodeAssistantMessagePhase } from './claude-code-transcript-phase';
3
4
  import type { ExportFormat, JsonValue } from './shared';
4
5
 
5
6
  export type { ClaudeCodeAssistantMessagePhase } from './claude-code-transcript-phase';
@@ -84,6 +85,7 @@ export type ClaudeCodeTranscriptPart = {
84
85
  };
85
86
 
86
87
  export type ClaudeCodeTranscriptEntry = {
88
+ assistantPhase?: ClaudeCodeAssistantMessagePhase | null;
87
89
  cwd: string | null;
88
90
  entryId: string;
89
91
  model?: string | null;
@@ -97,6 +99,9 @@ export type ClaudeCodeTranscriptEntry = {
97
99
 
98
100
  export type ClaudeCodeSessionTranscript = {
99
101
  entries: ClaudeCodeTranscriptEntry[];
102
+ isPartial?: boolean;
103
+ omittedEntryCount?: number;
104
+ rawPayloadsOmitted?: boolean;
100
105
  rawEvents: Record<string, JsonValue>[];
101
106
  renderablePartCount: number;
102
107
  session: ClaudeCodeSessionSummary;
@@ -3,16 +3,39 @@ import type { JsonValue } from './shared';
3
3
  export type ClaudeCodeAssistantMessagePhase = 'commentary' | 'final_answer';
4
4
 
5
5
  type ClaudeCodeAssistantPhaseEntry = {
6
+ assistantPhase?: ClaudeCodeAssistantMessagePhase | null;
7
+ parts?: Array<{
8
+ text?: string;
9
+ type: string;
10
+ }>;
6
11
  raw: Record<string, JsonValue>;
7
12
  role: string;
8
13
  };
9
14
 
15
+ type ClaudeCodeFilterEntry = ClaudeCodeAssistantPhaseEntry & {
16
+ parts: Array<{
17
+ text?: string;
18
+ type: string;
19
+ }>;
20
+ };
21
+
22
+ const SYNTHETIC_USER_TEXT_PREFIXES = [
23
+ '<local-command-caveat>',
24
+ '<local-command-stdout>',
25
+ '<task-notification>',
26
+ '[Request interrupted by user]',
27
+ ] as const;
28
+
10
29
  const asMessageObject = (value: JsonValue | undefined): Record<string, JsonValue> | null => {
11
30
  return typeof value === 'object' && value !== null && !Array.isArray(value)
12
31
  ? (value as Record<string, JsonValue>)
13
32
  : null;
14
33
  };
15
34
 
35
+ export const isClaudeCodeRawFlagEnabled = (value: JsonValue | undefined): boolean => {
36
+ return value === true || value === 'true';
37
+ };
38
+
16
39
  export const getClaudeCodeAssistantMessagePhase = (
17
40
  entry: ClaudeCodeAssistantPhaseEntry,
18
41
  ): ClaudeCodeAssistantMessagePhase | null => {
@@ -20,6 +43,42 @@ export const getClaudeCodeAssistantMessagePhase = (
20
43
  return null;
21
44
  }
22
45
 
46
+ if (entry.assistantPhase) {
47
+ return entry.assistantPhase;
48
+ }
49
+
23
50
  const stopReason = asMessageObject(entry.raw.message)?.stop_reason;
24
- return stopReason === 'tool_use' ? 'commentary' : 'final_answer';
51
+ if (stopReason !== 'tool_use') {
52
+ return 'final_answer';
53
+ }
54
+
55
+ const toolUseIndex = entry.parts?.findIndex((part) => part.type === 'tool_use') ?? -1;
56
+ const hasTextAfterToolUse =
57
+ toolUseIndex >= 0 &&
58
+ entry.parts?.slice(toolUseIndex + 1).some((part) => part.type === 'text' && part.text?.trim()) === true;
59
+ return hasTextAfterToolUse ? 'final_answer' : 'commentary';
60
+ };
61
+
62
+ export const isClaudeCodeSyntheticTranscriptEntry = (entry: ClaudeCodeFilterEntry): boolean => {
63
+ if (
64
+ isClaudeCodeRawFlagEnabled(entry.raw.isApiErrorMessage) ||
65
+ isClaudeCodeRawFlagEnabled(entry.raw.isMeta) ||
66
+ isClaudeCodeRawFlagEnabled(entry.raw.isCompactSummary)
67
+ ) {
68
+ return true;
69
+ }
70
+
71
+ if (asMessageObject(entry.raw.message)?.model === '<synthetic>') {
72
+ return true;
73
+ }
74
+
75
+ if (entry.role !== 'user') {
76
+ return false;
77
+ }
78
+
79
+ const text = entry.parts.find((part) => part.type === 'text')?.text?.trim() ?? '';
80
+ return (
81
+ text.startsWith('<command-name>/compact</command-name>') ||
82
+ SYNTHETIC_USER_TEXT_PREFIXES.some((prefix) => text.startsWith(prefix))
83
+ );
25
84
  };
@@ -5,7 +5,10 @@ import type {
5
5
  ClaudeCodeTranscriptEntry,
6
6
  ClaudeCodeTranscriptPart,
7
7
  } from './claude-code-exporter-types';
8
- import { getClaudeCodeAssistantMessagePhase } from './claude-code-transcript-phase';
8
+ import {
9
+ getClaudeCodeAssistantMessagePhase,
10
+ isClaudeCodeSyntheticTranscriptEntry,
11
+ } from './claude-code-transcript-phase';
9
12
  import {
10
13
  cleanExtractedText,
11
14
  cleanInlineTitle,
@@ -102,7 +105,7 @@ const renderToolResultPart = (part: ClaudeCodeTranscriptPart, options: ClaudeCod
102
105
  return '';
103
106
  }
104
107
 
105
- const outputText = part.outputText?.trim();
108
+ const outputText = (part.outputText ?? '').trim();
106
109
  if (!outputText) {
107
110
  return '';
108
111
  }
@@ -160,9 +163,9 @@ export const renderClaudeCodeTranscript = (
160
163
  transcript: ClaudeCodeSessionTranscript,
161
164
  options: ClaudeCodeExportOptions,
162
165
  ): string | null => {
163
- const sections = transcript.entries.flatMap((entry) =>
164
- entry.parts.map((part) => renderPart(entry, part, options)).filter(Boolean),
165
- );
166
+ const sections = transcript.entries
167
+ .filter((entry) => !isClaudeCodeSyntheticTranscriptEntry(entry))
168
+ .flatMap((entry) => entry.parts.map((part) => renderPart(entry, part, options)).filter(Boolean));
166
169
  if (sections.length === 0) {
167
170
  return null;
168
171
  }
@@ -2,8 +2,9 @@ import { listScopedThreads } from './codex-browser-db';
2
2
  import type { CodexAnalytics, DistributionItem, ModelTokenSummary } from './codex-browser-types';
3
3
  import type { ThreadRow } from './codex-thread-types';
4
4
  import { mapWithConcurrency } from './concurrency';
5
- import { asObject, asString, getPortablePathBasename, readJsonlObjects } from './shared';
6
- import { hashCacheKeyParts, hashCacheKeyPartsIterable, withCachedJson } from './ui-cache';
5
+ import { getPortablePathBasename } from './portable-path';
6
+ import { asObject, asString, readJsonlObjects } from './shared';
7
+ import { hashCacheKeyPartsIterable, withCachedJson } from './ui-cache';
7
8
 
8
9
  export type CodexAnalyticsInput = {
9
10
  dbPath: string;
@@ -38,6 +39,20 @@ const roundToTwoDecimals = (value: number) => {
38
39
  return Number(value.toFixed(2));
39
40
  };
40
41
 
42
+ const median = (values: number[]) => {
43
+ if (values.length === 0) {
44
+ return 0;
45
+ }
46
+
47
+ const sorted = [...values].sort((left, right) => left - right);
48
+ const midpoint = Math.floor(sorted.length / 2);
49
+ if (sorted.length % 2 === 1) {
50
+ return sorted[midpoint]!;
51
+ }
52
+
53
+ return roundToTwoDecimals((sorted[midpoint - 1]! + sorted[midpoint]!) / 2);
54
+ };
55
+
41
56
  const incrementCount = (counts: Map<string, number>, key: string) => {
42
57
  counts.set(key, (counts.get(key) ?? 0) + 1);
43
58
  };
@@ -90,6 +105,8 @@ const threadMetadataCacheKeyParts = (thread: ThreadRow) => [
90
105
  String(thread.archived_at ?? ''),
91
106
  thread.cwd,
92
107
  thread.model ?? '',
108
+ thread.reasoning_effort ?? '',
109
+ thread.source,
93
110
  thread.model_provider,
94
111
  thread.cli_version,
95
112
  thread.title,
@@ -98,7 +115,7 @@ const threadMetadataCacheKeyParts = (thread: ThreadRow) => [
98
115
 
99
116
  export const buildCodexAnalyticsCacheKey = (dbPath: string, threads: ThreadRow[], project: string | null) => {
100
117
  const parts = (function* () {
101
- yield 'v2';
118
+ yield 'v3';
102
119
  yield dbPath;
103
120
  yield project ?? 'all';
104
121
  yield String(threads.length);
@@ -111,7 +128,7 @@ export const buildCodexAnalyticsCacheKey = (dbPath: string, threads: ThreadRow[]
111
128
  };
112
129
 
113
130
  const buildThreadAnalyticsCacheKey = (thread: ThreadRow) => {
114
- return `thread-analytics-${hashCacheKeyParts('v1', ...threadMetadataCacheKeyParts(thread))}`;
131
+ return `thread-analytics-${hashCacheKeyPartsIterable(['v1', ...threadMetadataCacheKeyParts(thread)])}`;
115
132
  };
116
133
 
117
134
  const parseThreadAnalyticsFile = async (sessionFile: string): Promise<ThreadAnalyticsSummary> => {
@@ -129,7 +146,7 @@ const parseThreadAnalyticsFile = async (sessionFile: string): Promise<ThreadAnal
129
146
  }
130
147
 
131
148
  const payloadType = asString(payload.type);
132
- if (payloadType === 'function_call') {
149
+ if (payloadType === 'function_call' || payloadType === 'custom_tool_call') {
133
150
  toolNames.push(asString(payload.name) ?? 'unknown');
134
151
  continue;
135
152
  }
@@ -155,6 +172,8 @@ export const computeCodexAnalyticsFromThreads = async (
155
172
  ): Promise<CodexAnalytics> => {
156
173
  const totalTokens = threads.reduce((sum, thread) => sum + thread.tokens_used, 0);
157
174
  const projectNames = new Set(threads.map((thread) => getPortablePathBasename(thread.cwd)).filter(Boolean));
175
+ const reasoningEfforts = new Map<string, number>();
176
+ const sources = new Map<string, number>();
158
177
  const toolUsage = new Map<string, number>();
159
178
  let threadsWithWebSearch = 0;
160
179
  const loadThreadAnalytics = options.loadThreadAnalytics ?? getCachedThreadAnalytics;
@@ -163,6 +182,11 @@ export const computeCodexAnalyticsFromThreads = async (
163
182
  loadThreadAnalytics(thread),
164
183
  );
165
184
 
185
+ for (const thread of threads) {
186
+ incrementCount(reasoningEfforts, thread.reasoning_effort?.trim() || 'unspecified');
187
+ incrementCount(sources, thread.source.trim() || 'unknown');
188
+ }
189
+
166
190
  for (const analytics of threadAnalytics) {
167
191
  if (analytics.hasWebSearch) {
168
192
  threadsWithWebSearch += 1;
@@ -175,10 +199,13 @@ export const computeCodexAnalyticsFromThreads = async (
175
199
 
176
200
  return {
177
201
  modelsByTokens: buildModelsByTokens(threads),
202
+ reasoningEfforts: toDistribution(reasoningEfforts),
203
+ sources: toDistribution(sources),
178
204
  summary: {
179
205
  archivedThreads: threads.filter((thread) => Boolean(thread.archived)).length,
180
206
  averageTokensPerThread: threads.length === 0 ? 0 : roundToTwoDecimals(totalTokens / threads.length),
181
207
  distinctToolNames: toolUsage.size,
208
+ medianTokensPerThread: median(threads.map((thread) => thread.tokens_used)),
182
209
  threadsWithWebSearch,
183
210
  totalProjects: projectNames.size,
184
211
  totalThreads: threads.length,