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
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](LICENSE.md)
|
|
8
8
|
[](https://bun.sh)
|
|
9
9
|
|
|
10
|
-
Spiracha is a local TanStack Start app for browsing and exporting agent conversation history from Codex, Claude Code, Kiro, Qoder, Cursor, Antigravity, and OpenCode.
|
|
10
|
+
Spiracha is a local TanStack Start app for browsing and exporting agent conversation history from Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode.
|
|
11
11
|
|
|
12
12
|
The legacy CLI, MCP server, and Codex plugin surfaces have been removed in the 2.0 hard cut. Spiracha now exposes the UI and a stable local data API; client-specific workflows such as review collection belong in the client that calls the API.
|
|
13
13
|
|
|
@@ -54,11 +54,16 @@ GET /api/v1/conversations?cwd=/absolute/project&include_messages=true
|
|
|
54
54
|
POST /api/v1/conversation-query
|
|
55
55
|
GET /api/v1/conversations/:source/:id
|
|
56
56
|
GET /api/v1/conversations/:source/:id/export
|
|
57
|
+
DELETE /api/v1/conversations/:source/:id
|
|
58
|
+
POST /api/v1/conversations/delete
|
|
59
|
+
POST /api/v1/conversations/export
|
|
57
60
|
GET /api/v1/resolve?ref=<url-or-deeplink>
|
|
58
61
|
```
|
|
59
62
|
|
|
60
63
|
The default list selector is `last_final_answer`, which keeps `fgh --collect` style clients fast and small. Use `message_selector=all` when a client needs the full normalized thread.
|
|
61
64
|
|
|
65
|
+
Batch delete requires an explicit source and ID list. It returns `deletedIds`, `missingIds`, and a result for each requested ID, so partial success is represented in a `200` response body. Batch export also requires an explicit source and ID list, but is atomic: any missing ID returns an error instead of a partial archive.
|
|
66
|
+
|
|
62
67
|
Example:
|
|
63
68
|
|
|
64
69
|
```bash
|
package/apps/ui/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Spiracha UI
|
|
2
2
|
|
|
3
|
-
The browser UI for browsing local Codex, Claude Code, Kiro, Qoder, Cursor, Antigravity, and OpenCode history, inspecting transcript details, exporting chats, and analyzing Codex usage patterns.
|
|
3
|
+
The browser UI for browsing local Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode history, inspecting transcript details, exporting chats, and analyzing Codex usage patterns.
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
@@ -18,9 +18,11 @@ The browser UI for browsing local Codex, Claude Code, Kiro, Qoder, Cursor, Antig
|
|
|
18
18
|
- lists derived Codex projects from the Codex SQLite database
|
|
19
19
|
- lists Codex threads within a project in chronological order
|
|
20
20
|
- shows Codex thread timelines, tool calls, metadata, and raw event context
|
|
21
|
-
- exports Codex, Claude Code, Kiro, Qoder, Cursor, and OpenCode sessions or threads as Markdown, plain text, or optional zip archives with optional metadata, commentary, and tool-call inclusion
|
|
21
|
+
- exports Codex, Claude Code, Grok, Kiro, Qoder, Cursor, and OpenCode sessions or threads as Markdown, plain text, or optional zip archives with optional metadata, commentary, and tool-call inclusion
|
|
22
22
|
- lists Claude Code workspaces and sessions from local `~/.claude/projects` JSONL files
|
|
23
23
|
- shows dedicated Claude Code session detail pages with reasoning, tool calls, token metadata, and export actions
|
|
24
|
+
- lists Grok workspaces and sessions from local Grok session archives
|
|
25
|
+
- shows dedicated Grok session detail pages with compacted-history recovery, tool calls, metadata, export, and delete actions
|
|
24
26
|
- lists Kiro workspaces and sessions from local Kiro workspace session files
|
|
25
27
|
- shows dedicated Kiro session detail pages with image attachments, prompt logs, execution-derived tool calls, metadata, and export actions
|
|
26
28
|
- lists Qoder workspaces and sessions from local Qoder history and checkpoint storage
|
|
@@ -67,9 +69,15 @@ Runtime configuration is intentionally small:
|
|
|
67
69
|
- `SPIRACHA_ANALYTICS_TRANSCRIPT_CONCURRENCY`
|
|
68
70
|
- Optional positive integer for Codex analytics transcript parsing concurrency.
|
|
69
71
|
- Defaults to `8`.
|
|
72
|
+
- `SPIRACHA_TRANSCRIPT_LOAD_CONCURRENCY`
|
|
73
|
+
- Optional positive integer for detail-page transcript loading concurrency across sources.
|
|
74
|
+
- Defaults to `3` and is capped at `16` to protect the server from excessive parallel disk and database work.
|
|
70
75
|
- `SPIRACHA_CLAUDE_CODE_PROJECTS_DIR`
|
|
71
76
|
- Optional absolute path to the Claude Code projects directory.
|
|
72
77
|
- If unset, Spiracha reads `${SPIRACHA_CLAUDE_CODE_DATA_DIR:-~/.claude}/projects`. `SPIRACHA_CLAUDE_CODE_DIR` and `SPIRACHA_CLAUDE_HOME` are also accepted aliases for the Claude Code data directory.
|
|
78
|
+
- `SPIRACHA_GROK_SESSIONS_DIR`
|
|
79
|
+
- Optional path to the Grok sessions directory.
|
|
80
|
+
- If unset, Spiracha reads `${SPIRACHA_GROK_HOME:-~/.grok}/sessions`. `SPIRACHA_GROK_DIR` is accepted as a Grok home-directory alias.
|
|
73
81
|
- `SPIRACHA_KIRO_WORKSPACE_SESSIONS_DIR`
|
|
74
82
|
- Optional absolute path to the Kiro workspace sessions directory.
|
|
75
83
|
- If unset, Spiracha reads `${SPIRACHA_KIRO_DATA_DIR:-~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent}/workspace-sessions`. `SPIRACHA_KIRO_AGENT_DIR` and `SPIRACHA_KIRO_DIR` are also accepted aliases for the Kiro data directory.
|
|
@@ -82,6 +90,9 @@ Runtime configuration is intentionally small:
|
|
|
82
90
|
- `SPIRACHA_OPENCODE_DB`
|
|
83
91
|
- Optional absolute path to the OpenCode SQLite database.
|
|
84
92
|
- If unset, Spiracha reads `${SPIRACHA_OPENCODE_DATA_DIR:-${XDG_DATA_HOME:-~/.local/share}/opencode}/opencode.db`. `SPIRACHA_OPENCODE_DIR` is also accepted as an OpenCode data-directory alias.
|
|
93
|
+
- `SPIRACHA_OPENCODE_DB_CONCURRENCY`
|
|
94
|
+
- Optional positive integer for concurrent OpenCode database reads.
|
|
95
|
+
- Defaults to `2`.
|
|
85
96
|
- `SPIRACHA_CURSOR_USER_DIR`
|
|
86
97
|
- Optional absolute path to Cursor's `User` directory.
|
|
87
98
|
- If unset, Spiracha reads the platform default Cursor user-data directory.
|
|
@@ -100,6 +111,7 @@ Default source locations:
|
|
|
100
111
|
| --- | --- | --- |
|
|
101
112
|
| Codex | shared Codex DB probe list | `SPIRACHA_CODEX_DB` |
|
|
102
113
|
| Claude Code | `~/.claude/projects` | `SPIRACHA_CLAUDE_CODE_PROJECTS_DIR` |
|
|
114
|
+
| Grok | `~/.grok/sessions` | `SPIRACHA_GROK_SESSIONS_DIR`, `SPIRACHA_GROK_HOME` |
|
|
103
115
|
| Kiro | `~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent/workspace-sessions` | `SPIRACHA_KIRO_WORKSPACE_SESSIONS_DIR` |
|
|
104
116
|
| Qoder | `~/Library/Application Support/Qoder/User/globalStorage/state.vscdb` + `~/Library/Application Support/Qoder/User/workspaceStorage` | `SPIRACHA_QODER_GLOBAL_STATE_DB`, `SPIRACHA_QODER_WORKSPACE_STORAGE_DIR` |
|
|
105
117
|
| Cursor | `~/Library/Application Support/Cursor/User` on macOS | `SPIRACHA_CURSOR_USER_DIR`, `SPIRACHA_CURSOR_PROJECTS_DIR` |
|
|
@@ -125,6 +137,12 @@ Transcript detail pages expose the same display controls across sources: user me
|
|
|
125
137
|
- Claude Code workspace session listing
|
|
126
138
|
- `/claude-code-sessions/$sessionId`
|
|
127
139
|
- Claude Code session detail and export
|
|
140
|
+
- `/grok`
|
|
141
|
+
- Grok workspace inventory and search
|
|
142
|
+
- `/grok/$workspaceKey`
|
|
143
|
+
- Grok workspace session listing
|
|
144
|
+
- `/grok-sessions/$sessionId`
|
|
145
|
+
- Grok session detail, export, and delete
|
|
128
146
|
- `/kiro`
|
|
129
147
|
- Kiro workspace inventory and search
|
|
130
148
|
- `/kiro/$workspaceKey`
|
package/apps/ui/package.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"@tailwindcss/vite": "4.3.1",
|
|
4
4
|
"@tanstack/match-sorter-utils": "8.19.4",
|
|
5
5
|
"@tanstack/react-devtools": "0.10.5",
|
|
6
|
-
"@tanstack/react-query": "5.101.
|
|
7
|
-
"@tanstack/react-query-devtools": "5.101.
|
|
6
|
+
"@tanstack/react-query": "5.101.2",
|
|
7
|
+
"@tanstack/react-query-devtools": "5.101.2",
|
|
8
8
|
"@tanstack/react-router": "1.170.15",
|
|
9
9
|
"@tanstack/react-router-devtools": "1.167.0",
|
|
10
10
|
"@tanstack/react-router-ssr-query": "1.167.1",
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { AntigravityConversation } from '@spiracha/lib/antigravity-exporter-types';
|
|
2
2
|
import type { AntigravityDecryptionState } from '@spiracha/lib/antigravity-keychain';
|
|
3
3
|
import { Link } from '@tanstack/react-router';
|
|
4
|
+
import type { SortingState } from '@tanstack/react-table';
|
|
4
5
|
import { createColumnHelper } from '@tanstack/react-table';
|
|
5
|
-
import { Download, LockKeyhole, MoreHorizontal, ScrollText } from 'lucide-react';
|
|
6
|
+
import { Download, LockKeyhole, MoreHorizontal, ScrollText, Trash2 } from 'lucide-react';
|
|
6
7
|
import { useMemo } from 'react';
|
|
7
8
|
import { DataTable } from '#/components/data-table';
|
|
9
|
+
import { SelectionActionsToolbar } from '#/components/selection-actions-toolbar';
|
|
8
10
|
import { Badge } from '#/components/ui/badge';
|
|
9
11
|
import { Button } from '#/components/ui/button';
|
|
10
12
|
import {
|
|
@@ -24,8 +26,11 @@ import { formatBytes, formatDateTime, formatNumber } from '#/lib/formatters';
|
|
|
24
26
|
type AntigravityConversationsTableProps = {
|
|
25
27
|
conversations: AntigravityConversation[];
|
|
26
28
|
decryptionState: AntigravityDecryptionState | null;
|
|
29
|
+
onDeleteConversation: (conversation: AntigravityConversation) => void;
|
|
30
|
+
onDeleteConversations: (conversationIds: string[]) => void;
|
|
27
31
|
onExportArtifacts: (conversation: AntigravityConversation) => void;
|
|
28
32
|
onExportConversation: (conversation: AntigravityConversation) => void;
|
|
33
|
+
onExportConversations: (conversationIds: string[]) => void;
|
|
29
34
|
};
|
|
30
35
|
|
|
31
36
|
type ConversationExportState = {
|
|
@@ -34,10 +39,10 @@ type ConversationExportState = {
|
|
|
34
39
|
hasTranscript: boolean;
|
|
35
40
|
lockedTranscript: boolean;
|
|
36
41
|
showConversationAction: boolean;
|
|
37
|
-
showActions: boolean;
|
|
38
42
|
};
|
|
39
43
|
|
|
40
44
|
const columnHelper = createColumnHelper<AntigravityConversation>();
|
|
45
|
+
const defaultSorting: SortingState = [{ desc: true, id: 'updatedAt' }];
|
|
41
46
|
|
|
42
47
|
const getConversationExportState = (
|
|
43
48
|
conversation: AntigravityConversation,
|
|
@@ -56,7 +61,6 @@ const getConversationExportState = (
|
|
|
56
61
|
hasArtifacts,
|
|
57
62
|
hasTranscript,
|
|
58
63
|
lockedTranscript,
|
|
59
|
-
showActions: showConversationAction || hasArtifacts,
|
|
60
64
|
showConversationAction,
|
|
61
65
|
};
|
|
62
66
|
};
|
|
@@ -74,8 +78,17 @@ const getTranscriptLabel = (
|
|
|
74
78
|
return `${source} · ${status}`;
|
|
75
79
|
};
|
|
76
80
|
|
|
81
|
+
const getConversationSizeLabel = (conversation: AntigravityConversation): string => {
|
|
82
|
+
if (conversation.totalBytes > 0) {
|
|
83
|
+
return formatBytes(conversation.totalBytes);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return conversation.summaryPath ? 'Summary' : formatBytes(conversation.totalBytes);
|
|
87
|
+
};
|
|
88
|
+
|
|
77
89
|
const columns = (
|
|
78
90
|
decryptionState: AntigravityDecryptionState | null,
|
|
91
|
+
onDeleteConversation: (conversation: AntigravityConversation) => void,
|
|
79
92
|
onExportConversation: (conversation: AntigravityConversation) => void,
|
|
80
93
|
onExportArtifacts: (conversation: AntigravityConversation) => void,
|
|
81
94
|
) =>
|
|
@@ -110,6 +123,7 @@ const columns = (
|
|
|
110
123
|
</span>
|
|
111
124
|
),
|
|
112
125
|
header: 'Updated',
|
|
126
|
+
id: 'updatedAt',
|
|
113
127
|
}),
|
|
114
128
|
columnHelper.display({
|
|
115
129
|
cell: (info) => {
|
|
@@ -130,17 +144,13 @@ const columns = (
|
|
|
130
144
|
cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
|
|
131
145
|
header: 'Artifacts',
|
|
132
146
|
}),
|
|
133
|
-
columnHelper.accessor('
|
|
134
|
-
cell: (info) => <span className="font-mono text-sm">{
|
|
147
|
+
columnHelper.accessor('totalBytes', {
|
|
148
|
+
cell: (info) => <span className="font-mono text-sm">{getConversationSizeLabel(info.row.original)}</span>,
|
|
135
149
|
header: 'Size',
|
|
136
150
|
}),
|
|
137
151
|
columnHelper.display({
|
|
138
152
|
cell: (info) => {
|
|
139
153
|
const exportState = getConversationExportState(info.row.original, decryptionState);
|
|
140
|
-
if (!exportState.showActions) {
|
|
141
|
-
return <span className="text-[var(--muted-foreground)] text-sm">No export</span>;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
154
|
return (
|
|
145
155
|
<DropdownMenu>
|
|
146
156
|
<DropdownMenuTrigger asChild>
|
|
@@ -176,6 +186,13 @@ const columns = (
|
|
|
176
186
|
Export artifacts
|
|
177
187
|
</DropdownMenuItem>
|
|
178
188
|
) : null}
|
|
189
|
+
<DropdownMenuItem
|
|
190
|
+
className="text-[var(--destructive)]"
|
|
191
|
+
onClick={() => onDeleteConversation(info.row.original)}
|
|
192
|
+
>
|
|
193
|
+
<Trash2 className="mr-2 size-4" />
|
|
194
|
+
Delete conversation
|
|
195
|
+
</DropdownMenuItem>
|
|
179
196
|
</DropdownMenuContent>
|
|
180
197
|
</DropdownMenu>
|
|
181
198
|
);
|
|
@@ -188,12 +205,15 @@ const columns = (
|
|
|
188
205
|
export function AntigravityConversationsTable({
|
|
189
206
|
conversations,
|
|
190
207
|
decryptionState,
|
|
208
|
+
onDeleteConversation,
|
|
209
|
+
onDeleteConversations,
|
|
191
210
|
onExportArtifacts,
|
|
192
211
|
onExportConversation,
|
|
212
|
+
onExportConversations,
|
|
193
213
|
}: AntigravityConversationsTableProps) {
|
|
194
214
|
const tableColumns = useMemo(
|
|
195
|
-
() => columns(decryptionState, onExportConversation, onExportArtifacts),
|
|
196
|
-
[decryptionState, onExportArtifacts, onExportConversation],
|
|
215
|
+
() => columns(decryptionState, onDeleteConversation, onExportConversation, onExportArtifacts),
|
|
216
|
+
[decryptionState, onDeleteConversation, onExportArtifacts, onExportConversation],
|
|
197
217
|
);
|
|
198
218
|
|
|
199
219
|
return (
|
|
@@ -201,6 +221,25 @@ export function AntigravityConversationsTable({
|
|
|
201
221
|
columns={tableColumns}
|
|
202
222
|
data={conversations}
|
|
203
223
|
emptyMessage="No Antigravity conversations match the current workspace filter."
|
|
224
|
+
enableRowSelection
|
|
225
|
+
getRowId={(row) => row.conversationId}
|
|
226
|
+
initialSorting={defaultSorting}
|
|
227
|
+
renderToolbar={({ clearSelection, selectedRows }) => {
|
|
228
|
+
const selectedConversationIds = selectedRows.map((row) => row.conversationId);
|
|
229
|
+
const hasNonExportableSelection = selectedRows.some(
|
|
230
|
+
(row) => !getConversationExportState(row, decryptionState).canExportConversation,
|
|
231
|
+
);
|
|
232
|
+
return (
|
|
233
|
+
<SelectionActionsToolbar
|
|
234
|
+
clearSelection={clearSelection}
|
|
235
|
+
exportDisabled={hasNonExportableSelection}
|
|
236
|
+
itemLabel="conversation"
|
|
237
|
+
selectedCount={selectedRows.length}
|
|
238
|
+
onDeleteSelected={() => onDeleteConversations(selectedConversationIds)}
|
|
239
|
+
onExportSelected={() => onExportConversations(selectedConversationIds)}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
242
|
+
}}
|
|
204
243
|
/>
|
|
205
244
|
);
|
|
206
245
|
}
|
|
@@ -38,7 +38,7 @@ const columns = [
|
|
|
38
38
|
cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
|
|
39
39
|
header: 'Artifacts',
|
|
40
40
|
}),
|
|
41
|
-
columnHelper.accessor('
|
|
41
|
+
columnHelper.accessor('totalBytes', {
|
|
42
42
|
cell: (info) => <span className="font-mono text-sm">{formatBytes(info.getValue())}</span>,
|
|
43
43
|
header: 'Size',
|
|
44
44
|
}),
|
|
@@ -32,6 +32,7 @@ const navItems: readonly NavItem[] = [
|
|
|
32
32
|
label: 'Claude Code',
|
|
33
33
|
to: '/claude-code',
|
|
34
34
|
},
|
|
35
|
+
{ activePrefixes: ['/grok', '/grok-sessions'], icon: Bot, label: 'Grok', to: '/grok' },
|
|
35
36
|
{ activePrefixes: ['/kiro', '/kiro-sessions'], icon: BrainCircuit, label: 'Kiro', to: '/kiro' },
|
|
36
37
|
{ activePrefixes: ['/qoder', '/qoder-sessions'], icon: Workflow, label: 'Qoder', to: '/qoder' },
|
|
37
38
|
{
|
|
@@ -5,11 +5,15 @@ import type { ReactNode } from 'react';
|
|
|
5
5
|
type BreadcrumbItem =
|
|
6
6
|
| {
|
|
7
7
|
label: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
truncate?: boolean;
|
|
8
10
|
}
|
|
9
11
|
| {
|
|
10
12
|
label: string;
|
|
11
13
|
params?: Record<string, string>;
|
|
14
|
+
title?: string;
|
|
12
15
|
to: string;
|
|
16
|
+
truncate?: boolean;
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
type BreadcrumbsProps = {
|
|
@@ -20,27 +24,41 @@ const isLinkItem = (item: BreadcrumbItem): item is Extract<BreadcrumbItem, { to:
|
|
|
20
24
|
return 'to' in item;
|
|
21
25
|
};
|
|
22
26
|
|
|
27
|
+
const classNames = (...values: string[]) => values.filter(Boolean).join(' ');
|
|
28
|
+
|
|
23
29
|
export const Breadcrumbs = ({ items }: BreadcrumbsProps) => {
|
|
24
30
|
return (
|
|
25
31
|
<nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-1 text-sm">
|
|
26
32
|
{items.map((item, index) => {
|
|
33
|
+
const title = item.title ?? (item.truncate ? item.label : undefined);
|
|
34
|
+
const truncateClassName = item.truncate
|
|
35
|
+
? 'inline-block max-w-[min(34rem,62vw)] truncate align-bottom'
|
|
36
|
+
: '';
|
|
27
37
|
const content: ReactNode = isLinkItem(item) ? (
|
|
28
38
|
<Link
|
|
29
|
-
className=
|
|
39
|
+
className={classNames(
|
|
40
|
+
truncateClassName,
|
|
41
|
+
'text-[var(--muted-foreground)] transition hover:text-[var(--foreground)]',
|
|
42
|
+
)}
|
|
30
43
|
params={item.params}
|
|
44
|
+
title={title}
|
|
31
45
|
to={item.to}
|
|
32
46
|
>
|
|
33
47
|
{item.label}
|
|
34
48
|
</Link>
|
|
35
49
|
) : (
|
|
36
|
-
<span
|
|
50
|
+
<span
|
|
51
|
+
aria-current="page"
|
|
52
|
+
className={classNames(truncateClassName, 'font-medium text-[var(--foreground)]')}
|
|
53
|
+
title={title}
|
|
54
|
+
>
|
|
37
55
|
{item.label}
|
|
38
56
|
</span>
|
|
39
57
|
);
|
|
40
58
|
|
|
41
59
|
return (
|
|
42
60
|
<div
|
|
43
|
-
className="flex items-center gap-1"
|
|
61
|
+
className="flex min-w-0 items-center gap-1"
|
|
44
62
|
key={isLinkItem(item) ? `${item.label}-${item.to}-${index}` : `${item.label}-current-${index}`}
|
|
45
63
|
>
|
|
46
64
|
{index > 0 ? <ChevronRight className="size-3.5 text-[var(--muted-foreground)]" /> : null}
|
|
@@ -2,9 +2,10 @@ import type { ClaudeCodeSessionSummary } from '@spiracha/lib/claude-code-exporte
|
|
|
2
2
|
import { Link } from '@tanstack/react-router';
|
|
3
3
|
import type { SortingState } from '@tanstack/react-table';
|
|
4
4
|
import { createColumnHelper } from '@tanstack/react-table';
|
|
5
|
-
import { Download, MoreHorizontal } from 'lucide-react';
|
|
5
|
+
import { Download, MoreHorizontal, Trash2 } from 'lucide-react';
|
|
6
6
|
import { useMemo } from 'react';
|
|
7
7
|
import { DataTable } from '#/components/data-table';
|
|
8
|
+
import { SelectionActionsToolbar } from '#/components/selection-actions-toolbar';
|
|
8
9
|
import { Button } from '#/components/ui/button';
|
|
9
10
|
import {
|
|
10
11
|
DropdownMenu,
|
|
@@ -15,14 +16,20 @@ import {
|
|
|
15
16
|
import { formatDateTime, formatNumber, formatTokens } from '#/lib/formatters';
|
|
16
17
|
|
|
17
18
|
type ClaudeCodeSessionsTableProps = {
|
|
19
|
+
onDeleteSession: (session: ClaudeCodeSessionSummary) => void;
|
|
20
|
+
onDeleteSessions: (sessionIds: string[]) => void;
|
|
18
21
|
onExportSession: (session: ClaudeCodeSessionSummary) => void;
|
|
22
|
+
onExportSessions: (sessionIds: string[]) => void;
|
|
19
23
|
sessions: ClaudeCodeSessionSummary[];
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
const columnHelper = createColumnHelper<ClaudeCodeSessionSummary>();
|
|
23
27
|
const defaultSorting: SortingState = [{ desc: true, id: 'lastActive' }];
|
|
24
28
|
|
|
25
|
-
const columns = (
|
|
29
|
+
const columns = (
|
|
30
|
+
onDeleteSession: (session: ClaudeCodeSessionSummary) => void,
|
|
31
|
+
onExportSession: (session: ClaudeCodeSessionSummary) => void,
|
|
32
|
+
) =>
|
|
26
33
|
[
|
|
27
34
|
columnHelper.accessor('title', {
|
|
28
35
|
cell: (info) => (
|
|
@@ -39,8 +46,8 @@ const columns = (onExportSession: (session: ClaudeCodeSessionSummary) => void) =
|
|
|
39
46
|
}),
|
|
40
47
|
columnHelper.accessor('lastActiveAtMs', {
|
|
41
48
|
cell: (info) => (
|
|
42
|
-
<span className="whitespace-nowrap text-sm">
|
|
43
|
-
{formatDateTime(info.getValue()
|
|
49
|
+
<span className="whitespace-nowrap text-sm" suppressHydrationWarning>
|
|
50
|
+
{formatDateTime(info.getValue())}
|
|
44
51
|
</span>
|
|
45
52
|
),
|
|
46
53
|
header: 'Updated',
|
|
@@ -91,6 +98,13 @@ const columns = (onExportSession: (session: ClaudeCodeSessionSummary) => void) =
|
|
|
91
98
|
<Download className="mr-2 size-4" />
|
|
92
99
|
Export session
|
|
93
100
|
</DropdownMenuItem>
|
|
101
|
+
<DropdownMenuItem
|
|
102
|
+
className="text-[var(--destructive)]"
|
|
103
|
+
onClick={() => onDeleteSession(info.row.original)}
|
|
104
|
+
>
|
|
105
|
+
<Trash2 className="mr-2 size-4" />
|
|
106
|
+
Delete session
|
|
107
|
+
</DropdownMenuItem>
|
|
94
108
|
</DropdownMenuContent>
|
|
95
109
|
</DropdownMenu>
|
|
96
110
|
),
|
|
@@ -100,15 +114,37 @@ const columns = (onExportSession: (session: ClaudeCodeSessionSummary) => void) =
|
|
|
100
114
|
}),
|
|
101
115
|
] as const;
|
|
102
116
|
|
|
103
|
-
export function ClaudeCodeSessionsTable({
|
|
104
|
-
|
|
117
|
+
export function ClaudeCodeSessionsTable({
|
|
118
|
+
onDeleteSession,
|
|
119
|
+
onDeleteSessions,
|
|
120
|
+
onExportSession,
|
|
121
|
+
onExportSessions,
|
|
122
|
+
sessions,
|
|
123
|
+
}: ClaudeCodeSessionsTableProps) {
|
|
124
|
+
const tableColumns = useMemo(() => columns(onDeleteSession, onExportSession), [onDeleteSession, onExportSession]);
|
|
105
125
|
|
|
106
126
|
return (
|
|
107
127
|
<DataTable
|
|
108
128
|
columns={tableColumns}
|
|
109
129
|
data={sessions}
|
|
110
130
|
emptyMessage="No Claude Code sessions match the current workspace filter."
|
|
131
|
+
enableRowSelection
|
|
132
|
+
getRowId={(row) => row.sessionId}
|
|
111
133
|
initialSorting={defaultSorting}
|
|
134
|
+
renderToolbar={({ clearSelection, selectedRows }) => {
|
|
135
|
+
const selectedSessionIds = selectedRows.map((row) => row.sessionId);
|
|
136
|
+
const hasEmptySelection = selectedRows.some((row) => row.renderablePartCount === 0);
|
|
137
|
+
return (
|
|
138
|
+
<SelectionActionsToolbar
|
|
139
|
+
clearSelection={clearSelection}
|
|
140
|
+
exportDisabled={hasEmptySelection}
|
|
141
|
+
itemLabel="session"
|
|
142
|
+
selectedCount={selectedRows.length}
|
|
143
|
+
onDeleteSelected={() => onDeleteSessions(selectedSessionIds)}
|
|
144
|
+
onExportSelected={() => onExportSessions(selectedSessionIds)}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}}
|
|
112
148
|
/>
|
|
113
149
|
);
|
|
114
150
|
}
|
|
@@ -38,7 +38,9 @@ const columns = [
|
|
|
38
38
|
}),
|
|
39
39
|
columnHelper.accessor('lastActiveAtMs', {
|
|
40
40
|
cell: (info) => (
|
|
41
|
-
<span className="whitespace-nowrap text-sm"
|
|
41
|
+
<span className="whitespace-nowrap text-sm" suppressHydrationWarning>
|
|
42
|
+
{formatDateTime(info.getValue())}
|
|
43
|
+
</span>
|
|
42
44
|
),
|
|
43
45
|
header: 'Last active',
|
|
44
46
|
}),
|
|
@@ -2,9 +2,10 @@ import type { CursorThreadSummary } from '@spiracha/lib/cursor-exporter-types';
|
|
|
2
2
|
import { Link } from '@tanstack/react-router';
|
|
3
3
|
import type { SortingState } from '@tanstack/react-table';
|
|
4
4
|
import { createColumnHelper } from '@tanstack/react-table';
|
|
5
|
-
import { Download, MoreHorizontal, Trash2
|
|
5
|
+
import { Download, MoreHorizontal, Trash2 } from 'lucide-react';
|
|
6
6
|
import { useMemo } from 'react';
|
|
7
7
|
import { DataTable } from '#/components/data-table';
|
|
8
|
+
import { SelectionActionsToolbar } from '#/components/selection-actions-toolbar';
|
|
8
9
|
import { Button } from '#/components/ui/button';
|
|
9
10
|
import {
|
|
10
11
|
DropdownMenu,
|
|
@@ -133,55 +134,17 @@ export const CursorThreadsTable = ({
|
|
|
133
134
|
getRowId={(row) => row.composerId}
|
|
134
135
|
initialSorting={defaultSorting}
|
|
135
136
|
renderToolbar={({ clearSelection, selectedRows }) => {
|
|
136
|
-
if (selectedRows.length === 0) {
|
|
137
|
-
return (
|
|
138
|
-
<p className="text-[var(--muted-foreground)] text-sm">
|
|
139
|
-
Select threads to export or delete them in a batch.
|
|
140
|
-
</p>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
137
|
const selectedComposerIds = selectedRows.map((row) => row.composerId);
|
|
145
138
|
const hasEmptySelection = selectedRows.some((row) => row.bubbleCount === 0);
|
|
146
139
|
return (
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
size="sm"
|
|
156
|
-
type="button"
|
|
157
|
-
variant="outline"
|
|
158
|
-
onClick={() => onExportThreads(selectedComposerIds)}
|
|
159
|
-
>
|
|
160
|
-
<Download className="mr-2 size-4" />
|
|
161
|
-
Export selected threads
|
|
162
|
-
</Button>
|
|
163
|
-
<Button
|
|
164
|
-
className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
|
|
165
|
-
size="sm"
|
|
166
|
-
type="button"
|
|
167
|
-
variant="outline"
|
|
168
|
-
onClick={() => onDeleteThreads(selectedComposerIds)}
|
|
169
|
-
>
|
|
170
|
-
<Trash2 className="mr-2 size-4" />
|
|
171
|
-
Delete selected threads
|
|
172
|
-
</Button>
|
|
173
|
-
<Button
|
|
174
|
-
className="rounded-full"
|
|
175
|
-
size="sm"
|
|
176
|
-
type="button"
|
|
177
|
-
variant="ghost"
|
|
178
|
-
onClick={clearSelection}
|
|
179
|
-
>
|
|
180
|
-
<X className="mr-2 size-4" />
|
|
181
|
-
Clear selection
|
|
182
|
-
</Button>
|
|
183
|
-
</div>
|
|
184
|
-
</div>
|
|
140
|
+
<SelectionActionsToolbar
|
|
141
|
+
clearSelection={clearSelection}
|
|
142
|
+
exportDisabled={hasEmptySelection}
|
|
143
|
+
itemLabel="thread"
|
|
144
|
+
selectedCount={selectedRows.length}
|
|
145
|
+
onDeleteSelected={() => onDeleteThreads(selectedComposerIds)}
|
|
146
|
+
onExportSelected={() => onExportThreads(selectedComposerIds)}
|
|
147
|
+
/>
|
|
185
148
|
);
|
|
186
149
|
}}
|
|
187
150
|
/>
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
useReactTable,
|
|
10
10
|
} from '@tanstack/react-table';
|
|
11
11
|
import { ArrowDownUp } from 'lucide-react';
|
|
12
|
-
import { type
|
|
12
|
+
import { type ReactNode, useRef, useState } from 'react';
|
|
13
13
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
14
14
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '#/components/ui/table';
|
|
15
15
|
import { cn } from '#/lib/utils';
|
|
@@ -79,6 +79,7 @@ export function DataTable<TData>({
|
|
|
79
79
|
const [sorting, setSorting] = useState<SortingState>(initialSorting);
|
|
80
80
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
|
81
81
|
const lastSelectedRowIdRef = useRef<string | null>(null);
|
|
82
|
+
const pendingShiftSelectionRowIdRef = useRef<string | null>(null);
|
|
82
83
|
|
|
83
84
|
const updateSelectionForRow = (rowId: string, checked: boolean, shiftKey: boolean) => {
|
|
84
85
|
const visibleRowIds = table.getRowModel().rows.map((row) => row.id);
|
|
@@ -101,17 +102,18 @@ export function DataTable<TData>({
|
|
|
101
102
|
<Checkbox
|
|
102
103
|
aria-label={`Select row ${row.id}`}
|
|
103
104
|
checked={row.getIsSelected()}
|
|
104
|
-
|
|
105
|
+
onPointerDown={(event) => {
|
|
105
106
|
event.stopPropagation();
|
|
106
|
-
event.
|
|
107
|
-
updateSelectionForRow(row.id, !row.getIsSelected(), event.shiftKey);
|
|
107
|
+
pendingShiftSelectionRowIdRef.current = event.shiftKey ? row.id : null;
|
|
108
108
|
}}
|
|
109
109
|
onCheckedChange={(checked) => {
|
|
110
110
|
if (typeof checked !== 'boolean') {
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
const shiftKey = pendingShiftSelectionRowIdRef.current === row.id;
|
|
115
|
+
pendingShiftSelectionRowIdRef.current = null;
|
|
116
|
+
updateSelectionForRow(row.id, checked, shiftKey);
|
|
115
117
|
}}
|
|
116
118
|
/>
|
|
117
119
|
),
|
|
@@ -79,18 +79,8 @@ export function ExportDialog({
|
|
|
79
79
|
<SelectValue placeholder="Choose a format" />
|
|
80
80
|
</SelectTrigger>
|
|
81
81
|
<SelectContent className="border-[var(--border)] bg-[var(--panel)] text-[var(--foreground)] shadow-[var(--panel-shadow)]">
|
|
82
|
-
<SelectItem
|
|
83
|
-
|
|
84
|
-
value="md"
|
|
85
|
-
>
|
|
86
|
-
Markdown (.md)
|
|
87
|
-
</SelectItem>
|
|
88
|
-
<SelectItem
|
|
89
|
-
className="focus:bg-[var(--panel-secondary)] focus:text-[var(--foreground)] data-[highlighted]:bg-[var(--panel-secondary)] data-[highlighted]:text-[var(--foreground)]"
|
|
90
|
-
value="txt"
|
|
91
|
-
>
|
|
92
|
-
Plain text (.txt)
|
|
93
|
-
</SelectItem>
|
|
82
|
+
<SelectItem value="md">Markdown (.md)</SelectItem>
|
|
83
|
+
<SelectItem value="txt">Plain text (.txt)</SelectItem>
|
|
94
84
|
</SelectContent>
|
|
95
85
|
</Select>
|
|
96
86
|
</div>
|