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,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useId, useState } from 'react';
|
|
1
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
2
2
|
import { Button } from '#/components/ui/button';
|
|
3
3
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
4
4
|
import {
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
DialogTitle,
|
|
11
11
|
} from '#/components/ui/dialog';
|
|
12
12
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '#/components/ui/select';
|
|
13
|
+
import type { ExportDialogOptions } from '#/lib/export-options';
|
|
14
|
+
import { useSettings } from '#/lib/settings-store';
|
|
13
15
|
|
|
14
16
|
type ExportDialogProps = {
|
|
15
17
|
disabled?: boolean;
|
|
@@ -17,14 +19,10 @@ type ExportDialogProps = {
|
|
|
17
19
|
forceZipArchive?: boolean;
|
|
18
20
|
open: boolean;
|
|
19
21
|
pending?: boolean;
|
|
22
|
+
showCommentaryOption?: boolean;
|
|
23
|
+
showToolsOption?: boolean;
|
|
20
24
|
title?: string;
|
|
21
|
-
onExport: (options:
|
|
22
|
-
includeCommentary: boolean;
|
|
23
|
-
includeMetadata: boolean;
|
|
24
|
-
includeTools: boolean;
|
|
25
|
-
outputFormat: 'md' | 'txt';
|
|
26
|
-
zipArchive: boolean;
|
|
27
|
-
}) => void;
|
|
25
|
+
onExport: (options: ExportDialogOptions) => void;
|
|
28
26
|
onOpenChange: (open: boolean) => void;
|
|
29
27
|
};
|
|
30
28
|
|
|
@@ -34,27 +32,49 @@ export function ExportDialog({
|
|
|
34
32
|
forceZipArchive = false,
|
|
35
33
|
open,
|
|
36
34
|
pending = false,
|
|
35
|
+
showCommentaryOption = true,
|
|
36
|
+
showToolsOption = true,
|
|
37
37
|
title = 'Export thread',
|
|
38
38
|
onExport,
|
|
39
39
|
onOpenChange,
|
|
40
40
|
}: ExportDialogProps) {
|
|
41
|
-
const
|
|
42
|
-
const [
|
|
43
|
-
const [
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const effectiveZipArchive = forceZipArchive || zipArchive;
|
|
41
|
+
const { settings, updateSetting } = useSettings();
|
|
42
|
+
const [options, setOptions] = useState<ExportDialogOptions>(settings.exportDefaults);
|
|
43
|
+
const [submitted, setSubmitted] = useState(false);
|
|
44
|
+
const submissionInProgress = useRef(false);
|
|
45
|
+
const previousPending = useRef(pending);
|
|
46
|
+
const effectiveZipArchive = forceZipArchive || options.zipArchive;
|
|
47
47
|
const zipDescriptionId = useId();
|
|
48
48
|
|
|
49
49
|
useEffect(() => {
|
|
50
50
|
if (!open) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
setIncludeTools(true);
|
|
55
|
-
setZipArchive(false);
|
|
51
|
+
setOptions(settings.exportDefaults);
|
|
52
|
+
setSubmitted(false);
|
|
53
|
+
submissionInProgress.current = false;
|
|
56
54
|
}
|
|
57
|
-
}, [open]);
|
|
55
|
+
}, [open, settings.exportDefaults]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if ((previousPending.current && !pending) || errorMessage) {
|
|
59
|
+
setSubmitted(false);
|
|
60
|
+
submissionInProgress.current = false;
|
|
61
|
+
}
|
|
62
|
+
previousPending.current = pending;
|
|
63
|
+
}, [errorMessage, pending]);
|
|
64
|
+
|
|
65
|
+
const updateOption = <K extends keyof ExportDialogOptions>(key: K, value: ExportDialogOptions[K]) => {
|
|
66
|
+
setOptions((current) => ({ ...current, [key]: value }));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const submitExport = () => {
|
|
70
|
+
if (submissionInProgress.current) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
submissionInProgress.current = true;
|
|
74
|
+
setSubmitted(true);
|
|
75
|
+
updateSetting('exportDefaults', options);
|
|
76
|
+
onExport({ ...options, zipArchive: effectiveZipArchive });
|
|
77
|
+
};
|
|
58
78
|
|
|
59
79
|
return (
|
|
60
80
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
@@ -62,7 +82,7 @@ export function ExportDialog({
|
|
|
62
82
|
<DialogHeader>
|
|
63
83
|
<DialogTitle>{title}</DialogTitle>
|
|
64
84
|
<DialogDescription className="text-[var(--muted-foreground)]">
|
|
65
|
-
Choose the transcript format and
|
|
85
|
+
Choose the transcript format and export options.
|
|
66
86
|
</DialogDescription>
|
|
67
87
|
</DialogHeader>
|
|
68
88
|
|
|
@@ -71,7 +91,10 @@ export function ExportDialog({
|
|
|
71
91
|
<label className="font-medium text-sm" htmlFor="output-format">
|
|
72
92
|
Output format
|
|
73
93
|
</label>
|
|
74
|
-
<Select
|
|
94
|
+
<Select
|
|
95
|
+
value={options.outputFormat}
|
|
96
|
+
onValueChange={(value) => updateOption('outputFormat', value as 'md' | 'txt')}
|
|
97
|
+
>
|
|
75
98
|
<SelectTrigger
|
|
76
99
|
id="output-format"
|
|
77
100
|
className="border-[var(--border)] bg-[var(--panel-secondary)] text-[var(--foreground)]"
|
|
@@ -79,18 +102,8 @@ export function ExportDialog({
|
|
|
79
102
|
<SelectValue placeholder="Choose a format" />
|
|
80
103
|
</SelectTrigger>
|
|
81
104
|
<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>
|
|
105
|
+
<SelectItem value="md">Markdown (.md)</SelectItem>
|
|
106
|
+
<SelectItem value="txt">Plain text (.txt)</SelectItem>
|
|
94
107
|
</SelectContent>
|
|
95
108
|
</Select>
|
|
96
109
|
</div>
|
|
@@ -98,8 +111,8 @@ export function ExportDialog({
|
|
|
98
111
|
<div className="flex items-center gap-3 rounded-2xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3">
|
|
99
112
|
<Checkbox
|
|
100
113
|
aria-label="Include metadata"
|
|
101
|
-
checked={includeMetadata}
|
|
102
|
-
onCheckedChange={(checked) =>
|
|
114
|
+
checked={options.includeMetadata}
|
|
115
|
+
onCheckedChange={(checked) => updateOption('includeMetadata', checked === true)}
|
|
103
116
|
/>
|
|
104
117
|
<span className="space-y-1">
|
|
105
118
|
<span className="block font-medium text-sm">Include metadata</span>
|
|
@@ -109,33 +122,37 @@ export function ExportDialog({
|
|
|
109
122
|
</span>
|
|
110
123
|
</div>
|
|
111
124
|
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<span className="
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
{showCommentaryOption ? (
|
|
126
|
+
<div className="flex items-center gap-3 rounded-2xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3">
|
|
127
|
+
<Checkbox
|
|
128
|
+
aria-label="Include commentary"
|
|
129
|
+
checked={options.includeCommentary}
|
|
130
|
+
onCheckedChange={(checked) => updateOption('includeCommentary', checked === true)}
|
|
131
|
+
/>
|
|
132
|
+
<span className="space-y-1">
|
|
133
|
+
<span className="block font-medium text-sm">Include commentary</span>
|
|
134
|
+
<span className="block text-[var(--muted-foreground)] text-sm">
|
|
135
|
+
Includes assistant commentary-phase updates in the exported transcript.
|
|
136
|
+
</span>
|
|
122
137
|
</span>
|
|
123
|
-
</
|
|
124
|
-
|
|
138
|
+
</div>
|
|
139
|
+
) : null}
|
|
125
140
|
|
|
126
|
-
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<span className="
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
{showToolsOption ? (
|
|
142
|
+
<div className="flex items-center gap-3 rounded-2xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3">
|
|
143
|
+
<Checkbox
|
|
144
|
+
aria-label="Include tool calls"
|
|
145
|
+
checked={options.includeTools}
|
|
146
|
+
onCheckedChange={(checked) => updateOption('includeTools', checked === true)}
|
|
147
|
+
/>
|
|
148
|
+
<span className="space-y-1">
|
|
149
|
+
<span className="block font-medium text-sm">Include tool calls</span>
|
|
150
|
+
<span className="block text-[var(--muted-foreground)] text-sm">
|
|
151
|
+
Includes tool-call summaries and tool-output summaries in the export.
|
|
152
|
+
</span>
|
|
136
153
|
</span>
|
|
137
|
-
</
|
|
138
|
-
|
|
154
|
+
</div>
|
|
155
|
+
) : null}
|
|
139
156
|
|
|
140
157
|
<div className="flex items-center gap-3 rounded-2xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3">
|
|
141
158
|
<Checkbox
|
|
@@ -143,7 +160,7 @@ export function ExportDialog({
|
|
|
143
160
|
aria-describedby={zipDescriptionId}
|
|
144
161
|
checked={effectiveZipArchive}
|
|
145
162
|
disabled={forceZipArchive}
|
|
146
|
-
onCheckedChange={(checked) =>
|
|
163
|
+
onCheckedChange={(checked) => updateOption('zipArchive', checked === true)}
|
|
147
164
|
/>
|
|
148
165
|
<span className="space-y-1">
|
|
149
166
|
<span className="block font-medium text-sm">Zip archive</span>
|
|
@@ -162,19 +179,7 @@ export function ExportDialog({
|
|
|
162
179
|
<Button className="rounded-full" variant="outline" onClick={() => onOpenChange(false)}>
|
|
163
180
|
Cancel
|
|
164
181
|
</Button>
|
|
165
|
-
<Button
|
|
166
|
-
className="rounded-full"
|
|
167
|
-
disabled={pending || disabled}
|
|
168
|
-
onClick={() =>
|
|
169
|
-
onExport({
|
|
170
|
-
includeCommentary,
|
|
171
|
-
includeMetadata,
|
|
172
|
-
includeTools,
|
|
173
|
-
outputFormat,
|
|
174
|
-
zipArchive: effectiveZipArchive,
|
|
175
|
-
})
|
|
176
|
-
}
|
|
177
|
-
>
|
|
182
|
+
<Button className="rounded-full" disabled={pending || disabled || submitted} onClick={submitExport}>
|
|
178
183
|
{pending ? 'Exporting...' : 'Download export'}
|
|
179
184
|
</Button>
|
|
180
185
|
</DialogFooter>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { GrokSessionSummary } from '@spiracha/lib/grok-exporter-types';
|
|
2
|
+
import { Link } from '@tanstack/react-router';
|
|
3
|
+
import type { SortingState } from '@tanstack/react-table';
|
|
4
|
+
import { createColumnHelper } from '@tanstack/react-table';
|
|
5
|
+
import { Download, MoreHorizontal, Trash2 } from 'lucide-react';
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import { DataTable } from '#/components/data-table';
|
|
8
|
+
import { SelectionActionsToolbar } from '#/components/selection-actions-toolbar';
|
|
9
|
+
import { Button } from '#/components/ui/button';
|
|
10
|
+
import {
|
|
11
|
+
DropdownMenu,
|
|
12
|
+
DropdownMenuContent,
|
|
13
|
+
DropdownMenuItem,
|
|
14
|
+
DropdownMenuTrigger,
|
|
15
|
+
} from '#/components/ui/dropdown-menu';
|
|
16
|
+
import { formatDateTime, formatNumber } from '#/lib/formatters';
|
|
17
|
+
|
|
18
|
+
type GrokSessionsTableProps = {
|
|
19
|
+
onDeleteSession: (session: GrokSessionSummary) => void;
|
|
20
|
+
onDeleteSessions: (sessionIds: string[]) => void;
|
|
21
|
+
onExportSession: (session: GrokSessionSummary) => void;
|
|
22
|
+
onExportSessions: (sessionIds: string[]) => void;
|
|
23
|
+
sessions: GrokSessionSummary[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const columnHelper = createColumnHelper<GrokSessionSummary>();
|
|
27
|
+
const defaultSorting: SortingState = [{ desc: true, id: 'lastActive' }];
|
|
28
|
+
|
|
29
|
+
const columns = (
|
|
30
|
+
onDeleteSession: (session: GrokSessionSummary) => void,
|
|
31
|
+
onExportSession: (session: GrokSessionSummary) => void,
|
|
32
|
+
) =>
|
|
33
|
+
[
|
|
34
|
+
columnHelper.accessor('title', {
|
|
35
|
+
cell: (info) => (
|
|
36
|
+
<Link
|
|
37
|
+
className="block w-[16rem] max-w-[22rem] space-y-1 rounded-md outline-none transition hover:opacity-80 focus-visible:ring-2 focus-visible:ring-[var(--accent)] lg:w-auto"
|
|
38
|
+
params={{ sessionId: info.row.original.sessionId }}
|
|
39
|
+
to="/grok-sessions/$sessionId"
|
|
40
|
+
>
|
|
41
|
+
<p className="truncate font-medium underline-offset-2 hover:underline">{info.getValue()}</p>
|
|
42
|
+
<p className="truncate text-[var(--muted-foreground)] text-xs">{info.row.original.sessionId}</p>
|
|
43
|
+
</Link>
|
|
44
|
+
),
|
|
45
|
+
header: 'Session',
|
|
46
|
+
}),
|
|
47
|
+
columnHelper.accessor('lastActiveAtMs', {
|
|
48
|
+
cell: (info) => (
|
|
49
|
+
<span className="whitespace-nowrap text-sm" suppressHydrationWarning>
|
|
50
|
+
{formatDateTime(info.getValue())}
|
|
51
|
+
</span>
|
|
52
|
+
),
|
|
53
|
+
header: 'Updated',
|
|
54
|
+
id: 'lastActive',
|
|
55
|
+
}),
|
|
56
|
+
columnHelper.accessor('modelLabel', {
|
|
57
|
+
cell: (info) => (
|
|
58
|
+
<span className="text-sm">{info.getValue() ?? info.row.original.currentModelId ?? 'unknown'}</span>
|
|
59
|
+
),
|
|
60
|
+
header: 'Model',
|
|
61
|
+
}),
|
|
62
|
+
columnHelper.accessor('agentName', {
|
|
63
|
+
cell: (info) => <span className="font-mono text-sm">{info.getValue() ?? 'unknown'}</span>,
|
|
64
|
+
header: 'Agent',
|
|
65
|
+
}),
|
|
66
|
+
columnHelper.accessor('messageCount', {
|
|
67
|
+
cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
|
|
68
|
+
header: 'Messages',
|
|
69
|
+
}),
|
|
70
|
+
columnHelper.accessor('toolCallCount', {
|
|
71
|
+
cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
|
|
72
|
+
header: 'Tools',
|
|
73
|
+
}),
|
|
74
|
+
columnHelper.display({
|
|
75
|
+
cell: (info) => (
|
|
76
|
+
<DropdownMenu>
|
|
77
|
+
<DropdownMenuTrigger asChild>
|
|
78
|
+
<Button
|
|
79
|
+
aria-label={`Actions for ${info.row.original.title}`}
|
|
80
|
+
className="rounded-full"
|
|
81
|
+
size="icon"
|
|
82
|
+
type="button"
|
|
83
|
+
variant="ghost"
|
|
84
|
+
onClick={(event) => event.stopPropagation()}
|
|
85
|
+
>
|
|
86
|
+
<MoreHorizontal className="size-4" />
|
|
87
|
+
</Button>
|
|
88
|
+
</DropdownMenuTrigger>
|
|
89
|
+
<DropdownMenuContent align="end">
|
|
90
|
+
<DropdownMenuItem
|
|
91
|
+
disabled={info.row.original.renderablePartCount === 0}
|
|
92
|
+
onClick={() => onExportSession(info.row.original)}
|
|
93
|
+
>
|
|
94
|
+
<Download className="mr-2 size-4" />
|
|
95
|
+
Export session
|
|
96
|
+
</DropdownMenuItem>
|
|
97
|
+
<DropdownMenuItem
|
|
98
|
+
className="text-[var(--destructive)]"
|
|
99
|
+
onClick={() => onDeleteSession(info.row.original)}
|
|
100
|
+
>
|
|
101
|
+
<Trash2 className="mr-2 size-4" />
|
|
102
|
+
Delete session
|
|
103
|
+
</DropdownMenuItem>
|
|
104
|
+
</DropdownMenuContent>
|
|
105
|
+
</DropdownMenu>
|
|
106
|
+
),
|
|
107
|
+
enableSorting: false,
|
|
108
|
+
header: '',
|
|
109
|
+
id: 'actions',
|
|
110
|
+
}),
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
export const GrokSessionsTable = ({
|
|
114
|
+
onDeleteSession,
|
|
115
|
+
onDeleteSessions,
|
|
116
|
+
onExportSession,
|
|
117
|
+
onExportSessions,
|
|
118
|
+
sessions,
|
|
119
|
+
}: GrokSessionsTableProps) => {
|
|
120
|
+
const tableColumns = useMemo(() => columns(onDeleteSession, onExportSession), [onDeleteSession, onExportSession]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<DataTable
|
|
124
|
+
columns={tableColumns}
|
|
125
|
+
data={sessions}
|
|
126
|
+
emptyMessage="No Grok sessions match the current workspace filter."
|
|
127
|
+
enableRowSelection
|
|
128
|
+
getRowId={(row) => row.sessionId}
|
|
129
|
+
initialSorting={defaultSorting}
|
|
130
|
+
renderToolbar={({ clearSelection, selectedRows }) => {
|
|
131
|
+
const selectedSessionIds = selectedRows.map((row) => row.sessionId);
|
|
132
|
+
const hasEmptySelection = selectedRows.some((row) => row.renderablePartCount === 0);
|
|
133
|
+
return (
|
|
134
|
+
<SelectionActionsToolbar
|
|
135
|
+
clearSelection={clearSelection}
|
|
136
|
+
exportDisabled={hasEmptySelection}
|
|
137
|
+
itemLabel="session"
|
|
138
|
+
selectedCount={selectedRows.length}
|
|
139
|
+
onDeleteSelected={() => onDeleteSessions(selectedSessionIds)}
|
|
140
|
+
onExportSelected={() => onExportSessions(selectedSessionIds)}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { GrokWorkspaceGroup } from '@spiracha/lib/grok-exporter-types';
|
|
2
|
+
import { Link } from '@tanstack/react-router';
|
|
3
|
+
import { createColumnHelper } from '@tanstack/react-table';
|
|
4
|
+
import { DataTable } from '#/components/data-table';
|
|
5
|
+
import { formatDateTime, formatNumber } from '#/lib/formatters';
|
|
6
|
+
|
|
7
|
+
type GrokWorkspacesTableProps = {
|
|
8
|
+
workspaces: GrokWorkspaceGroup[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const columnHelper = createColumnHelper<GrokWorkspaceGroup>();
|
|
12
|
+
|
|
13
|
+
const columns = [
|
|
14
|
+
columnHelper.accessor('label', {
|
|
15
|
+
cell: (info) => (
|
|
16
|
+
<Link
|
|
17
|
+
className="block w-[16rem] max-w-[22rem] space-y-1 rounded-md outline-none transition hover:opacity-80 focus-visible:ring-2 focus-visible:ring-[var(--accent)] lg:w-auto"
|
|
18
|
+
params={{ workspaceKey: info.row.original.key }}
|
|
19
|
+
to="/grok/$workspaceKey"
|
|
20
|
+
>
|
|
21
|
+
<p className="truncate font-medium underline-offset-2 hover:underline">{info.getValue()}</p>
|
|
22
|
+
<p className="truncate text-[var(--muted-foreground)] text-xs">{info.row.original.worktree}</p>
|
|
23
|
+
</Link>
|
|
24
|
+
),
|
|
25
|
+
header: 'Workspace',
|
|
26
|
+
}),
|
|
27
|
+
columnHelper.accessor('sessionCount', {
|
|
28
|
+
cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
|
|
29
|
+
header: 'Sessions',
|
|
30
|
+
}),
|
|
31
|
+
columnHelper.accessor('messageCount', {
|
|
32
|
+
cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
|
|
33
|
+
header: 'Messages',
|
|
34
|
+
}),
|
|
35
|
+
columnHelper.accessor('toolCallCount', {
|
|
36
|
+
cell: (info) => <span className="font-mono text-sm">{formatNumber(info.getValue())}</span>,
|
|
37
|
+
header: 'Tools',
|
|
38
|
+
}),
|
|
39
|
+
columnHelper.accessor('lastActiveAtMs', {
|
|
40
|
+
cell: (info) => (
|
|
41
|
+
<span className="whitespace-nowrap text-sm" suppressHydrationWarning>
|
|
42
|
+
{formatDateTime(info.getValue())}
|
|
43
|
+
</span>
|
|
44
|
+
),
|
|
45
|
+
header: 'Last updated',
|
|
46
|
+
}),
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export const GrokWorkspacesTable = ({ workspaces }: GrokWorkspacesTableProps) => {
|
|
50
|
+
return (
|
|
51
|
+
<DataTable columns={columns} data={workspaces} emptyMessage="No Grok workspaces match the current search." />
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -2,9 +2,10 @@ import type { KiroSessionSummary } from '@spiracha/lib/kiro-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 } 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 } from '#/lib/formatters';
|
|
16
17
|
|
|
17
18
|
type KiroSessionsTableProps = {
|
|
19
|
+
onDeleteSession: (session: KiroSessionSummary) => void;
|
|
20
|
+
onDeleteSessions: (sessionIds: string[]) => void;
|
|
18
21
|
onExportSession: (session: KiroSessionSummary) => void;
|
|
22
|
+
onExportSessions: (sessionIds: string[]) => void;
|
|
19
23
|
sessions: KiroSessionSummary[];
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
const columnHelper = createColumnHelper<KiroSessionSummary>();
|
|
23
27
|
const defaultSorting: SortingState = [{ desc: true, id: 'lastActive' }];
|
|
24
28
|
|
|
25
|
-
const columns = (
|
|
29
|
+
const columns = (
|
|
30
|
+
onDeleteSession: (session: KiroSessionSummary) => void,
|
|
31
|
+
onExportSession: (session: KiroSessionSummary) => void,
|
|
32
|
+
) =>
|
|
26
33
|
[
|
|
27
34
|
columnHelper.accessor('title', {
|
|
28
35
|
cell: (info) => (
|
|
@@ -89,6 +96,13 @@ const columns = (onExportSession: (session: KiroSessionSummary) => void) =>
|
|
|
89
96
|
<Download className="mr-2 size-4" />
|
|
90
97
|
Export session
|
|
91
98
|
</DropdownMenuItem>
|
|
99
|
+
<DropdownMenuItem
|
|
100
|
+
className="text-[var(--destructive)]"
|
|
101
|
+
onClick={() => onDeleteSession(info.row.original)}
|
|
102
|
+
>
|
|
103
|
+
<Trash2 className="mr-2 size-4" />
|
|
104
|
+
Delete session
|
|
105
|
+
</DropdownMenuItem>
|
|
92
106
|
</DropdownMenuContent>
|
|
93
107
|
</DropdownMenu>
|
|
94
108
|
),
|
|
@@ -98,15 +112,37 @@ const columns = (onExportSession: (session: KiroSessionSummary) => void) =>
|
|
|
98
112
|
}),
|
|
99
113
|
] as const;
|
|
100
114
|
|
|
101
|
-
export const KiroSessionsTable = ({
|
|
102
|
-
|
|
115
|
+
export const KiroSessionsTable = ({
|
|
116
|
+
onDeleteSession,
|
|
117
|
+
onDeleteSessions,
|
|
118
|
+
onExportSession,
|
|
119
|
+
onExportSessions,
|
|
120
|
+
sessions,
|
|
121
|
+
}: KiroSessionsTableProps) => {
|
|
122
|
+
const tableColumns = useMemo(() => columns(onDeleteSession, onExportSession), [onDeleteSession, onExportSession]);
|
|
103
123
|
|
|
104
124
|
return (
|
|
105
125
|
<DataTable
|
|
106
126
|
columns={tableColumns}
|
|
107
127
|
data={sessions}
|
|
108
128
|
emptyMessage="No Kiro sessions match the current workspace filter."
|
|
129
|
+
enableRowSelection
|
|
130
|
+
getRowId={(row) => row.sessionId}
|
|
109
131
|
initialSorting={defaultSorting}
|
|
132
|
+
renderToolbar={({ clearSelection, selectedRows }) => {
|
|
133
|
+
const selectedSessionIds = selectedRows.map((row) => row.sessionId);
|
|
134
|
+
const hasEmptySelection = selectedRows.some((row) => row.renderablePartCount === 0);
|
|
135
|
+
return (
|
|
136
|
+
<SelectionActionsToolbar
|
|
137
|
+
clearSelection={clearSelection}
|
|
138
|
+
exportDisabled={hasEmptySelection}
|
|
139
|
+
itemLabel="session"
|
|
140
|
+
selectedCount={selectedRows.length}
|
|
141
|
+
onDeleteSelected={() => onDeleteSessions(selectedSessionIds)}
|
|
142
|
+
onExportSelected={() => onExportSessions(selectedSessionIds)}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
}}
|
|
110
146
|
/>
|
|
111
147
|
);
|
|
112
148
|
};
|
|
@@ -2,9 +2,10 @@ import type { OpenCodeSessionSummary } from '@spiracha/lib/opencode-exporter-typ
|
|
|
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 { Badge } from '#/components/ui/badge';
|
|
9
10
|
import { Button } from '#/components/ui/button';
|
|
10
11
|
import {
|
|
@@ -16,7 +17,10 @@ import {
|
|
|
16
17
|
import { formatDateTime, formatNumber, formatTokens } from '#/lib/formatters';
|
|
17
18
|
|
|
18
19
|
type OpenCodeSessionsTableProps = {
|
|
20
|
+
onDeleteSession: (session: OpenCodeSessionSummary) => void;
|
|
21
|
+
onDeleteSessions: (sessionIds: string[]) => void;
|
|
19
22
|
onExportSession: (session: OpenCodeSessionSummary) => void;
|
|
23
|
+
onExportSessions: (sessionIds: string[]) => void;
|
|
20
24
|
sessions: OpenCodeSessionSummary[];
|
|
21
25
|
};
|
|
22
26
|
|
|
@@ -31,7 +35,10 @@ const formatCost = (value: number) => {
|
|
|
31
35
|
return `$${value.toFixed(value < 0.01 ? 4 : 2)}`;
|
|
32
36
|
};
|
|
33
37
|
|
|
34
|
-
const columns = (
|
|
38
|
+
const columns = (
|
|
39
|
+
onDeleteSession: (session: OpenCodeSessionSummary) => void,
|
|
40
|
+
onExportSession: (session: OpenCodeSessionSummary) => void,
|
|
41
|
+
) =>
|
|
35
42
|
[
|
|
36
43
|
columnHelper.accessor('title', {
|
|
37
44
|
cell: (info) => (
|
|
@@ -105,6 +112,13 @@ const columns = (onExportSession: (session: OpenCodeSessionSummary) => void) =>
|
|
|
105
112
|
<Download className="mr-2 size-4" />
|
|
106
113
|
Export session
|
|
107
114
|
</DropdownMenuItem>
|
|
115
|
+
<DropdownMenuItem
|
|
116
|
+
className="text-[var(--destructive)]"
|
|
117
|
+
onClick={() => onDeleteSession(info.row.original)}
|
|
118
|
+
>
|
|
119
|
+
<Trash2 className="mr-2 size-4" />
|
|
120
|
+
Delete session
|
|
121
|
+
</DropdownMenuItem>
|
|
108
122
|
</DropdownMenuContent>
|
|
109
123
|
</DropdownMenu>
|
|
110
124
|
),
|
|
@@ -114,15 +128,37 @@ const columns = (onExportSession: (session: OpenCodeSessionSummary) => void) =>
|
|
|
114
128
|
}),
|
|
115
129
|
] as const;
|
|
116
130
|
|
|
117
|
-
export const OpenCodeSessionsTable = ({
|
|
118
|
-
|
|
131
|
+
export const OpenCodeSessionsTable = ({
|
|
132
|
+
onDeleteSession,
|
|
133
|
+
onDeleteSessions,
|
|
134
|
+
onExportSession,
|
|
135
|
+
onExportSessions,
|
|
136
|
+
sessions,
|
|
137
|
+
}: OpenCodeSessionsTableProps) => {
|
|
138
|
+
const tableColumns = useMemo(() => columns(onDeleteSession, onExportSession), [onDeleteSession, onExportSession]);
|
|
119
139
|
|
|
120
140
|
return (
|
|
121
141
|
<DataTable
|
|
122
142
|
columns={tableColumns}
|
|
123
143
|
data={sessions}
|
|
124
144
|
emptyMessage="No OpenCode sessions match the current workspace filter."
|
|
145
|
+
enableRowSelection
|
|
146
|
+
getRowId={(row) => row.sessionId}
|
|
125
147
|
initialSorting={defaultSorting}
|
|
148
|
+
renderToolbar={({ clearSelection, selectedRows }) => {
|
|
149
|
+
const selectedSessionIds = selectedRows.map((row) => row.sessionId);
|
|
150
|
+
const hasEmptySelection = selectedRows.some((row) => row.renderablePartCount === 0);
|
|
151
|
+
return (
|
|
152
|
+
<SelectionActionsToolbar
|
|
153
|
+
clearSelection={clearSelection}
|
|
154
|
+
exportDisabled={hasEmptySelection}
|
|
155
|
+
itemLabel="session"
|
|
156
|
+
selectedCount={selectedRows.length}
|
|
157
|
+
onDeleteSelected={() => onDeleteSessions(selectedSessionIds)}
|
|
158
|
+
onExportSelected={() => onExportSessions(selectedSessionIds)}
|
|
159
|
+
/>
|
|
160
|
+
);
|
|
161
|
+
}}
|
|
126
162
|
/>
|
|
127
163
|
);
|
|
128
164
|
};
|
|
@@ -5,10 +5,12 @@ type PageHeaderProps = {
|
|
|
5
5
|
breadcrumb?: ReactNode;
|
|
6
6
|
eyebrow?: string;
|
|
7
7
|
subtitle?: string;
|
|
8
|
-
title
|
|
8
|
+
title?: string;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export function PageHeader({ actions, breadcrumb, eyebrow, subtitle, title }: PageHeaderProps) {
|
|
12
|
+
const hasHeading = Boolean(title || subtitle);
|
|
13
|
+
|
|
12
14
|
return (
|
|
13
15
|
<div className="flex flex-col gap-4 border-[var(--border)] border-b pb-5 sm:flex-row sm:items-end sm:justify-between">
|
|
14
16
|
<div className="space-y-2">
|
|
@@ -18,14 +20,18 @@ export function PageHeader({ actions, breadcrumb, eyebrow, subtitle, title }: Pa
|
|
|
18
20
|
{eyebrow}
|
|
19
21
|
</p>
|
|
20
22
|
) : null}
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
{hasHeading ? (
|
|
24
|
+
<div>
|
|
25
|
+
{title ? (
|
|
26
|
+
<h2 className="font-semibold text-2xl tracking-[-0.03em] sm:text-[2rem]">{title}</h2>
|
|
27
|
+
) : null}
|
|
28
|
+
{subtitle ? (
|
|
29
|
+
<p className="mt-2 max-w-[60rem] whitespace-pre-wrap break-words text-[var(--muted-foreground)] text-sm">
|
|
30
|
+
{subtitle}
|
|
31
|
+
</p>
|
|
32
|
+
) : null}
|
|
33
|
+
</div>
|
|
34
|
+
) : null}
|
|
29
35
|
</div>
|
|
30
36
|
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
|
|
31
37
|
</div>
|