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.
- package/AGENTS.md +27 -12
- package/README.md +42 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +36 -11
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +46 -3
- package/apps/ui/src/components/breadcrumbs.tsx +21 -3
- package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
- package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
- package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
- package/apps/ui/src/components/data-table.tsx +23 -5
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +79 -74
- package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
- package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
- package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
- package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
- package/apps/ui/src/components/page-header.tsx +15 -9
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
- package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
- package/apps/ui/src/components/threads-table.tsx +13 -47
- package/apps/ui/src/components/transcript-view.tsx +148 -52
- package/apps/ui/src/components/ui/select.tsx +1 -1
- package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
- package/apps/ui/src/lib/antigravity-server.ts +209 -11
- package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +185 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +16 -0
- package/apps/ui/src/lib/codex-server.ts +134 -52
- package/apps/ui/src/lib/cursor-server.ts +68 -53
- package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
- package/apps/ui/src/lib/delete-batch.ts +52 -0
- package/apps/ui/src/lib/error-presentation.ts +43 -0
- package/apps/ui/src/lib/export-mutation.ts +20 -0
- package/apps/ui/src/lib/export-options.ts +15 -0
- package/apps/ui/src/lib/formatters.ts +3 -5
- package/apps/ui/src/lib/grok-queries.ts +22 -0
- package/apps/ui/src/lib/grok-server.ts +168 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +97 -7
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +96 -7
- package/apps/ui/src/lib/path-utils.ts +1 -1
- package/apps/ui/src/lib/qoder-queries.ts +15 -0
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/route-state-reset.tsx +10 -0
- package/apps/ui/src/lib/settings-server.ts +33 -0
- package/apps/ui/src/lib/settings-store.tsx +82 -38
- package/apps/ui/src/lib/settings.ts +65 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
- package/apps/ui/src/routeTree.gen.ts +341 -234
- package/apps/ui/src/routes/__root.tsx +12 -15
- package/apps/ui/src/routes/analytics.tsx +10 -61
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +149 -29
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
- package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
- package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +168 -28
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
- package/apps/ui/src/routes/claude-code.index.tsx +2 -2
- package/apps/ui/src/routes/codex.$project.tsx +33 -31
- package/apps/ui/src/routes/codex.index.tsx +5 -10
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +31 -18
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
- package/apps/ui/src/routes/opencode.index.tsx +6 -2
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +37 -27
- package/src/client.ts +183 -5
- package/src/lib/antigravity-db.ts +424 -94
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +534 -42
- package/src/lib/claude-code-exporter-types.ts +5 -0
- package/src/lib/claude-code-transcript-phase.ts +60 -1
- package/src/lib/claude-code-transcript.ts +8 -5
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +18 -27
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +106 -23
- package/src/lib/codex-thread-parser.ts +149 -55
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +87 -79
- package/src/lib/concurrency.ts +56 -3
- package/src/lib/conversation-api.ts +410 -27
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
- package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
- package/src/lib/conversation-data/codex-adapter.ts +108 -73
- package/src/lib/conversation-data/cursor-adapter.ts +77 -29
- package/src/lib/conversation-data/grok-adapter.ts +223 -0
- package/src/lib/conversation-data/index.ts +105 -11
- package/src/lib/conversation-data/kiro-adapter.ts +59 -12
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +67 -17
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +67 -37
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +75 -0
- package/src/lib/cursor-db.ts +68 -28
- package/src/lib/cursor-id.ts +17 -0
- package/src/lib/cursor-recovery.ts +146 -35
- package/src/lib/cursor-transcript-phase.ts +40 -0
- package/src/lib/cursor-transcript.ts +21 -5
- package/src/lib/grok-db.ts +1068 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +61 -0
- package/src/lib/grok-transcript.ts +167 -0
- package/src/lib/kiro-db.ts +187 -43
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +599 -60
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/portable-path.ts +9 -0
- package/src/lib/qoder-acp-client.ts +11 -2
- package/src/lib/qoder-db.ts +51 -17
- package/src/lib/qoder-transcript-phase.ts +64 -0
- package/src/lib/qoder-transcript.ts +30 -2
- package/src/lib/shared.ts +63 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +87 -0
- package/src/lib/ui-cache.ts +185 -24
- package/src/lib/ui-export-archive.ts +58 -39
- package/src/lib/ui-export-files.ts +75 -15
- package/src/lib/ui-export-zip.ts +40 -0
- package/apps/ui/package.json +0 -65
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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(
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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(
|
|
166
|
-
|
|
167
|
-
|
|
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 {
|
|
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:
|
|
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:
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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:
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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:
|
|
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
|
|
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',
|