spiracha 2.0.0 → 2.1.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/README.md +6 -1
- package/apps/ui/README.md +20 -2
- package/apps/ui/package.json +2 -2
- package/apps/ui/src/components/antigravity-conversations-table.tsx +50 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +1 -0
- 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 +7 -5
- package/apps/ui/src/components/export-dialog.tsx +2 -12
- 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/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/threads-table.tsx +9 -46
- 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 +152 -7
- package/apps/ui/src/lib/antigravity-transcript-events.ts +11 -3
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +175 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +19 -3
- package/apps/ui/src/lib/codex-server.ts +135 -53
- package/apps/ui/src/lib/cursor-server.ts +48 -5
- 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 +169 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +85 -7
- package/apps/ui/src/lib/opencode-server.ts +85 -7
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
- package/apps/ui/src/routeTree.gen.ts +107 -0
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +76 -10
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +269 -31
- 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 +143 -17
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +165 -26
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +2 -2
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +11 -1
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +66 -15
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +199 -32
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +105 -19
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +236 -44
- package/apps/ui/src/routes/opencode.index.tsx +16 -3
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +7 -4
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +75 -28
- package/apps/ui/src/routes/threads.$threadId.tsx +548 -64
- package/package.json +19 -18
- package/src/client.ts +134 -1
- package/src/lib/antigravity-db.ts +208 -32
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/claude-code-db.ts +498 -32
- 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-browser-export.ts +23 -27
- package/src/lib/codex-thread-cache.ts +41 -13
- package/src/lib/codex-thread-parser.ts +86 -35
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +39 -19
- package/src/lib/concurrency.ts +41 -0
- package/src/lib/conversation-api.ts +399 -19
- package/src/lib/conversation-data/antigravity-adapter.ts +21 -2
- package/src/lib/conversation-data/claude-code-adapter.ts +37 -6
- package/src/lib/conversation-data/codex-adapter.ts +28 -4
- package/src/lib/conversation-data/cursor-adapter.ts +35 -1
- package/src/lib/conversation-data/grok-adapter.ts +210 -0
- package/src/lib/conversation-data/index.ts +76 -1
- package/src/lib/conversation-data/kiro-adapter.ts +41 -7
- package/src/lib/conversation-data/opencode-adapter.ts +24 -2
- package/src/lib/conversation-data/qoder-adapter.ts +44 -19
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +57 -0
- package/src/lib/cursor-db.ts +34 -5
- package/src/lib/grok-db.ts +1026 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +52 -0
- package/src/lib/grok-transcript.ts +154 -0
- package/src/lib/kiro-db.ts +52 -1
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +598 -55
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/shared.ts +12 -0
- package/src/lib/transcript-load-limiter.ts +82 -0
- package/src/lib/ui-cache.ts +43 -17
- package/src/lib/ui-export-archive.ts +60 -0
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
deleteCodexThread,
|
|
3
|
+
getThreadBrowseData,
|
|
4
|
+
listScopedThreads,
|
|
5
|
+
resolveCodexThreadDbPath,
|
|
6
|
+
} from '../codex-browser-db';
|
|
2
7
|
import type { MessageEvent, ThreadBrowseData, ThreadEvent } from '../codex-browser-types';
|
|
3
8
|
import { parseCodexTranscriptFile } from '../codex-thread-parser';
|
|
4
9
|
import type { ThreadRow } from '../codex-thread-types';
|
|
5
10
|
import { cleanInlineTitle } from '../shared';
|
|
11
|
+
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
6
12
|
import { selectConversationMessages } from './message-selector';
|
|
7
13
|
import { getConversationPathMatch } from './path-match';
|
|
8
14
|
import type {
|
|
@@ -12,6 +18,7 @@ import type {
|
|
|
12
18
|
ConversationMessagePhase,
|
|
13
19
|
ConversationMessageRole,
|
|
14
20
|
ConversationPathMatch,
|
|
21
|
+
DeleteConversationOptions,
|
|
15
22
|
GetConversationOptions,
|
|
16
23
|
ListConversationsForPathOptions,
|
|
17
24
|
} from './types';
|
|
@@ -142,9 +149,17 @@ const toConversationMessage = (event: ThreadEvent): ConversationMessage | null =
|
|
|
142
149
|
};
|
|
143
150
|
|
|
144
151
|
const readCodexMessages = async (thread: ThreadRow): Promise<ConversationMessage[]> => {
|
|
145
|
-
const transcript = await
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
const transcript = await runWithTranscriptLoadLimit(
|
|
153
|
+
() =>
|
|
154
|
+
parseCodexTranscriptFile(thread.rollout_path, {
|
|
155
|
+
includeRaw: false,
|
|
156
|
+
}),
|
|
157
|
+
{
|
|
158
|
+
id: thread.id,
|
|
159
|
+
path: thread.rollout_path,
|
|
160
|
+
source: 'codex-api',
|
|
161
|
+
},
|
|
162
|
+
);
|
|
148
163
|
|
|
149
164
|
return transcript.events.flatMap((event) => {
|
|
150
165
|
const message = toConversationMessage(event);
|
|
@@ -238,7 +253,16 @@ const getCodexConversation = async (options: GetConversationOptions): Promise<Co
|
|
|
238
253
|
});
|
|
239
254
|
};
|
|
240
255
|
|
|
256
|
+
const deleteCodexConversation = async (options: DeleteConversationOptions) => {
|
|
257
|
+
const result = await deleteCodexThread(getCodexDbPath(options), options.id, { deleteSessionFiles: true });
|
|
258
|
+
return {
|
|
259
|
+
deletedFiles: result.deletedSessionFiles,
|
|
260
|
+
deletedIds: result.deletedThreadIds,
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
241
264
|
export const codexConversationAdapter: ConversationAdapter = {
|
|
265
|
+
deleteConversation: deleteCodexConversation,
|
|
242
266
|
getConversation: getCodexConversation,
|
|
243
267
|
listConversationsForPath: listCodexConversationsForPath,
|
|
244
268
|
source: 'codex',
|
|
@@ -11,7 +11,9 @@ import type {
|
|
|
11
11
|
CursorWorkspaceGroup,
|
|
12
12
|
} from '../cursor-exporter-types';
|
|
13
13
|
import { getCursorGlobalDbPath, resolveCursorUserDir } from '../cursor-exporter-types';
|
|
14
|
+
import { collectCursorThreadsForDeletion, isCursorRunning, pruneCursorThreads } from '../cursor-recovery';
|
|
14
15
|
import { cleanInlineTitle } from '../shared';
|
|
16
|
+
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
15
17
|
import { createDeepLinks, createTextMessage, finalizeMessages } from './adapter-helpers';
|
|
16
18
|
import { selectConversationMessages } from './message-selector';
|
|
17
19
|
import { getFirstConversationPathMatch } from './path-match';
|
|
@@ -20,6 +22,7 @@ import type {
|
|
|
20
22
|
ConversationDetail,
|
|
21
23
|
ConversationMessage,
|
|
22
24
|
ConversationPathMatch,
|
|
25
|
+
DeleteConversationOptions,
|
|
23
26
|
GetConversationOptions,
|
|
24
27
|
ListConversationsForPathOptions,
|
|
25
28
|
} from './types';
|
|
@@ -90,8 +93,16 @@ const buildConversation = async (
|
|
|
90
93
|
matches: ConversationPathMatch[],
|
|
91
94
|
options: Pick<ListConversationsForPathOptions, 'includeMessages' | 'messageSelector'>,
|
|
92
95
|
): Promise<ConversationDetail> => {
|
|
96
|
+
const globalDbPath = getCursorGlobalDbPath(userDir);
|
|
93
97
|
const transcript = options.includeMessages
|
|
94
|
-
? await
|
|
98
|
+
? await runWithTranscriptLoadLimit(
|
|
99
|
+
() => readCursorThreadTranscriptWithAgentFiles(globalDbPath, thread.composerId, userDir),
|
|
100
|
+
{
|
|
101
|
+
id: thread.composerId,
|
|
102
|
+
path: thread.transcriptDirs[0] ?? globalDbPath,
|
|
103
|
+
source: 'cursor-api',
|
|
104
|
+
},
|
|
105
|
+
)
|
|
95
106
|
: null;
|
|
96
107
|
const allMessages = transcript ? transcriptToMessages(transcript) : [];
|
|
97
108
|
const messages = options.includeMessages
|
|
@@ -164,7 +175,30 @@ const getCursorConversation = async (options: GetConversationOptions): Promise<C
|
|
|
164
175
|
return null;
|
|
165
176
|
};
|
|
166
177
|
|
|
178
|
+
const deleteCursorConversation = async (options: DeleteConversationOptions) => {
|
|
179
|
+
const userDir = getUserDir(options);
|
|
180
|
+
if (!options.locations?.cursorUserDir && (await isCursorRunning())) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
'Quit Cursor before deleting. It rewrites chat history on exit, which can resurrect deleted threads.',
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const existing = await getCursorConversation(options);
|
|
187
|
+
if (!existing) {
|
|
188
|
+
return { deletedFiles: [], deletedIds: [] };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const threads = await collectCursorThreadsForDeletion([options.id], userDir);
|
|
192
|
+
const deletedFiles = threads.flatMap((thread) => thread.transcriptDirs);
|
|
193
|
+
const result = await pruneCursorThreads(threads, true, userDir);
|
|
194
|
+
return {
|
|
195
|
+
deletedFiles,
|
|
196
|
+
deletedIds: result.composerIds,
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
167
200
|
export const cursorConversationAdapter: ConversationAdapter = {
|
|
201
|
+
deleteConversation: deleteCursorConversation,
|
|
168
202
|
getConversation: getCursorConversation,
|
|
169
203
|
listConversationsForPath: listCursorConversationsForPath,
|
|
170
204
|
source: 'cursor',
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deleteGrokSession,
|
|
3
|
+
listGrokSessionsForGroup,
|
|
4
|
+
listGrokWorkspaceGroups,
|
|
5
|
+
readGrokSessionTranscript,
|
|
6
|
+
resolveGrokSessionsDir,
|
|
7
|
+
} from '../grok-db';
|
|
8
|
+
import type {
|
|
9
|
+
GrokSessionSummary,
|
|
10
|
+
GrokSessionTranscript,
|
|
11
|
+
GrokTranscriptEntry,
|
|
12
|
+
GrokTranscriptPart,
|
|
13
|
+
} from '../grok-exporter-types';
|
|
14
|
+
import { getFinalGrokAssistantTextPartIds, getGrokTextPartPhase } from '../grok-transcript-phase';
|
|
15
|
+
import { cleanInlineTitle } from '../shared';
|
|
16
|
+
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
17
|
+
import {
|
|
18
|
+
createDeepLinks,
|
|
19
|
+
createTextMessage,
|
|
20
|
+
finalizeMessages,
|
|
21
|
+
normalizeAssistantPhase,
|
|
22
|
+
normalizeRole,
|
|
23
|
+
} from './adapter-helpers';
|
|
24
|
+
import { selectConversationMessages } from './message-selector';
|
|
25
|
+
import { getConversationPathMatch } from './path-match';
|
|
26
|
+
import type {
|
|
27
|
+
ConversationAdapter,
|
|
28
|
+
ConversationDetail,
|
|
29
|
+
ConversationMessage,
|
|
30
|
+
ConversationPathMatch,
|
|
31
|
+
DeleteConversationOptions,
|
|
32
|
+
GetConversationOptions,
|
|
33
|
+
ListConversationsForPathOptions,
|
|
34
|
+
} from './types';
|
|
35
|
+
|
|
36
|
+
const getSessionsDir = (options: { locations?: { grokSessionsDir?: string } }) =>
|
|
37
|
+
options.locations?.grokSessionsDir ?? resolveGrokSessionsDir();
|
|
38
|
+
|
|
39
|
+
const partToMessages = (
|
|
40
|
+
entry: GrokTranscriptEntry,
|
|
41
|
+
part: GrokTranscriptPart,
|
|
42
|
+
finalTextPartIds: Set<string>,
|
|
43
|
+
order: number,
|
|
44
|
+
): ConversationMessage[] => {
|
|
45
|
+
if (part.type === 'text') {
|
|
46
|
+
return createTextMessage({
|
|
47
|
+
createdAtMs: entry.createdAtMs,
|
|
48
|
+
id: part.partId,
|
|
49
|
+
metadata: { modelFingerprint: entry.modelFingerprint, modelId: entry.modelId },
|
|
50
|
+
order,
|
|
51
|
+
phase: normalizeAssistantPhase(getGrokTextPartPhase(entry, part, finalTextPartIds), 'unknown'),
|
|
52
|
+
role: normalizeRole(entry.role),
|
|
53
|
+
text: part.text,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (part.type === 'reasoning') {
|
|
58
|
+
return createTextMessage({
|
|
59
|
+
createdAtMs: entry.createdAtMs,
|
|
60
|
+
id: part.partId,
|
|
61
|
+
order,
|
|
62
|
+
phase: 'reasoning',
|
|
63
|
+
role: 'assistant',
|
|
64
|
+
text: part.text,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (part.type === 'tool_call') {
|
|
69
|
+
return createTextMessage({
|
|
70
|
+
createdAtMs: entry.createdAtMs,
|
|
71
|
+
id: part.partId,
|
|
72
|
+
metadata: { toolCallId: part.toolCallId, toolName: part.toolName },
|
|
73
|
+
order,
|
|
74
|
+
phase: 'tool_call',
|
|
75
|
+
role: 'tool',
|
|
76
|
+
text: [part.toolName, part.argumentsText].filter(Boolean).join('\n'),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (part.type === 'tool_result') {
|
|
81
|
+
return createTextMessage({
|
|
82
|
+
createdAtMs: entry.createdAtMs,
|
|
83
|
+
id: part.partId,
|
|
84
|
+
metadata: { toolCallId: part.toolCallId },
|
|
85
|
+
order,
|
|
86
|
+
phase: 'tool_output',
|
|
87
|
+
role: 'tool',
|
|
88
|
+
text: part.outputText,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const transcriptToMessages = (transcript: GrokSessionTranscript): ConversationMessage[] => {
|
|
96
|
+
const finalTextPartIds = getFinalGrokAssistantTextPartIds(transcript.entries);
|
|
97
|
+
return finalizeMessages(
|
|
98
|
+
transcript.entries.flatMap((entry, entryIndex) =>
|
|
99
|
+
entry.parts.flatMap((part, partIndex) =>
|
|
100
|
+
partToMessages(entry, part, finalTextPartIds, entryIndex + partIndex),
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const buildConversation = async (
|
|
107
|
+
session: GrokSessionSummary,
|
|
108
|
+
sessionsDir: string,
|
|
109
|
+
matches: ConversationPathMatch[],
|
|
110
|
+
options: Pick<ListConversationsForPathOptions, 'includeMessages' | 'messageSelector'>,
|
|
111
|
+
loadedTranscript: GrokSessionTranscript | null = null,
|
|
112
|
+
): Promise<ConversationDetail> => {
|
|
113
|
+
const transcript =
|
|
114
|
+
loadedTranscript ??
|
|
115
|
+
(options.includeMessages
|
|
116
|
+
? await runWithTranscriptLoadLimit(
|
|
117
|
+
() => readGrokSessionTranscript(sessionsDir, session.sessionId, { includeRawPayloads: false }),
|
|
118
|
+
{
|
|
119
|
+
id: session.sessionId,
|
|
120
|
+
path: sessionsDir,
|
|
121
|
+
source: 'grok-api',
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
: null);
|
|
125
|
+
const allMessages = transcript ? transcriptToMessages(transcript) : [];
|
|
126
|
+
const messages = options.includeMessages
|
|
127
|
+
? selectConversationMessages(allMessages, options.messageSelector ?? 'last_final_answer')
|
|
128
|
+
: [];
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
createdAtMs: session.createdAtMs,
|
|
132
|
+
deepLinks: createDeepLinks('grok', session.sessionId, `/grok-sessions/${session.sessionId}`),
|
|
133
|
+
id: session.sessionId,
|
|
134
|
+
matches,
|
|
135
|
+
messageCount: options.includeMessages ? allMessages.length : session.messageCount,
|
|
136
|
+
messages,
|
|
137
|
+
metadata: {
|
|
138
|
+
agentName: session.agentName,
|
|
139
|
+
currentModelId: session.currentModelId,
|
|
140
|
+
gitBranch: session.gitBranch,
|
|
141
|
+
headCommit: session.headCommit,
|
|
142
|
+
modelLabel: session.modelLabel,
|
|
143
|
+
renderablePartCount: session.renderablePartCount,
|
|
144
|
+
sandboxProfile: session.sandboxProfile,
|
|
145
|
+
},
|
|
146
|
+
source: 'grok',
|
|
147
|
+
title: cleanInlineTitle(session.title),
|
|
148
|
+
updatedAtMs: session.lastActiveAtMs,
|
|
149
|
+
workspaceKey: session.workspaceKey,
|
|
150
|
+
workspacePath: session.worktree,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const listGrokConversationsForPath = async (options: ListConversationsForPathOptions) => {
|
|
155
|
+
const sessionsDir = getSessionsDir(options);
|
|
156
|
+
const groups = await listGrokWorkspaceGroups(sessionsDir);
|
|
157
|
+
const conversations: ConversationDetail[] = [];
|
|
158
|
+
|
|
159
|
+
for (const group of groups) {
|
|
160
|
+
const match = await getConversationPathMatch(options.cwd, group.worktree);
|
|
161
|
+
if (!match) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const sessions = await listGrokSessionsForGroup(group.key, sessionsDir);
|
|
165
|
+
for (const session of sessions) {
|
|
166
|
+
conversations.push(await buildConversation(session, sessionsDir, [match], options));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return conversations;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const getGrokConversation = async (options: GetConversationOptions): Promise<ConversationDetail | null> => {
|
|
174
|
+
const sessionsDir = getSessionsDir(options);
|
|
175
|
+
const transcript = await runWithTranscriptLoadLimit(
|
|
176
|
+
() => readGrokSessionTranscript(sessionsDir, options.id, { includeRawPayloads: false }),
|
|
177
|
+
{
|
|
178
|
+
id: options.id,
|
|
179
|
+
path: sessionsDir,
|
|
180
|
+
source: 'grok-api',
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
return transcript
|
|
184
|
+
? buildConversation(
|
|
185
|
+
transcript.session,
|
|
186
|
+
sessionsDir,
|
|
187
|
+
[],
|
|
188
|
+
{
|
|
189
|
+
includeMessages: true,
|
|
190
|
+
messageSelector: options.messageSelector ?? 'all',
|
|
191
|
+
},
|
|
192
|
+
transcript,
|
|
193
|
+
)
|
|
194
|
+
: null;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const deleteGrokConversation = async (options: DeleteConversationOptions) => {
|
|
198
|
+
const result = await deleteGrokSession(getSessionsDir(options), options.id);
|
|
199
|
+
return {
|
|
200
|
+
deletedFiles: result.deletedFiles,
|
|
201
|
+
deletedIds: result.deletedSessionIds,
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const grokConversationAdapter: ConversationAdapter = {
|
|
206
|
+
deleteConversation: deleteGrokConversation,
|
|
207
|
+
getConversation: getGrokConversation,
|
|
208
|
+
listConversationsForPath: listGrokConversationsForPath,
|
|
209
|
+
source: 'grok',
|
|
210
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { mapWithConcurrency } from '../concurrency';
|
|
1
2
|
import { antigravityConversationAdapter } from './antigravity-adapter';
|
|
2
3
|
import { claudeCodeConversationAdapter } from './claude-code-adapter';
|
|
3
4
|
import { codexConversationAdapter } from './codex-adapter';
|
|
4
5
|
import { cursorConversationAdapter } from './cursor-adapter';
|
|
6
|
+
import { grokConversationAdapter } from './grok-adapter';
|
|
5
7
|
import { kiroConversationAdapter } from './kiro-adapter';
|
|
6
8
|
import { selectConversationMessages } from './message-selector';
|
|
7
9
|
import { opencodeConversationAdapter } from './opencode-adapter';
|
|
@@ -14,6 +16,11 @@ import {
|
|
|
14
16
|
type ConversationPage,
|
|
15
17
|
type ConversationSource,
|
|
16
18
|
type ConversationSourceInfo,
|
|
19
|
+
type DeleteConversationItemResult,
|
|
20
|
+
type DeleteConversationOptions,
|
|
21
|
+
type DeleteConversationResult,
|
|
22
|
+
type DeleteConversationsOptions,
|
|
23
|
+
type DeleteConversationsResult,
|
|
17
24
|
type GetConversationOptions,
|
|
18
25
|
type ListConversationsForPathOptions,
|
|
19
26
|
type ResolvedConversationRef,
|
|
@@ -27,6 +34,7 @@ export {
|
|
|
27
34
|
type ConversationDataLocations,
|
|
28
35
|
type ConversationDeepLinks,
|
|
29
36
|
type ConversationDetail,
|
|
37
|
+
type ConversationIdSetOptions,
|
|
30
38
|
type ConversationMessage,
|
|
31
39
|
type ConversationMessagePhase,
|
|
32
40
|
type ConversationMessageRole,
|
|
@@ -35,6 +43,13 @@ export {
|
|
|
35
43
|
type ConversationPathMatch,
|
|
36
44
|
type ConversationSource,
|
|
37
45
|
type ConversationSourceInfo,
|
|
46
|
+
type ConversationZipDownload,
|
|
47
|
+
type DeleteConversationItemResult,
|
|
48
|
+
type DeleteConversationOptions,
|
|
49
|
+
type DeleteConversationResult,
|
|
50
|
+
type DeleteConversationsOptions,
|
|
51
|
+
type DeleteConversationsResult,
|
|
52
|
+
type ExportConversationsZipOptions,
|
|
38
53
|
type GetConversationOptions,
|
|
39
54
|
type ListConversationsForPathOptions,
|
|
40
55
|
type ResolvedConversationRef,
|
|
@@ -45,6 +60,7 @@ const SOURCE_LABELS: Record<ConversationSource, string> = {
|
|
|
45
60
|
'claude-code': 'Claude Code',
|
|
46
61
|
codex: 'Codex',
|
|
47
62
|
cursor: 'Cursor',
|
|
63
|
+
grok: 'Grok',
|
|
48
64
|
kiro: 'Kiro',
|
|
49
65
|
opencode: 'OpenCode',
|
|
50
66
|
qoder: 'Qoder',
|
|
@@ -64,6 +80,7 @@ const ADAPTERS: Partial<Record<ConversationSource, ConversationAdapter>> = {
|
|
|
64
80
|
'claude-code': claudeCodeConversationAdapter,
|
|
65
81
|
codex: codexConversationAdapter,
|
|
66
82
|
cursor: cursorConversationAdapter,
|
|
83
|
+
grok: grokConversationAdapter,
|
|
67
84
|
kiro: kiroConversationAdapter,
|
|
68
85
|
opencode: opencodeConversationAdapter,
|
|
69
86
|
qoder: qoderConversationAdapter,
|
|
@@ -71,6 +88,16 @@ const ADAPTERS: Partial<Record<ConversationSource, ConversationAdapter>> = {
|
|
|
71
88
|
|
|
72
89
|
const MAX_LIMIT = 200;
|
|
73
90
|
const DEFAULT_LIMIT = 100;
|
|
91
|
+
const DELETE_CONCURRENCY_BY_SOURCE: Record<ConversationSource, number> = {
|
|
92
|
+
antigravity: 1,
|
|
93
|
+
'claude-code': 4,
|
|
94
|
+
codex: 1,
|
|
95
|
+
cursor: 1,
|
|
96
|
+
grok: 1,
|
|
97
|
+
kiro: 1,
|
|
98
|
+
opencode: 2,
|
|
99
|
+
qoder: 1,
|
|
100
|
+
};
|
|
74
101
|
|
|
75
102
|
const getEnabledSources = (sources: ListConversationsForPathOptions['sources']): ConversationSource[] => {
|
|
76
103
|
if (!sources || sources === 'all') {
|
|
@@ -181,10 +208,56 @@ export const getConversation = async (options: GetConversationOptions) => {
|
|
|
181
208
|
return getAdapter(options.source)?.getConversation(options) ?? null;
|
|
182
209
|
};
|
|
183
210
|
|
|
211
|
+
export const deleteConversation = async (
|
|
212
|
+
options: DeleteConversationOptions,
|
|
213
|
+
): Promise<DeleteConversationResult | null> => {
|
|
214
|
+
return (await getAdapter(options.source)?.deleteConversation?.(options)) ?? null;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const deleteConversations = async (
|
|
218
|
+
options: DeleteConversationsOptions,
|
|
219
|
+
): Promise<DeleteConversationsResult | null> => {
|
|
220
|
+
const adapter = getAdapter(options.source);
|
|
221
|
+
if (!adapter?.deleteConversation) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const deleteAdapterConversation = adapter.deleteConversation;
|
|
225
|
+
|
|
226
|
+
const rawResults = await mapWithConcurrency(
|
|
227
|
+
options.ids,
|
|
228
|
+
DELETE_CONCURRENCY_BY_SOURCE[options.source],
|
|
229
|
+
async (id) => ({
|
|
230
|
+
id,
|
|
231
|
+
result: await deleteAdapterConversation({
|
|
232
|
+
id,
|
|
233
|
+
locations: options.locations,
|
|
234
|
+
source: options.source,
|
|
235
|
+
}),
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
const deletedIdSet = new Set(rawResults.flatMap(({ result }) => result.deletedIds));
|
|
239
|
+
const results: DeleteConversationItemResult[] = rawResults.map(({ id, result }) => ({
|
|
240
|
+
deleted: result.deletedIds.length > 0 || deletedIdSet.has(id),
|
|
241
|
+
deletedFiles: result.deletedFiles,
|
|
242
|
+
deletedIds: result.deletedIds,
|
|
243
|
+
id,
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
deletedFiles: [...new Set(results.flatMap((result) => result.deletedFiles))],
|
|
248
|
+
deletedIds: [...deletedIdSet],
|
|
249
|
+
missingIds: results.filter((result) => !result.deleted).map((result) => result.id),
|
|
250
|
+
results,
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
|
|
184
254
|
const sourceFromSessionRoute = (segment: string): ConversationSource | null => {
|
|
185
255
|
if (segment === 'claude-code-sessions') {
|
|
186
256
|
return 'claude-code';
|
|
187
257
|
}
|
|
258
|
+
if (segment === 'grok-sessions') {
|
|
259
|
+
return 'grok';
|
|
260
|
+
}
|
|
188
261
|
if (segment === 'kiro-sessions') {
|
|
189
262
|
return 'kiro';
|
|
190
263
|
}
|
|
@@ -287,7 +360,9 @@ export const renderConversationMarkdown = (
|
|
|
287
360
|
messages: ConversationMessage[];
|
|
288
361
|
title: string | null;
|
|
289
362
|
},
|
|
290
|
-
options: {
|
|
363
|
+
options: {
|
|
364
|
+
messageSelector?: ConversationMessageSelector;
|
|
365
|
+
} = {},
|
|
291
366
|
) => {
|
|
292
367
|
const selectedMessages = options.messageSelector
|
|
293
368
|
? selectConversationMessages(conversation.messages, options.messageSelector)
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
deleteKiroSession,
|
|
3
|
+
listKiroSessionsForGroup,
|
|
4
|
+
listKiroWorkspaceGroups,
|
|
5
|
+
readKiroSessionTranscript,
|
|
6
|
+
} from '../kiro-db';
|
|
2
7
|
import type {
|
|
3
8
|
KiroSessionSummary,
|
|
4
9
|
KiroSessionTranscript,
|
|
@@ -8,6 +13,7 @@ import type {
|
|
|
8
13
|
import { resolveKiroWorkspaceSessionsDir } from '../kiro-exporter-types';
|
|
9
14
|
import { getFinalKiroAssistantMessageEntryIds, getKiroMessagePhase } from '../kiro-transcript-phase';
|
|
10
15
|
import { cleanInlineTitle } from '../shared';
|
|
16
|
+
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
11
17
|
import {
|
|
12
18
|
createDeepLinks,
|
|
13
19
|
createTextMessage,
|
|
@@ -23,6 +29,7 @@ import type {
|
|
|
23
29
|
ConversationDetail,
|
|
24
30
|
ConversationMessage,
|
|
25
31
|
ConversationPathMatch,
|
|
32
|
+
DeleteConversationOptions,
|
|
26
33
|
GetConversationOptions,
|
|
27
34
|
ListConversationsForPathOptions,
|
|
28
35
|
} from './types';
|
|
@@ -74,8 +81,16 @@ const buildConversation = async (
|
|
|
74
81
|
sessionsDir: string,
|
|
75
82
|
matches: ConversationPathMatch[],
|
|
76
83
|
options: Pick<ListConversationsForPathOptions, 'includeMessages' | 'messageSelector'>,
|
|
84
|
+
loadedTranscript: KiroSessionTranscript | null = null,
|
|
77
85
|
): Promise<ConversationDetail> => {
|
|
78
|
-
const transcript = options.includeMessages
|
|
86
|
+
const transcript = options.includeMessages
|
|
87
|
+
? (loadedTranscript ??
|
|
88
|
+
(await runWithTranscriptLoadLimit(() => readKiroSessionTranscript(sessionsDir, session.sessionId), {
|
|
89
|
+
id: session.sessionId,
|
|
90
|
+
path: session.filePath,
|
|
91
|
+
source: 'kiro-api',
|
|
92
|
+
})))
|
|
93
|
+
: null;
|
|
79
94
|
const allMessages = transcript ? transcriptToMessages(transcript) : [];
|
|
80
95
|
const messages = options.includeMessages
|
|
81
96
|
? selectConversationMessages(allMessages, options.messageSelector ?? 'last_final_answer')
|
|
@@ -124,16 +139,35 @@ const listKiroConversationsForPath = async (options: ListConversationsForPathOpt
|
|
|
124
139
|
|
|
125
140
|
const getKiroConversation = async (options: GetConversationOptions): Promise<ConversationDetail | null> => {
|
|
126
141
|
const sessionsDir = getSessionsDir(options);
|
|
127
|
-
const transcript = await readKiroSessionTranscript(sessionsDir, options.id)
|
|
142
|
+
const transcript = await runWithTranscriptLoadLimit(() => readKiroSessionTranscript(sessionsDir, options.id), {
|
|
143
|
+
id: options.id,
|
|
144
|
+
path: sessionsDir,
|
|
145
|
+
source: 'kiro-api',
|
|
146
|
+
});
|
|
128
147
|
return transcript
|
|
129
|
-
? buildConversation(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
? buildConversation(
|
|
149
|
+
transcript.session,
|
|
150
|
+
sessionsDir,
|
|
151
|
+
[],
|
|
152
|
+
{
|
|
153
|
+
includeMessages: true,
|
|
154
|
+
messageSelector: options.messageSelector ?? 'all',
|
|
155
|
+
},
|
|
156
|
+
transcript,
|
|
157
|
+
)
|
|
133
158
|
: null;
|
|
134
159
|
};
|
|
135
160
|
|
|
161
|
+
const deleteKiroConversation = async (options: DeleteConversationOptions) => {
|
|
162
|
+
const result = await deleteKiroSession(getSessionsDir(options), options.id);
|
|
163
|
+
return {
|
|
164
|
+
deletedFiles: result.deletedFiles,
|
|
165
|
+
deletedIds: result.deletedSessionIds,
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
136
169
|
export const kiroConversationAdapter: ConversationAdapter = {
|
|
170
|
+
deleteConversation: deleteKiroConversation,
|
|
137
171
|
getConversation: getKiroConversation,
|
|
138
172
|
listConversationsForPath: listKiroConversationsForPath,
|
|
139
173
|
source: 'kiro',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
deleteOpenCodeSession,
|
|
2
3
|
listOpenCodeSessionsForGroup,
|
|
3
4
|
listOpenCodeWorkspaceGroups,
|
|
4
5
|
readOpenCodeSessionTranscript,
|
|
@@ -12,6 +13,7 @@ import { resolveOpenCodeDbPath } from '../opencode-exporter-types';
|
|
|
12
13
|
import { splitOpenCodeThinkTaggedText } from '../opencode-think-tags';
|
|
13
14
|
import { getFinalOpenCodeAssistantTextPartIds, getOpenCodeTextPartPhase } from '../opencode-transcript-phase';
|
|
14
15
|
import { cleanInlineTitle } from '../shared';
|
|
16
|
+
import { runWithTranscriptLoadLimit } from '../transcript-load-limiter';
|
|
15
17
|
import {
|
|
16
18
|
createDeepLinks,
|
|
17
19
|
createTextMessage,
|
|
@@ -26,6 +28,7 @@ import type {
|
|
|
26
28
|
ConversationDetail,
|
|
27
29
|
ConversationMessage,
|
|
28
30
|
ConversationPathMatch,
|
|
31
|
+
DeleteConversationOptions,
|
|
29
32
|
GetConversationOptions,
|
|
30
33
|
ListConversationsForPathOptions,
|
|
31
34
|
} from './types';
|
|
@@ -109,7 +112,13 @@ const buildConversation = async (
|
|
|
109
112
|
): Promise<ConversationDetail> => {
|
|
110
113
|
const transcript =
|
|
111
114
|
loadedTranscript ??
|
|
112
|
-
(options.includeMessages
|
|
115
|
+
(options.includeMessages
|
|
116
|
+
? await runWithTranscriptLoadLimit(() => readOpenCodeSessionTranscript(dbPath, session.sessionId), {
|
|
117
|
+
id: session.sessionId,
|
|
118
|
+
path: dbPath,
|
|
119
|
+
source: 'opencode-api',
|
|
120
|
+
})
|
|
121
|
+
: null);
|
|
113
122
|
const allMessages = transcript ? transcriptToMessages(transcript) : [];
|
|
114
123
|
const messages = options.includeMessages
|
|
115
124
|
? selectConversationMessages(allMessages, options.messageSelector ?? 'last_final_answer')
|
|
@@ -158,7 +167,11 @@ const listOpenCodeConversationsForPath = async (options: ListConversationsForPat
|
|
|
158
167
|
|
|
159
168
|
const getOpenCodeConversation = async (options: GetConversationOptions): Promise<ConversationDetail | null> => {
|
|
160
169
|
const dbPath = getDbPath(options);
|
|
161
|
-
const transcript = await readOpenCodeSessionTranscript(dbPath, options.id)
|
|
170
|
+
const transcript = await runWithTranscriptLoadLimit(() => readOpenCodeSessionTranscript(dbPath, options.id), {
|
|
171
|
+
id: options.id,
|
|
172
|
+
path: dbPath,
|
|
173
|
+
source: 'opencode-api',
|
|
174
|
+
});
|
|
162
175
|
return transcript
|
|
163
176
|
? buildConversation(
|
|
164
177
|
transcript.session,
|
|
@@ -173,7 +186,16 @@ const getOpenCodeConversation = async (options: GetConversationOptions): Promise
|
|
|
173
186
|
: null;
|
|
174
187
|
};
|
|
175
188
|
|
|
189
|
+
const deleteOpenCodeConversation = async (options: DeleteConversationOptions) => {
|
|
190
|
+
const result = await deleteOpenCodeSession(getDbPath(options), options.id);
|
|
191
|
+
return {
|
|
192
|
+
deletedFiles: [],
|
|
193
|
+
deletedIds: result.deletedSessionIds,
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
|
|
176
197
|
export const opencodeConversationAdapter: ConversationAdapter = {
|
|
198
|
+
deleteConversation: deleteOpenCodeConversation,
|
|
177
199
|
getConversation: getOpenCodeConversation,
|
|
178
200
|
listConversationsForPath: listOpenCodeConversationsForPath,
|
|
179
201
|
source: 'opencode',
|