spiracha 2.1.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 +37 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +17 -10
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +1 -0
- package/apps/ui/src/components/app-shell.tsx +45 -3
- package/apps/ui/src/components/data-table.tsx +17 -1
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +77 -62
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -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 +4 -1
- package/apps/ui/src/lib/antigravity-server.ts +70 -17
- package/apps/ui/src/lib/antigravity-transcript-events.ts +46 -70
- package/apps/ui/src/lib/claude-code-server.ts +14 -4
- package/apps/ui/src/lib/codex-queries.ts +3 -3
- package/apps/ui/src/lib/codex-server.ts +3 -3
- package/apps/ui/src/lib/cursor-server.ts +41 -69
- 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/grok-server.ts +2 -3
- package/apps/ui/src/lib/kiro-server.ts +14 -2
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +13 -2
- 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-transcript-events.ts +1 -1
- 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 +1 -1
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +8 -0
- package/apps/ui/src/routeTree.gen.ts +282 -282
- 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 +73 -19
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +42 -60
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +25 -11
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +27 -32
- 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 +29 -16
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +33 -27
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +20 -26
- package/apps/ui/src/routes/grok.index.tsx +2 -2
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +27 -32
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +29 -36
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +45 -61
- package/apps/ui/src/routes/opencode.index.tsx +8 -17
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +12 -11
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +19 -25
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +87 -56
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +26 -17
- package/src/client.ts +61 -16
- package/src/lib/antigravity-db.ts +236 -82
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +45 -19
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +2 -7
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +88 -33
- package/src/lib/codex-thread-parser.ts +78 -35
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-renderer.ts +49 -61
- package/src/lib/concurrency.ts +15 -3
- package/src/lib/conversation-api.ts +34 -31
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +54 -29
- package/src/lib/conversation-data/claude-code-adapter.ts +32 -6
- package/src/lib/conversation-data/codex-adapter.ts +91 -80
- package/src/lib/conversation-data/cursor-adapter.ts +48 -34
- package/src/lib/conversation-data/grok-adapter.ts +19 -6
- package/src/lib/conversation-data/index.ts +29 -10
- package/src/lib/conversation-data/kiro-adapter.ts +18 -5
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +43 -15
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +23 -18
- package/src/lib/conversation-zip-export.ts +20 -2
- package/src/lib/cursor-db.ts +34 -23
- 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 +76 -34
- package/src/lib/grok-transcript-phase.ts +11 -2
- package/src/lib/grok-transcript.ts +15 -2
- package/src/lib/kiro-db.ts +161 -68
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/opencode-db.ts +4 -8
- package/src/lib/opencode-think-tags.ts +17 -3
- 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 +51 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +8 -3
- package/src/lib/ui-cache.ts +152 -17
- package/src/lib/ui-export-archive.ts +14 -55
- 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,7 +1,6 @@
|
|
|
1
1
|
import { useEffect, useId, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
AlertDialog,
|
|
4
|
-
AlertDialogAction,
|
|
5
4
|
AlertDialogCancel,
|
|
6
5
|
AlertDialogContent,
|
|
7
6
|
AlertDialogDescription,
|
|
@@ -9,6 +8,7 @@ import {
|
|
|
9
8
|
AlertDialogHeader,
|
|
10
9
|
AlertDialogTitle,
|
|
11
10
|
} from '#/components/ui/alert-dialog';
|
|
11
|
+
import { Button } from '#/components/ui/button';
|
|
12
12
|
import { Checkbox } from '#/components/ui/checkbox';
|
|
13
13
|
|
|
14
14
|
type DeleteConfirmDialogProps = {
|
|
@@ -75,12 +75,13 @@ export function DeleteConfirmDialog({
|
|
|
75
75
|
{errorMessage ? <p className="text-[var(--destructive)] text-sm">{errorMessage}</p> : null}
|
|
76
76
|
<AlertDialogFooter>
|
|
77
77
|
<AlertDialogCancel className="border-[var(--border)]">Cancel</AlertDialogCancel>
|
|
78
|
-
<
|
|
78
|
+
<Button
|
|
79
79
|
className="bg-[var(--destructive)] text-[var(--destructive-foreground)] hover:bg-[var(--destructive)]/90"
|
|
80
|
+
type="button"
|
|
80
81
|
onClick={() => onConfirm({ deleteSessionFiles })}
|
|
81
82
|
>
|
|
82
83
|
{confirmLabel}
|
|
83
|
-
</
|
|
84
|
+
</Button>
|
|
84
85
|
</AlertDialogFooter>
|
|
85
86
|
</AlertDialogContent>
|
|
86
87
|
</AlertDialog>
|
|
@@ -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)]"
|
|
@@ -88,8 +111,8 @@ export function ExportDialog({
|
|
|
88
111
|
<div className="flex items-center gap-3 rounded-2xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3">
|
|
89
112
|
<Checkbox
|
|
90
113
|
aria-label="Include metadata"
|
|
91
|
-
checked={includeMetadata}
|
|
92
|
-
onCheckedChange={(checked) =>
|
|
114
|
+
checked={options.includeMetadata}
|
|
115
|
+
onCheckedChange={(checked) => updateOption('includeMetadata', checked === true)}
|
|
93
116
|
/>
|
|
94
117
|
<span className="space-y-1">
|
|
95
118
|
<span className="block font-medium text-sm">Include metadata</span>
|
|
@@ -99,33 +122,37 @@ export function ExportDialog({
|
|
|
99
122
|
</span>
|
|
100
123
|
</div>
|
|
101
124
|
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<span className="
|
|
110
|
-
|
|
111
|
-
|
|
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>
|
|
112
137
|
</span>
|
|
113
|
-
</
|
|
114
|
-
|
|
138
|
+
</div>
|
|
139
|
+
) : null}
|
|
115
140
|
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<span className="
|
|
124
|
-
|
|
125
|
-
|
|
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>
|
|
126
153
|
</span>
|
|
127
|
-
</
|
|
128
|
-
|
|
154
|
+
</div>
|
|
155
|
+
) : null}
|
|
129
156
|
|
|
130
157
|
<div className="flex items-center gap-3 rounded-2xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3">
|
|
131
158
|
<Checkbox
|
|
@@ -133,7 +160,7 @@ export function ExportDialog({
|
|
|
133
160
|
aria-describedby={zipDescriptionId}
|
|
134
161
|
checked={effectiveZipArchive}
|
|
135
162
|
disabled={forceZipArchive}
|
|
136
|
-
onCheckedChange={(checked) =>
|
|
163
|
+
onCheckedChange={(checked) => updateOption('zipArchive', checked === true)}
|
|
137
164
|
/>
|
|
138
165
|
<span className="space-y-1">
|
|
139
166
|
<span className="block font-medium text-sm">Zip archive</span>
|
|
@@ -152,19 +179,7 @@ export function ExportDialog({
|
|
|
152
179
|
<Button className="rounded-full" variant="outline" onClick={() => onOpenChange(false)}>
|
|
153
180
|
Cancel
|
|
154
181
|
</Button>
|
|
155
|
-
<Button
|
|
156
|
-
className="rounded-full"
|
|
157
|
-
disabled={pending || disabled}
|
|
158
|
-
onClick={() =>
|
|
159
|
-
onExport({
|
|
160
|
-
includeCommentary,
|
|
161
|
-
includeMetadata,
|
|
162
|
-
includeTools,
|
|
163
|
-
outputFormat,
|
|
164
|
-
zipArchive: effectiveZipArchive,
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
>
|
|
182
|
+
<Button className="rounded-full" disabled={pending || disabled || submitted} onClick={submitExport}>
|
|
168
183
|
{pending ? 'Exporting...' : 'Download export'}
|
|
169
184
|
</Button>
|
|
170
185
|
</DialogFooter>
|
|
@@ -2,6 +2,7 @@ import type { ProjectSummary } from '@spiracha/lib/codex-browser-types';
|
|
|
2
2
|
import { Link } from '@tanstack/react-router';
|
|
3
3
|
import { createColumnHelper } from '@tanstack/react-table';
|
|
4
4
|
import { MoreHorizontal, Trash2 } from 'lucide-react';
|
|
5
|
+
import { useMemo } from 'react';
|
|
5
6
|
import { DataTable } from '#/components/data-table';
|
|
6
7
|
import { Button } from '#/components/ui/button';
|
|
7
8
|
import {
|
|
@@ -71,6 +72,7 @@ const columns = (onDeleteProject: (project: ProjectSummary) => void) =>
|
|
|
71
72
|
<DropdownMenu>
|
|
72
73
|
<DropdownMenuTrigger asChild>
|
|
73
74
|
<Button
|
|
75
|
+
aria-label={`Actions for ${info.row.original.name}`}
|
|
74
76
|
className="rounded-full"
|
|
75
77
|
size="icon"
|
|
76
78
|
type="button"
|
|
@@ -97,9 +99,10 @@ const columns = (onDeleteProject: (project: ProjectSummary) => void) =>
|
|
|
97
99
|
] as const;
|
|
98
100
|
|
|
99
101
|
export function ProjectsTable({ projects, onDeleteProject }: ProjectsTableProps) {
|
|
102
|
+
const memoizedColumns = useMemo(() => columns(onDeleteProject), [onDeleteProject]);
|
|
100
103
|
return (
|
|
101
104
|
<DataTable
|
|
102
|
-
columns={
|
|
105
|
+
columns={memoizedColumns}
|
|
103
106
|
data={projects}
|
|
104
107
|
emptyMessage="No projects match the current search."
|
|
105
108
|
initialSorting={[{ desc: true, id: 'lastUpdatedAtMs' }]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getErrorPresentation } from '#/lib/error-presentation';
|
|
2
|
+
import { ReloadErrorPanel } from './reload-error-panel';
|
|
3
|
+
|
|
4
|
+
type RouteErrorPanelProps = {
|
|
5
|
+
error: Error;
|
|
6
|
+
title: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const RouteErrorPanel = ({ error, title }: RouteErrorPanelProps) => {
|
|
10
|
+
const presentation = getErrorPresentation(error, { fallbackTitle: title });
|
|
11
|
+
|
|
12
|
+
return <ReloadErrorPanel description={presentation.description} title={presentation.title} />;
|
|
13
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ThreadGoal } from '@spiracha/lib/codex-browser-types';
|
|
2
|
+
import { Badge } from '#/components/ui/badge';
|
|
3
|
+
import { formatNumber } from '#/lib/formatters';
|
|
4
|
+
|
|
5
|
+
const formatGoalStatus = (status: string) => status.replaceAll(/[-_]+/gu, ' ');
|
|
6
|
+
|
|
7
|
+
const formatElapsedTime = (seconds: number) => {
|
|
8
|
+
const wholeSeconds = Math.max(0, Math.floor(seconds));
|
|
9
|
+
const hours = Math.floor(wholeSeconds / 3_600);
|
|
10
|
+
const minutes = Math.floor((wholeSeconds % 3_600) / 60);
|
|
11
|
+
const remainingSeconds = wholeSeconds % 60;
|
|
12
|
+
|
|
13
|
+
return [hours > 0 ? `${hours}h` : null, minutes > 0 ? `${minutes}m` : null, `${remainingSeconds}s`]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(' ');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const formatGoalTokens = (goal: ThreadGoal) => {
|
|
19
|
+
const used = formatNumber(goal.tokensUsed);
|
|
20
|
+
return goal.tokenBudget === null ? `${used} tokens used` : `${used} / ${formatNumber(goal.tokenBudget)} tokens`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function ThreadGoalsPanel({ goals }: { goals: ThreadGoal[] }) {
|
|
24
|
+
return (
|
|
25
|
+
<section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
26
|
+
<h3 className="font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]">Goals</h3>
|
|
27
|
+
{goals.length === 0 ? (
|
|
28
|
+
<p className="mt-4 text-[var(--muted-foreground)] text-sm">No goals were recorded for this thread.</p>
|
|
29
|
+
) : (
|
|
30
|
+
<div className="mt-4 space-y-3">
|
|
31
|
+
{goals.map((goal) => (
|
|
32
|
+
<article
|
|
33
|
+
key={goal.goalId}
|
|
34
|
+
className="rounded-xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3.5"
|
|
35
|
+
>
|
|
36
|
+
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
37
|
+
<p className="font-medium text-sm leading-6">{goal.objective}</p>
|
|
38
|
+
<Badge variant="outline">{formatGoalStatus(goal.status)}</Badge>
|
|
39
|
+
</div>
|
|
40
|
+
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[var(--muted-foreground)] text-xs">
|
|
41
|
+
<span>{formatGoalTokens(goal)}</span>
|
|
42
|
+
<span>{formatElapsedTime(goal.timeUsedSeconds)}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</article>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
</section>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { DynamicToolDefinition, ThreadEvent } from '@spiracha/lib/codex-browser-types';
|
|
2
|
+
import { type TranscriptSortOrder, TranscriptView } from '#/components/transcript-view';
|
|
3
|
+
import { Badge } from '#/components/ui/badge';
|
|
4
|
+
import { Button } from '#/components/ui/button';
|
|
5
|
+
|
|
6
|
+
type ThreadToolsPanelProps = {
|
|
7
|
+
assistantModel: string | null;
|
|
8
|
+
availableTools: DynamicToolDefinition[];
|
|
9
|
+
events: ThreadEvent[] | null;
|
|
10
|
+
loadingTranscript?: boolean;
|
|
11
|
+
projectPath: string | null;
|
|
12
|
+
showRawJson: boolean;
|
|
13
|
+
sortOrder: TranscriptSortOrder;
|
|
14
|
+
transcriptIsPartial?: boolean;
|
|
15
|
+
transcriptState: 'available' | 'deferred' | 'missing';
|
|
16
|
+
onLoadTranscript?: () => void;
|
|
17
|
+
onSortOrderChange?: (value: TranscriptSortOrder) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const getToolActivityEvents = (events: ThreadEvent[]) =>
|
|
21
|
+
events.filter((event) => event.kind === 'tool_call' || event.kind === 'tool_output' || event.kind === 'web_search');
|
|
22
|
+
|
|
23
|
+
const sortJsonKeys = (value: unknown): unknown => {
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
return value.map(sortJsonKeys);
|
|
26
|
+
}
|
|
27
|
+
if (value && typeof value === 'object') {
|
|
28
|
+
return Object.fromEntries(
|
|
29
|
+
Object.entries(value)
|
|
30
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
31
|
+
.map(([key, child]) => [key, sortJsonKeys(child)]),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getToolDefinitionKey = (tool: DynamicToolDefinition) =>
|
|
38
|
+
JSON.stringify([tool.namespace, tool.name, tool.description, tool.deferLoading, sortJsonKeys(tool.inputSchema)]);
|
|
39
|
+
|
|
40
|
+
const ToolDefinitions = ({ tools }: { tools: DynamicToolDefinition[] }) => {
|
|
41
|
+
const uniqueTools = [...new Map(tools.map((tool) => [getToolDefinitionKey(tool), tool])).entries()];
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<section className="rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
45
|
+
<h3 className="font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]">
|
|
46
|
+
Available tools
|
|
47
|
+
</h3>
|
|
48
|
+
{uniqueTools.length === 0 ? (
|
|
49
|
+
<p className="mt-4 text-[var(--muted-foreground)] text-sm">
|
|
50
|
+
No dynamic tool definitions were recorded for this thread.
|
|
51
|
+
</p>
|
|
52
|
+
) : (
|
|
53
|
+
<div className="mt-4 grid gap-3 xl:grid-cols-2">
|
|
54
|
+
{uniqueTools.map(([toolKey, tool]) => (
|
|
55
|
+
<article
|
|
56
|
+
key={toolKey}
|
|
57
|
+
className="rounded-xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3.5"
|
|
58
|
+
>
|
|
59
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
60
|
+
<p className="font-medium font-mono text-sm">{tool.name}</p>
|
|
61
|
+
{tool.namespace ? <Badge variant="outline">{tool.namespace}</Badge> : null}
|
|
62
|
+
{tool.deferLoading ? <Badge variant="outline">deferred</Badge> : null}
|
|
63
|
+
</div>
|
|
64
|
+
<p className="mt-1.5 text-[var(--muted-foreground)] text-sm">
|
|
65
|
+
{tool.description || 'No description.'}
|
|
66
|
+
</p>
|
|
67
|
+
{tool.inputSchema ? (
|
|
68
|
+
<details className="mt-3 text-xs">
|
|
69
|
+
<summary className="cursor-pointer text-[var(--muted-foreground)]">
|
|
70
|
+
Input schema
|
|
71
|
+
</summary>
|
|
72
|
+
<pre className="mt-2 overflow-x-auto rounded-lg bg-[var(--code-background)] p-3 text-[var(--code-foreground)] leading-5">
|
|
73
|
+
{JSON.stringify(tool.inputSchema, null, 2)}
|
|
74
|
+
</pre>
|
|
75
|
+
</details>
|
|
76
|
+
) : null}
|
|
77
|
+
</article>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</section>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export function ThreadToolsPanel({
|
|
86
|
+
assistantModel,
|
|
87
|
+
availableTools,
|
|
88
|
+
events,
|
|
89
|
+
loadingTranscript = false,
|
|
90
|
+
projectPath,
|
|
91
|
+
showRawJson,
|
|
92
|
+
sortOrder,
|
|
93
|
+
transcriptIsPartial = false,
|
|
94
|
+
transcriptState,
|
|
95
|
+
onLoadTranscript,
|
|
96
|
+
onSortOrderChange,
|
|
97
|
+
}: ThreadToolsPanelProps) {
|
|
98
|
+
const toolEvents = events ? getToolActivityEvents(events) : [];
|
|
99
|
+
const canLoadMore = transcriptState === 'deferred' || transcriptIsPartial;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="space-y-4">
|
|
103
|
+
<ToolDefinitions tools={availableTools} />
|
|
104
|
+
<section className="space-y-3 rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]">
|
|
105
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
106
|
+
<h3 className="font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]">
|
|
107
|
+
Tool activity
|
|
108
|
+
</h3>
|
|
109
|
+
{canLoadMore && onLoadTranscript ? (
|
|
110
|
+
<Button disabled={loadingTranscript} size="sm" variant="outline" onClick={onLoadTranscript}>
|
|
111
|
+
{loadingTranscript ? 'Loading tool activity...' : 'Load tool activity'}
|
|
112
|
+
</Button>
|
|
113
|
+
) : null}
|
|
114
|
+
</div>
|
|
115
|
+
{transcriptState === 'missing' ? (
|
|
116
|
+
<p className="text-[var(--muted-foreground)] text-sm">
|
|
117
|
+
The rollout file is missing, so recorded tool activity is unavailable.
|
|
118
|
+
</p>
|
|
119
|
+
) : toolEvents.length === 0 ? (
|
|
120
|
+
<p className="text-[var(--muted-foreground)] text-sm">
|
|
121
|
+
{canLoadMore
|
|
122
|
+
? 'The current transcript preview has no tool activity. Load the full thread to inspect every call.'
|
|
123
|
+
: 'No tool calls, outputs, or web searches were recorded for this thread.'}
|
|
124
|
+
</p>
|
|
125
|
+
) : (
|
|
126
|
+
<TranscriptView
|
|
127
|
+
assistantModel={assistantModel}
|
|
128
|
+
events={toolEvents}
|
|
129
|
+
projectPath={projectPath}
|
|
130
|
+
showCommentary={false}
|
|
131
|
+
showExtraEvents
|
|
132
|
+
showRawJson={showRawJson}
|
|
133
|
+
showToolCalls
|
|
134
|
+
showUserMessages={false}
|
|
135
|
+
sortOrder={sortOrder}
|
|
136
|
+
onSortOrderChange={onSortOrderChange}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
</section>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Link } from '@tanstack/react-router';
|
|
|
3
3
|
import type { SortingState } from '@tanstack/react-table';
|
|
4
4
|
import { createColumnHelper } from '@tanstack/react-table';
|
|
5
5
|
import { Download, MoreHorizontal, Trash2 } from 'lucide-react';
|
|
6
|
+
import { useMemo } from 'react';
|
|
6
7
|
import { DataTable } from '#/components/data-table';
|
|
7
8
|
import { SelectionActionsToolbar } from '#/components/selection-actions-toolbar';
|
|
8
9
|
import { Button } from '#/components/ui/button';
|
|
@@ -103,6 +104,7 @@ const columns = (
|
|
|
103
104
|
<DropdownMenu>
|
|
104
105
|
<DropdownMenuTrigger asChild>
|
|
105
106
|
<Button
|
|
107
|
+
aria-label={`Actions for ${info.row.original.thread.title}`}
|
|
106
108
|
className="rounded-full"
|
|
107
109
|
size="icon"
|
|
108
110
|
type="button"
|
|
@@ -140,9 +142,10 @@ export function ThreadsTable({
|
|
|
140
142
|
onExportThread,
|
|
141
143
|
onExportThreads,
|
|
142
144
|
}: ThreadsTableProps) {
|
|
145
|
+
const memoizedColumns = useMemo(() => columns(onDeleteThread, onExportThread), [onDeleteThread, onExportThread]);
|
|
143
146
|
return (
|
|
144
147
|
<DataTable
|
|
145
|
-
columns={
|
|
148
|
+
columns={memoizedColumns}
|
|
146
149
|
data={threads}
|
|
147
150
|
emptyMessage="No threads match the current project filter."
|
|
148
151
|
enableRowSelection
|