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
|
@@ -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' }]}
|
|
@@ -5,6 +5,7 @@ import { createColumnHelper } from '@tanstack/react-table';
|
|
|
5
5
|
import { Download, MoreHorizontal } 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,
|
|
@@ -16,6 +17,7 @@ import { formatDateTime, formatNumber } from '#/lib/formatters';
|
|
|
16
17
|
|
|
17
18
|
type QoderSessionsTableProps = {
|
|
18
19
|
onExportSession: (session: QoderSessionSummary) => void;
|
|
20
|
+
onExportSessions: (sessionIds: string[]) => void;
|
|
19
21
|
sessions: QoderSessionSummary[];
|
|
20
22
|
};
|
|
21
23
|
|
|
@@ -38,7 +40,11 @@ const columns = (onExportSession: (session: QoderSessionSummary) => void) =>
|
|
|
38
40
|
header: 'Session',
|
|
39
41
|
}),
|
|
40
42
|
columnHelper.accessor('lastActiveAtMs', {
|
|
41
|
-
cell: (info) =>
|
|
43
|
+
cell: (info) => (
|
|
44
|
+
<span className="whitespace-nowrap text-sm" suppressHydrationWarning>
|
|
45
|
+
{formatDateTime(info.getValue())}
|
|
46
|
+
</span>
|
|
47
|
+
),
|
|
42
48
|
header: 'Updated',
|
|
43
49
|
id: 'lastActive',
|
|
44
50
|
}),
|
|
@@ -98,7 +104,7 @@ const columns = (onExportSession: (session: QoderSessionSummary) => void) =>
|
|
|
98
104
|
}),
|
|
99
105
|
] as const;
|
|
100
106
|
|
|
101
|
-
export const QoderSessionsTable = ({ onExportSession, sessions }: QoderSessionsTableProps) => {
|
|
107
|
+
export const QoderSessionsTable = ({ onExportSession, onExportSessions, sessions }: QoderSessionsTableProps) => {
|
|
102
108
|
const tableColumns = useMemo(() => columns(onExportSession), [onExportSession]);
|
|
103
109
|
|
|
104
110
|
return (
|
|
@@ -106,7 +112,22 @@ export const QoderSessionsTable = ({ onExportSession, sessions }: QoderSessionsT
|
|
|
106
112
|
columns={tableColumns}
|
|
107
113
|
data={sessions}
|
|
108
114
|
emptyMessage="No Qoder sessions match the current workspace filter."
|
|
115
|
+
enableRowSelection
|
|
116
|
+
getRowId={(row) => row.sessionId}
|
|
109
117
|
initialSorting={defaultSorting}
|
|
118
|
+
renderToolbar={({ clearSelection, selectedRows }) => {
|
|
119
|
+
const selectedSessionIds = selectedRows.map((row) => row.sessionId);
|
|
120
|
+
const hasEmptySelection = selectedRows.some((row) => row.renderablePartCount === 0);
|
|
121
|
+
return (
|
|
122
|
+
<SelectionActionsToolbar
|
|
123
|
+
clearSelection={clearSelection}
|
|
124
|
+
exportDisabled={hasEmptySelection}
|
|
125
|
+
itemLabel="session"
|
|
126
|
+
selectedCount={selectedRows.length}
|
|
127
|
+
onExportSelected={() => onExportSessions(selectedSessionIds)}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}}
|
|
110
131
|
/>
|
|
111
132
|
);
|
|
112
133
|
};
|
|
@@ -41,7 +41,11 @@ const columns = [
|
|
|
41
41
|
header: 'Snapshots',
|
|
42
42
|
}),
|
|
43
43
|
columnHelper.accessor('lastActiveAtMs', {
|
|
44
|
-
cell: (info) =>
|
|
44
|
+
cell: (info) => (
|
|
45
|
+
<span className="whitespace-nowrap text-sm" suppressHydrationWarning>
|
|
46
|
+
{formatDateTime(info.getValue())}
|
|
47
|
+
</span>
|
|
48
|
+
),
|
|
45
49
|
header: 'Last active',
|
|
46
50
|
}),
|
|
47
51
|
] as const;
|
|
@@ -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,80 @@
|
|
|
1
|
+
import { Download, Trash2, X } from 'lucide-react';
|
|
2
|
+
import { Button } from '#/components/ui/button';
|
|
3
|
+
|
|
4
|
+
type SelectionActionsToolbarProps = {
|
|
5
|
+
clearSelection: () => void;
|
|
6
|
+
deleteDisabled?: boolean;
|
|
7
|
+
exportDisabled?: boolean;
|
|
8
|
+
itemLabel: string;
|
|
9
|
+
onDeleteSelected?: () => void;
|
|
10
|
+
onExportSelected?: () => void;
|
|
11
|
+
selectedCount: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const pluralize = (count: number, itemLabel: string) => `${itemLabel}${count === 1 ? '' : 's'}`;
|
|
15
|
+
|
|
16
|
+
export const SelectionActionsToolbar = ({
|
|
17
|
+
clearSelection,
|
|
18
|
+
deleteDisabled = false,
|
|
19
|
+
exportDisabled = false,
|
|
20
|
+
itemLabel,
|
|
21
|
+
onDeleteSelected,
|
|
22
|
+
onExportSelected,
|
|
23
|
+
selectedCount,
|
|
24
|
+
}: SelectionActionsToolbarProps) => {
|
|
25
|
+
if (selectedCount === 0) {
|
|
26
|
+
const actions = [onExportSelected ? 'export' : null, onDeleteSelected ? 'delete' : null].filter(Boolean);
|
|
27
|
+
const actionText =
|
|
28
|
+
actions.length === 2
|
|
29
|
+
? `${actions[0]} or ${actions[1]} them in a batch`
|
|
30
|
+
: actions.length === 1
|
|
31
|
+
? `${actions[0]} them in a batch`
|
|
32
|
+
: 'manage them in a batch';
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<p className="text-[var(--muted-foreground)] text-sm">
|
|
36
|
+
Select {pluralize(2, itemLabel)} to {actionText}.
|
|
37
|
+
</p>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
43
|
+
<p className="text-sm">
|
|
44
|
+
{selectedCount} {pluralize(selectedCount, itemLabel)} selected
|
|
45
|
+
</p>
|
|
46
|
+
<div className="flex flex-wrap gap-2">
|
|
47
|
+
{onExportSelected ? (
|
|
48
|
+
<Button
|
|
49
|
+
className="rounded-full"
|
|
50
|
+
disabled={exportDisabled}
|
|
51
|
+
size="sm"
|
|
52
|
+
type="button"
|
|
53
|
+
variant="outline"
|
|
54
|
+
onClick={onExportSelected}
|
|
55
|
+
>
|
|
56
|
+
<Download className="mr-2 size-4" />
|
|
57
|
+
Export selected {pluralize(selectedCount, itemLabel)}
|
|
58
|
+
</Button>
|
|
59
|
+
) : null}
|
|
60
|
+
{onDeleteSelected ? (
|
|
61
|
+
<Button
|
|
62
|
+
className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
|
|
63
|
+
disabled={deleteDisabled}
|
|
64
|
+
size="sm"
|
|
65
|
+
type="button"
|
|
66
|
+
variant="outline"
|
|
67
|
+
onClick={onDeleteSelected}
|
|
68
|
+
>
|
|
69
|
+
<Trash2 className="mr-2 size-4" />
|
|
70
|
+
Delete selected {pluralize(selectedCount, itemLabel)}
|
|
71
|
+
</Button>
|
|
72
|
+
) : null}
|
|
73
|
+
<Button className="rounded-full" size="sm" type="button" variant="ghost" onClick={clearSelection}>
|
|
74
|
+
<X className="mr-2 size-4" />
|
|
75
|
+
Clear selection
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -2,8 +2,10 @@ import type { ThreadListEntry } from '@spiracha/lib/codex-browser-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
|
+
import { useMemo } from 'react';
|
|
6
7
|
import { DataTable } from '#/components/data-table';
|
|
8
|
+
import { SelectionActionsToolbar } from '#/components/selection-actions-toolbar';
|
|
7
9
|
import { Button } from '#/components/ui/button';
|
|
8
10
|
import {
|
|
9
11
|
DropdownMenu,
|
|
@@ -102,6 +104,7 @@ const columns = (
|
|
|
102
104
|
<DropdownMenu>
|
|
103
105
|
<DropdownMenuTrigger asChild>
|
|
104
106
|
<Button
|
|
107
|
+
aria-label={`Actions for ${info.row.original.thread.title}`}
|
|
105
108
|
className="rounded-full"
|
|
106
109
|
size="icon"
|
|
107
110
|
type="button"
|
|
@@ -139,62 +142,25 @@ export function ThreadsTable({
|
|
|
139
142
|
onExportThread,
|
|
140
143
|
onExportThreads,
|
|
141
144
|
}: ThreadsTableProps) {
|
|
145
|
+
const memoizedColumns = useMemo(() => columns(onDeleteThread, onExportThread), [onDeleteThread, onExportThread]);
|
|
142
146
|
return (
|
|
143
147
|
<DataTable
|
|
144
|
-
columns={
|
|
148
|
+
columns={memoizedColumns}
|
|
145
149
|
data={threads}
|
|
146
150
|
emptyMessage="No threads match the current project filter."
|
|
147
151
|
enableRowSelection
|
|
148
152
|
getRowId={(row) => row.thread.id}
|
|
149
153
|
initialSorting={defaultSorting}
|
|
150
154
|
renderToolbar={({ clearSelection, selectedRows }) => {
|
|
151
|
-
if (selectedRows.length === 0) {
|
|
152
|
-
return (
|
|
153
|
-
<p className="text-[var(--muted-foreground)] text-sm">
|
|
154
|
-
Select threads to export or delete them in a batch.
|
|
155
|
-
</p>
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
155
|
const selectedThreadIds = selectedRows.map((row) => row.thread.id);
|
|
160
156
|
return (
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
size="sm"
|
|
169
|
-
type="button"
|
|
170
|
-
variant="outline"
|
|
171
|
-
onClick={() => onExportThreads(selectedThreadIds)}
|
|
172
|
-
>
|
|
173
|
-
<Download className="mr-2 size-4" />
|
|
174
|
-
Export selected threads
|
|
175
|
-
</Button>
|
|
176
|
-
<Button
|
|
177
|
-
className="rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]"
|
|
178
|
-
size="sm"
|
|
179
|
-
type="button"
|
|
180
|
-
variant="outline"
|
|
181
|
-
onClick={() => onDeleteThreads(selectedThreadIds)}
|
|
182
|
-
>
|
|
183
|
-
<Trash2 className="mr-2 size-4" />
|
|
184
|
-
Delete selected threads
|
|
185
|
-
</Button>
|
|
186
|
-
<Button
|
|
187
|
-
className="rounded-full"
|
|
188
|
-
size="sm"
|
|
189
|
-
type="button"
|
|
190
|
-
variant="ghost"
|
|
191
|
-
onClick={clearSelection}
|
|
192
|
-
>
|
|
193
|
-
<X className="mr-2 size-4" />
|
|
194
|
-
Clear selection
|
|
195
|
-
</Button>
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
157
|
+
<SelectionActionsToolbar
|
|
158
|
+
clearSelection={clearSelection}
|
|
159
|
+
itemLabel="thread"
|
|
160
|
+
selectedCount={selectedRows.length}
|
|
161
|
+
onDeleteSelected={() => onDeleteThreads(selectedThreadIds)}
|
|
162
|
+
onExportSelected={() => onExportThreads(selectedThreadIds)}
|
|
163
|
+
/>
|
|
198
164
|
);
|
|
199
165
|
}}
|
|
200
166
|
/>
|