spiracha 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/apps/ui/README.md +20 -2
- package/apps/ui/package.json +2 -2
- package/apps/ui/src/components/antigravity-conversations-table.tsx +50 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +1 -0
- package/apps/ui/src/components/breadcrumbs.tsx +21 -3
- package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
- package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
- package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
- package/apps/ui/src/components/data-table.tsx +7 -5
- package/apps/ui/src/components/export-dialog.tsx +2 -12
- package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
- package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
- package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
- package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
- package/apps/ui/src/components/page-header.tsx +15 -9
- package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -0
- package/apps/ui/src/components/threads-table.tsx +9 -46
- package/apps/ui/src/components/transcript-view.tsx +148 -52
- package/apps/ui/src/components/ui/select.tsx +1 -1
- package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
- package/apps/ui/src/lib/antigravity-server.ts +152 -7
- package/apps/ui/src/lib/antigravity-transcript-events.ts +11 -3
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +175 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +19 -3
- package/apps/ui/src/lib/codex-server.ts +135 -53
- package/apps/ui/src/lib/cursor-server.ts +48 -5
- package/apps/ui/src/lib/formatters.ts +3 -5
- package/apps/ui/src/lib/grok-queries.ts +22 -0
- package/apps/ui/src/lib/grok-server.ts +169 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +85 -7
- package/apps/ui/src/lib/opencode-server.ts +85 -7
- package/apps/ui/src/lib/qoder-server.ts +67 -11
- package/apps/ui/src/lib/route-search.ts +114 -0
- package/apps/ui/src/lib/source-session-export-server.ts +86 -3
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/workspace-delete-navigation.ts +12 -0
- package/apps/ui/src/routeTree.gen.ts +107 -0
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +76 -10
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +269 -31
- package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
- package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
- package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +143 -17
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +165 -26
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +2 -2
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +11 -1
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +388 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +301 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +66 -15
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +199 -32
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +105 -19
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +236 -44
- package/apps/ui/src/routes/opencode.index.tsx +16 -3
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +7 -4
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +75 -28
- package/apps/ui/src/routes/threads.$threadId.tsx +548 -64
- package/package.json +19 -18
- package/src/client.ts +134 -1
- package/src/lib/antigravity-db.ts +208 -32
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/claude-code-db.ts +498 -32
- package/src/lib/claude-code-exporter-types.ts +5 -0
- package/src/lib/claude-code-transcript-phase.ts +60 -1
- package/src/lib/claude-code-transcript.ts +8 -5
- package/src/lib/codex-browser-export.ts +23 -27
- package/src/lib/codex-thread-cache.ts +41 -13
- package/src/lib/codex-thread-parser.ts +86 -35
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +39 -19
- package/src/lib/concurrency.ts +41 -0
- package/src/lib/conversation-api.ts +399 -19
- package/src/lib/conversation-data/antigravity-adapter.ts +21 -2
- package/src/lib/conversation-data/claude-code-adapter.ts +37 -6
- package/src/lib/conversation-data/codex-adapter.ts +28 -4
- package/src/lib/conversation-data/cursor-adapter.ts +35 -1
- package/src/lib/conversation-data/grok-adapter.ts +210 -0
- package/src/lib/conversation-data/index.ts +76 -1
- package/src/lib/conversation-data/kiro-adapter.ts +41 -7
- package/src/lib/conversation-data/opencode-adapter.ts +24 -2
- package/src/lib/conversation-data/qoder-adapter.ts +44 -19
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +57 -0
- package/src/lib/cursor-db.ts +34 -5
- package/src/lib/grok-db.ts +1026 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +52 -0
- package/src/lib/grok-transcript.ts +154 -0
- package/src/lib/kiro-db.ts +52 -1
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +598 -55
- package/src/lib/opencode-transcript.ts +8 -5
- package/src/lib/shared.ts +12 -0
- package/src/lib/transcript-load-limiter.ts +82 -0
- package/src/lib/ui-cache.ts +43 -17
- package/src/lib/ui-export-archive.ts +60 -0
|
@@ -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>
|
|
@@ -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,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
|
+
};
|
|
@@ -2,8 +2,9 @@ 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
6
|
import { DataTable } from '#/components/data-table';
|
|
7
|
+
import { SelectionActionsToolbar } from '#/components/selection-actions-toolbar';
|
|
7
8
|
import { Button } from '#/components/ui/button';
|
|
8
9
|
import {
|
|
9
10
|
DropdownMenu,
|
|
@@ -148,53 +149,15 @@ export function ThreadsTable({
|
|
|
148
149
|
getRowId={(row) => row.thread.id}
|
|
149
150
|
initialSorting={defaultSorting}
|
|
150
151
|
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
152
|
const selectedThreadIds = selectedRows.map((row) => row.thread.id);
|
|
160
153
|
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>
|
|
154
|
+
<SelectionActionsToolbar
|
|
155
|
+
clearSelection={clearSelection}
|
|
156
|
+
itemLabel="thread"
|
|
157
|
+
selectedCount={selectedRows.length}
|
|
158
|
+
onDeleteSelected={() => onDeleteThreads(selectedThreadIds)}
|
|
159
|
+
onExportSelected={() => onExportThreads(selectedThreadIds)}
|
|
160
|
+
/>
|
|
198
161
|
);
|
|
199
162
|
}}
|
|
200
163
|
/>
|