spiracha 1.0.0 → 1.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/AGENTS.md +28 -1
- package/README.md +47 -7
- package/apps/ui/AGENTS.md +70 -0
- package/apps/ui/README.md +72 -0
- package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +1 -0
- package/apps/ui/dist/client/assets/analytics-BjYaHqXk.js +1 -0
- package/apps/ui/dist/client/assets/checkbox-wPoGG3of.js +1 -0
- package/apps/ui/dist/client/assets/data-table-6yDgAdtf.js +4 -0
- package/apps/ui/dist/client/assets/delete-confirm-dialog-DJUAk7ha.js +11 -0
- package/apps/ui/dist/client/assets/download-BhWd-Pm5.js +1 -0
- package/apps/ui/dist/client/assets/es2015-BlyMI4CF.js +41 -0
- package/apps/ui/dist/client/assets/formatters-BxjZwWSE.js +1 -0
- package/apps/ui/dist/client/assets/index-T01rPkb4.js +22 -0
- package/apps/ui/dist/client/assets/input-B3YN8gzg.js +1 -0
- package/apps/ui/dist/client/assets/metric-card-BWW7TWER.js +1 -0
- package/apps/ui/dist/client/assets/page-header-BZ8Gnxgs.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-B7XcpoLt.js +1 -0
- package/apps/ui/dist/client/assets/projects._project-EfBhCHPY.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-4vfIwLjw.js +1 -0
- package/apps/ui/dist/client/assets/projects.index-DzEZ4pAJ.js +1 -0
- package/apps/ui/dist/client/assets/routes-CWCCZykE.js +1 -0
- package/apps/ui/dist/client/assets/select-DLXGsyZ4.js +1 -0
- package/apps/ui/dist/client/assets/settings-b0Xthfae.js +1 -0
- package/apps/ui/dist/client/assets/styles-8Wtc8YJw.css +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-CgtoCqTb.js +1 -0
- package/apps/ui/dist/client/assets/threads._threadId-DBiDb38K.js +7 -0
- package/apps/ui/dist/client/favicon.ico +0 -0
- package/apps/ui/dist/client/logo192.png +0 -0
- package/apps/ui/dist/client/logo512.png +0 -0
- package/apps/ui/dist/client/manifest.json +25 -0
- package/apps/ui/dist/client/robots.txt +3 -0
- package/apps/ui/dist/server/assets/__23tanstack-start-plugin-adapters-BzCA6dXo.js +5 -0
- package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-BjsXNYgm.js +99 -0
- package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +6 -0
- package/apps/ui/dist/server/assets/analytics-Br_fZB6a.js +139 -0
- package/apps/ui/dist/server/assets/button-CmTDnzOn.js +46 -0
- package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +19 -0
- package/apps/ui/dist/server/assets/codex-queries-CAF6HYiG.js +109 -0
- package/apps/ui/dist/server/assets/codex-server-Cqh0hb93.js +1995 -0
- package/apps/ui/dist/server/assets/data-table-Cdct823O.js +189 -0
- package/apps/ui/dist/server/assets/delete-confirm-dialog-CWqcTXTF.js +139 -0
- package/apps/ui/dist/server/assets/download-CzHmFWGk.js +286 -0
- package/apps/ui/dist/server/assets/formatters-B6o5pTY9.js +72 -0
- package/apps/ui/dist/server/assets/input-B4tEzctc.js +46 -0
- package/apps/ui/dist/server/assets/loading-panel-DbLdvjtR.js +27 -0
- package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +23 -0
- package/apps/ui/dist/server/assets/page-header-CxdZM86z.js +25 -0
- package/apps/ui/dist/server/assets/path-transforms-DD1e7rhY.js +31 -0
- package/apps/ui/dist/server/assets/projects._project-Bwf6iJC-.js +335 -0
- package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +26 -0
- package/apps/ui/dist/server/assets/projects._project-DdVSdfPe.js +18 -0
- package/apps/ui/dist/server/assets/projects.index-CaplpeMy.js +26 -0
- package/apps/ui/dist/server/assets/projects.index-DKeVeqUZ.js +171 -0
- package/apps/ui/dist/server/assets/router-ve2Hrl2Y.js +307 -0
- package/apps/ui/dist/server/assets/routes-BJyx5OmO.js +34 -0
- package/apps/ui/dist/server/assets/routes-pkOwjjYc.js +168 -0
- package/apps/ui/dist/server/assets/select-GW76p-ld.js +76 -0
- package/apps/ui/dist/server/assets/settings-MvWDgc1u.js +100 -0
- package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +52 -0
- package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +13 -0
- package/apps/ui/dist/server/assets/start-BAvbjjfs.js +4 -0
- package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +26 -0
- package/apps/ui/dist/server/assets/threads._threadId-D3PYZIwl.js +18 -0
- package/apps/ui/dist/server/assets/threads._threadId-D3xaWM86.js +1037 -0
- package/apps/ui/dist/server/assets/utils-C_uf36nf.js +8 -0
- package/apps/ui/dist/server/server.js +5678 -0
- package/package.json +47 -7
- package/src/export-chats.ts +1 -14
- package/src/lib/codex-analytics.ts +100 -0
- package/src/lib/codex-browser-db.ts +518 -0
- package/src/lib/codex-browser-export.ts +418 -0
- package/src/lib/codex-browser-types.ts +224 -0
- package/src/lib/codex-exporter-cli.ts +5 -0
- package/src/lib/codex-exporter-transcript.ts +143 -32
- package/src/lib/codex-exporter-types.ts +8 -0
- package/src/lib/codex-thread-cache.ts +58 -0
- package/src/lib/codex-thread-parser.ts +604 -0
- package/src/lib/interactive-cli.ts +5 -13
- package/src/lib/native-open.ts +54 -0
- package/src/lib/path-transforms.ts +45 -0
- package/src/lib/shared.ts +37 -1
- package/src/lib/sqlite-error.ts +14 -0
- package/src/lib/sqlite-retry.ts +39 -0
- package/src/lib/ui-cache.ts +96 -0
- package/src/lib/ui-export-files.ts +77 -0
- package/src/mcp-server.ts +1 -0
- package/src/spiracha.ts +14 -1
- package/src/ui-cli.ts +310 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { t as cn } from "./utils-C_uf36nf.js";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import "lucide-react";
|
|
4
|
+
import { DropdownMenu } from "radix-ui";
|
|
5
|
+
//#region src/components/ui/dropdown-menu.tsx
|
|
6
|
+
function DropdownMenu$1({ ...props }) {
|
|
7
|
+
return /* @__PURE__ */ jsx(DropdownMenu.Root, {
|
|
8
|
+
"data-slot": "dropdown-menu",
|
|
9
|
+
...props
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function DropdownMenuTrigger({ ...props }) {
|
|
13
|
+
return /* @__PURE__ */ jsx(DropdownMenu.Trigger, {
|
|
14
|
+
"data-slot": "dropdown-menu-trigger",
|
|
15
|
+
...props
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function DropdownMenuContent({ className, sideOffset = 4, ...props }) {
|
|
19
|
+
return /* @__PURE__ */ jsx(DropdownMenu.Portal, { children: /* @__PURE__ */ jsx(DropdownMenu.Content, {
|
|
20
|
+
"data-slot": "dropdown-menu-content",
|
|
21
|
+
sideOffset,
|
|
22
|
+
className: cn("data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in", className),
|
|
23
|
+
...props
|
|
24
|
+
}) });
|
|
25
|
+
}
|
|
26
|
+
function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
|
|
27
|
+
return /* @__PURE__ */ jsx(DropdownMenu.Item, {
|
|
28
|
+
"data-slot": "dropdown-menu-item",
|
|
29
|
+
"data-inset": inset,
|
|
30
|
+
"data-variant": variant,
|
|
31
|
+
className: cn("relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[disabled]:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 data-[variant=destructive]:*:[svg]:text-destructive!", className),
|
|
32
|
+
...props
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/components/ui/input.tsx
|
|
37
|
+
function Input({ className, type, ...props }) {
|
|
38
|
+
return /* @__PURE__ */ jsx("input", {
|
|
39
|
+
type,
|
|
40
|
+
"data-slot": "input",
|
|
41
|
+
className: cn("h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", className),
|
|
42
|
+
...props
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { DropdownMenuTrigger as a, DropdownMenuItem as i, DropdownMenu$1 as n, DropdownMenuContent as r, Input as t };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Loader2 } from "lucide-react";
|
|
3
|
+
//#region src/components/loading-panel.tsx
|
|
4
|
+
function LoadingPanel({ description = "Fetching local Codex data. Larger projects can take a moment.", title = "Loading" }) {
|
|
5
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
6
|
+
className: "rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] px-6 py-10 text-center shadow-[var(--panel-shadow)]",
|
|
7
|
+
children: [
|
|
8
|
+
/* @__PURE__ */ jsx("div", {
|
|
9
|
+
className: "flex justify-center",
|
|
10
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
11
|
+
className: "rounded-full border border-[var(--border)] bg-[var(--panel-secondary)] p-3",
|
|
12
|
+
children: /* @__PURE__ */ jsx(Loader2, { className: "size-5 animate-spin text-[var(--accent)]" })
|
|
13
|
+
})
|
|
14
|
+
}),
|
|
15
|
+
/* @__PURE__ */ jsx("p", {
|
|
16
|
+
className: "mt-4 font-medium text-sm",
|
|
17
|
+
children: title
|
|
18
|
+
}),
|
|
19
|
+
/* @__PURE__ */ jsx("p", {
|
|
20
|
+
className: "mt-2 text-[var(--muted-foreground)] text-sm",
|
|
21
|
+
children: description
|
|
22
|
+
})
|
|
23
|
+
]
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { LoadingPanel as t };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/components/metric-card.tsx
|
|
3
|
+
function MetricCard({ helper, label, value }) {
|
|
4
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
5
|
+
className: "rounded-2xl border border-[var(--border)] bg-[var(--panel)] p-4 shadow-[var(--panel-shadow)]",
|
|
6
|
+
children: [
|
|
7
|
+
/* @__PURE__ */ jsx("p", {
|
|
8
|
+
className: "font-semibold text-[10px] text-[var(--muted-foreground)] uppercase tracking-[0.18em]",
|
|
9
|
+
children: label
|
|
10
|
+
}),
|
|
11
|
+
/* @__PURE__ */ jsx("p", {
|
|
12
|
+
className: "mt-2 truncate font-semibold text-lg tracking-[-0.03em]",
|
|
13
|
+
children: value
|
|
14
|
+
}),
|
|
15
|
+
helper ? /* @__PURE__ */ jsx("p", {
|
|
16
|
+
className: "mt-1.5 text-[var(--muted-foreground)] text-xs",
|
|
17
|
+
children: helper
|
|
18
|
+
}) : null
|
|
19
|
+
]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { MetricCard as t };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/components/page-header.tsx
|
|
3
|
+
function PageHeader({ actions, eyebrow, subtitle, title }) {
|
|
4
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
5
|
+
className: "flex flex-col gap-4 border-[var(--border)] border-b pb-5 sm:flex-row sm:items-end sm:justify-between",
|
|
6
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
7
|
+
className: "space-y-2",
|
|
8
|
+
children: [eyebrow ? /* @__PURE__ */ jsx("p", {
|
|
9
|
+
className: "font-semibold text-[11px] text-[var(--muted-foreground)] uppercase tracking-[0.18em]",
|
|
10
|
+
children: eyebrow
|
|
11
|
+
}) : null, /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
|
|
12
|
+
className: "font-semibold text-2xl tracking-[-0.03em] sm:text-[2rem]",
|
|
13
|
+
children: title
|
|
14
|
+
}), subtitle ? /* @__PURE__ */ jsx("p", {
|
|
15
|
+
className: "mt-2 max-w-[60rem] whitespace-pre-wrap break-words text-[var(--muted-foreground)] text-sm",
|
|
16
|
+
children: subtitle
|
|
17
|
+
}) : null] })]
|
|
18
|
+
}), actions ? /* @__PURE__ */ jsx("div", {
|
|
19
|
+
className: "flex flex-wrap items-center gap-2",
|
|
20
|
+
children: actions
|
|
21
|
+
}) : null]
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { PageHeader as t };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region ../../src/lib/path-transforms.ts
|
|
2
|
+
var escapeForRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
3
|
+
var toUniquePathVariants = (projectPath) => {
|
|
4
|
+
const normalized = projectPath.trim();
|
|
5
|
+
const variants = [
|
|
6
|
+
normalized,
|
|
7
|
+
normalized.replaceAll("\\", "/"),
|
|
8
|
+
normalized.replaceAll("/", "\\")
|
|
9
|
+
].filter(Boolean);
|
|
10
|
+
return [...new Set(variants)].sort((left, right) => right.length - left.length);
|
|
11
|
+
};
|
|
12
|
+
var replaceExactProjectPath = (text, projectPath) => {
|
|
13
|
+
let result = text;
|
|
14
|
+
for (const variant of toUniquePathVariants(projectPath)) {
|
|
15
|
+
const escapedVariant = escapeForRegex(variant);
|
|
16
|
+
result = result.replace(new RegExp(`${escapedVariant}(?<separator>[\\\\/])`, "gu"), "");
|
|
17
|
+
result = result.replace(new RegExp(`${escapedVariant}(?=$|[^A-Za-z0-9._-])`, "gu"), ".");
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
var redactRemainingUsernames = (text) => {
|
|
22
|
+
return text.replace(/\/Users\/[^/\\]+(?=\/|$)/gu, "~").replace(/[A-Za-z]:[\\/]+Users[\\/]+[^\\/]+(?=[\\/]|$)/gu, "~");
|
|
23
|
+
};
|
|
24
|
+
var applyPathTransforms = (text, settings) => {
|
|
25
|
+
let result = text;
|
|
26
|
+
if (settings.convertToProjectRoot && settings.projectPath) result = replaceExactProjectPath(result, settings.projectPath);
|
|
27
|
+
if (settings.redactUsername) result = redactRemainingUsernames(result);
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { applyPathTransforms as t };
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { t as Button } from "./button-CmTDnzOn.js";
|
|
2
|
+
import { n as useSettings } from "./settings-store-DpEJEQ7M.js";
|
|
3
|
+
import { c as deleteThreadFn, d as exportThreadsFn, l as deleteThreadsFn, r as projectThreadsQueryOptions, u as exportThreadFn } from "./codex-queries-CAF6HYiG.js";
|
|
4
|
+
import { t as Route } from "./projects._project-DdVSdfPe.js";
|
|
5
|
+
import { t as DataTable } from "./data-table-Cdct823O.js";
|
|
6
|
+
import { t as PageHeader } from "./page-header-CxdZM86z.js";
|
|
7
|
+
import { n as formatBytes, o as formatNumber, r as formatDateTime, s as formatTokens } from "./formatters-B6o5pTY9.js";
|
|
8
|
+
import { t as DeleteConfirmDialog } from "./delete-confirm-dialog-CWqcTXTF.js";
|
|
9
|
+
import { n as downloadUrlFile, r as ExportDialog, t as downloadTextFile } from "./download-CzHmFWGk.js";
|
|
10
|
+
import { a as DropdownMenuTrigger, i as DropdownMenuItem, n as DropdownMenu, r as DropdownMenuContent, t as Input } from "./input-B4tEzctc.js";
|
|
11
|
+
import { startTransition, useDeferredValue, useState } from "react";
|
|
12
|
+
import { Link } from "@tanstack/react-router";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
|
15
|
+
import { Download, MoreHorizontal, Trash2, X } from "lucide-react";
|
|
16
|
+
import { createColumnHelper } from "@tanstack/react-table";
|
|
17
|
+
//#region src/components/threads-table.tsx
|
|
18
|
+
var columnHelper = createColumnHelper();
|
|
19
|
+
var defaultSorting = [{
|
|
20
|
+
desc: true,
|
|
21
|
+
id: "updatedAt"
|
|
22
|
+
}];
|
|
23
|
+
var columns = (onDeleteThread, onExportThread) => [
|
|
24
|
+
columnHelper.accessor((row) => row.thread.title, {
|
|
25
|
+
cell: (info) => /* @__PURE__ */ jsxs(Link, {
|
|
26
|
+
className: "block w-[16rem] max-w-[20rem] space-y-1 rounded-md outline-none transition hover:opacity-80 focus-visible:ring-2 focus-visible:ring-[var(--accent)] lg:w-auto",
|
|
27
|
+
params: { threadId: info.row.original.thread.id },
|
|
28
|
+
to: "/threads/$threadId",
|
|
29
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
30
|
+
className: "truncate font-medium underline-offset-2 hover:underline",
|
|
31
|
+
children: info.getValue()
|
|
32
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
33
|
+
className: "line-clamp-2 text-[var(--muted-foreground)] text-xs",
|
|
34
|
+
children: info.row.original.thread.preview
|
|
35
|
+
})]
|
|
36
|
+
}),
|
|
37
|
+
header: "Thread",
|
|
38
|
+
id: "title"
|
|
39
|
+
}),
|
|
40
|
+
columnHelper.accessor((row) => row.thread.updated_at_ms ?? row.thread.updated_at * 1e3, {
|
|
41
|
+
cell: (info) => /* @__PURE__ */ jsx("span", {
|
|
42
|
+
className: "whitespace-nowrap text-sm",
|
|
43
|
+
children: formatDateTime(info.getValue())
|
|
44
|
+
}),
|
|
45
|
+
header: "Updated",
|
|
46
|
+
id: "updatedAt"
|
|
47
|
+
}),
|
|
48
|
+
columnHelper.accessor((row) => row.thread.created_at_ms ?? row.thread.created_at * 1e3, {
|
|
49
|
+
cell: (info) => /* @__PURE__ */ jsx("span", {
|
|
50
|
+
className: "whitespace-nowrap text-sm",
|
|
51
|
+
children: formatDateTime(info.getValue())
|
|
52
|
+
}),
|
|
53
|
+
header: "Created",
|
|
54
|
+
id: "createdAt"
|
|
55
|
+
}),
|
|
56
|
+
columnHelper.accessor((row) => row.thread.model ?? "unknown", {
|
|
57
|
+
cell: (info) => /* @__PURE__ */ jsx("span", {
|
|
58
|
+
className: "truncate font-mono text-sm",
|
|
59
|
+
children: info.getValue()
|
|
60
|
+
}),
|
|
61
|
+
header: "Model",
|
|
62
|
+
id: "model"
|
|
63
|
+
}),
|
|
64
|
+
columnHelper.accessor((row) => row.thread.tokens_used, {
|
|
65
|
+
cell: (info) => /* @__PURE__ */ jsx("span", {
|
|
66
|
+
className: "whitespace-nowrap font-mono text-sm",
|
|
67
|
+
children: formatTokens(info.getValue())
|
|
68
|
+
}),
|
|
69
|
+
header: "Tokens",
|
|
70
|
+
id: "tokens"
|
|
71
|
+
}),
|
|
72
|
+
columnHelper.accessor((row) => row.rolloutSizeBytes, {
|
|
73
|
+
cell: (info) => /* @__PURE__ */ jsx("span", {
|
|
74
|
+
className: "whitespace-nowrap font-mono text-sm",
|
|
75
|
+
children: formatBytes(info.getValue() ?? 0)
|
|
76
|
+
}),
|
|
77
|
+
header: "Size",
|
|
78
|
+
id: "size"
|
|
79
|
+
}),
|
|
80
|
+
columnHelper.accessor((row) => row.stats.toolCallCount, {
|
|
81
|
+
cell: (info) => info.row.original.stats.deferred ? /* @__PURE__ */ jsx("span", {
|
|
82
|
+
className: "text-[var(--muted-foreground)] text-sm",
|
|
83
|
+
children: "Deferred"
|
|
84
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
85
|
+
className: "font-mono text-sm",
|
|
86
|
+
children: formatNumber(info.getValue())
|
|
87
|
+
}),
|
|
88
|
+
header: "Tools",
|
|
89
|
+
id: "tools"
|
|
90
|
+
}),
|
|
91
|
+
columnHelper.accessor((row) => row.thread.archived, {
|
|
92
|
+
cell: (info) => /* @__PURE__ */ jsx("span", {
|
|
93
|
+
className: "text-sm",
|
|
94
|
+
children: info.getValue() ? "Archived" : "Active"
|
|
95
|
+
}),
|
|
96
|
+
header: "State",
|
|
97
|
+
id: "state"
|
|
98
|
+
}),
|
|
99
|
+
columnHelper.display({
|
|
100
|
+
cell: (info) => /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
101
|
+
asChild: true,
|
|
102
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
103
|
+
className: "rounded-full",
|
|
104
|
+
size: "icon",
|
|
105
|
+
type: "button",
|
|
106
|
+
variant: "ghost",
|
|
107
|
+
onClick: (event) => event.stopPropagation(),
|
|
108
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" })
|
|
109
|
+
})
|
|
110
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
111
|
+
align: "end",
|
|
112
|
+
children: [/* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
113
|
+
onClick: () => onExportThread(info.row.original),
|
|
114
|
+
children: [/* @__PURE__ */ jsx(Download, { className: "mr-2 size-4" }), "Export thread"]
|
|
115
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
116
|
+
className: "text-[var(--destructive)]",
|
|
117
|
+
onClick: () => onDeleteThread(info.row.original),
|
|
118
|
+
children: [/* @__PURE__ */ jsx(Trash2, { className: "mr-2 size-4" }), "Delete thread"]
|
|
119
|
+
})]
|
|
120
|
+
})] }),
|
|
121
|
+
enableSorting: false,
|
|
122
|
+
header: "",
|
|
123
|
+
id: "actions"
|
|
124
|
+
})
|
|
125
|
+
];
|
|
126
|
+
function ThreadsTable({ threads, onDeleteThread, onDeleteThreads, onExportThread, onExportThreads }) {
|
|
127
|
+
return /* @__PURE__ */ jsx(DataTable, {
|
|
128
|
+
columns: columns(onDeleteThread, onExportThread),
|
|
129
|
+
data: threads,
|
|
130
|
+
emptyMessage: "No threads match the current project filter.",
|
|
131
|
+
enableRowSelection: true,
|
|
132
|
+
getRowId: (row) => row.thread.id,
|
|
133
|
+
initialSorting: defaultSorting,
|
|
134
|
+
renderToolbar: ({ clearSelection, selectedRows }) => {
|
|
135
|
+
if (selectedRows.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
136
|
+
className: "text-[var(--muted-foreground)] text-sm",
|
|
137
|
+
children: "Select threads to export or delete them in a batch."
|
|
138
|
+
});
|
|
139
|
+
const selectedThreadIds = selectedRows.map((row) => row.thread.id);
|
|
140
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
141
|
+
className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
|
|
142
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
143
|
+
className: "text-sm",
|
|
144
|
+
children: [
|
|
145
|
+
selectedRows.length,
|
|
146
|
+
" thread",
|
|
147
|
+
selectedRows.length === 1 ? "" : "s",
|
|
148
|
+
" selected"
|
|
149
|
+
]
|
|
150
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
151
|
+
className: "flex flex-wrap gap-2",
|
|
152
|
+
children: [
|
|
153
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
154
|
+
className: "rounded-full",
|
|
155
|
+
size: "sm",
|
|
156
|
+
type: "button",
|
|
157
|
+
variant: "outline",
|
|
158
|
+
onClick: () => onExportThreads(selectedThreadIds),
|
|
159
|
+
children: [/* @__PURE__ */ jsx(Download, { className: "mr-2 size-4" }), "Export selected threads"]
|
|
160
|
+
}),
|
|
161
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
162
|
+
className: "rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]",
|
|
163
|
+
size: "sm",
|
|
164
|
+
type: "button",
|
|
165
|
+
variant: "outline",
|
|
166
|
+
onClick: () => onDeleteThreads(selectedThreadIds),
|
|
167
|
+
children: [/* @__PURE__ */ jsx(Trash2, { className: "mr-2 size-4" }), "Delete selected threads"]
|
|
168
|
+
}),
|
|
169
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
170
|
+
className: "rounded-full",
|
|
171
|
+
size: "sm",
|
|
172
|
+
type: "button",
|
|
173
|
+
variant: "ghost",
|
|
174
|
+
onClick: clearSelection,
|
|
175
|
+
children: [/* @__PURE__ */ jsx(X, { className: "mr-2 size-4" }), "Clear selection"]
|
|
176
|
+
})
|
|
177
|
+
]
|
|
178
|
+
})]
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/routes/projects.$project.tsx?tsr-split=component
|
|
185
|
+
function ProjectDetailPage() {
|
|
186
|
+
const params = Route.useParams();
|
|
187
|
+
const queryClient = useQueryClient();
|
|
188
|
+
const threads = useSuspenseQuery(projectThreadsQueryOptions(params.project)).data;
|
|
189
|
+
const { settings } = useSettings();
|
|
190
|
+
const [searchInput, setSearchInput] = useState("");
|
|
191
|
+
const [pendingDelete, setPendingDelete] = useState(null);
|
|
192
|
+
const [pendingExport, setPendingExport] = useState(null);
|
|
193
|
+
const deferredSearch = useDeferredValue(searchInput.trim().toLowerCase());
|
|
194
|
+
const deleteThreadMutation = useMutation({
|
|
195
|
+
mutationFn: (input) => {
|
|
196
|
+
if (input.threadIds.length === 1) return deleteThreadFn({ data: {
|
|
197
|
+
deleteSessionFiles: input.deleteSessionFiles,
|
|
198
|
+
threadId: input.threadIds[0]
|
|
199
|
+
} });
|
|
200
|
+
return deleteThreadsFn({ data: input });
|
|
201
|
+
},
|
|
202
|
+
onSuccess: async () => {
|
|
203
|
+
await Promise.all([
|
|
204
|
+
queryClient.invalidateQueries({ queryKey: ["analytics"] }),
|
|
205
|
+
queryClient.invalidateQueries({ queryKey: ["dashboard"] }),
|
|
206
|
+
queryClient.invalidateQueries({ queryKey: ["project-threads", params.project] }),
|
|
207
|
+
queryClient.invalidateQueries({ queryKey: ["projects"] })
|
|
208
|
+
]);
|
|
209
|
+
setPendingDelete(null);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
const exportThreadMutation = useMutation({
|
|
213
|
+
mutationFn: async (options) => {
|
|
214
|
+
if (!pendingExport) throw new Error("No thread selected for export");
|
|
215
|
+
console.info("[spiracha:export-ui] request", {
|
|
216
|
+
outputFormat: options.outputFormat,
|
|
217
|
+
project: params.project,
|
|
218
|
+
selectedThreadCount: pendingExport.threadIds.length,
|
|
219
|
+
selectedThreadIds: pendingExport.threadIds
|
|
220
|
+
});
|
|
221
|
+
const download = pendingExport.threadIds.length === 1 ? await exportThreadFn({ data: {
|
|
222
|
+
...options,
|
|
223
|
+
...settings,
|
|
224
|
+
threadId: pendingExport.threadIds[0]
|
|
225
|
+
} }) : await exportThreadsFn({ data: {
|
|
226
|
+
...options,
|
|
227
|
+
...settings,
|
|
228
|
+
threadIds: pendingExport.threadIds
|
|
229
|
+
} });
|
|
230
|
+
console.info("[spiracha:export-ui] response", {
|
|
231
|
+
downloadUrl: download.mode === "download_url" ? download.downloadUrl : null,
|
|
232
|
+
fileName: download.fileName,
|
|
233
|
+
mode: download.mode,
|
|
234
|
+
project: params.project,
|
|
235
|
+
selectedThreadCount: pendingExport.threadIds.length
|
|
236
|
+
});
|
|
237
|
+
if (download.mode === "download") {
|
|
238
|
+
downloadTextFile(download.fileName, download.content, download.mimeType);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
await downloadUrlFile(download.fileName, download.downloadUrl);
|
|
242
|
+
},
|
|
243
|
+
onError: (error) => {
|
|
244
|
+
console.error("[spiracha:export-ui] failed", {
|
|
245
|
+
error: error instanceof Error ? error.message : String(error),
|
|
246
|
+
project: params.project,
|
|
247
|
+
selectedThreadCount: pendingExport?.threadIds.length ?? 0,
|
|
248
|
+
selectedThreadIds: pendingExport?.threadIds ?? []
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
onSuccess: () => {
|
|
252
|
+
setPendingExport(null);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
const visibleThreads = [...threads].filter((thread) => {
|
|
256
|
+
if (!deferredSearch) return true;
|
|
257
|
+
return `${thread.thread.title}\n${thread.thread.preview}`.toLowerCase().includes(deferredSearch);
|
|
258
|
+
});
|
|
259
|
+
const lookupSelectedThreads = (threadIds) => {
|
|
260
|
+
const threadIdSet = new Set(threadIds);
|
|
261
|
+
return visibleThreads.filter((thread) => threadIdSet.has(thread.thread.id));
|
|
262
|
+
};
|
|
263
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
264
|
+
className: "space-y-6",
|
|
265
|
+
children: [
|
|
266
|
+
/* @__PURE__ */ jsx(PageHeader, {
|
|
267
|
+
actions: /* @__PURE__ */ jsx("div", {
|
|
268
|
+
className: "flex flex-col gap-2 sm:flex-row",
|
|
269
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
270
|
+
className: "h-10 w-full rounded-full border-[var(--border)] bg-[var(--panel)] px-4 sm:w-[20rem]",
|
|
271
|
+
placeholder: "Search thread title or preview",
|
|
272
|
+
value: searchInput,
|
|
273
|
+
onChange: (event) => {
|
|
274
|
+
startTransition(() => {
|
|
275
|
+
setSearchInput(event.target.value);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
}),
|
|
280
|
+
eyebrow: "Project",
|
|
281
|
+
subtitle: "Sort by any column, inspect tool call volume, and manage thread records for this derived project.",
|
|
282
|
+
title: params.project
|
|
283
|
+
}),
|
|
284
|
+
/* @__PURE__ */ jsx(ThreadsTable, {
|
|
285
|
+
threads: visibleThreads,
|
|
286
|
+
onDeleteThread: (thread) => setPendingDelete({ threads: [thread] }),
|
|
287
|
+
onDeleteThreads: (threadIds) => {
|
|
288
|
+
const selectedThreads = lookupSelectedThreads(threadIds);
|
|
289
|
+
if (selectedThreads.length === 0) return;
|
|
290
|
+
setPendingDelete({ threads: selectedThreads });
|
|
291
|
+
},
|
|
292
|
+
onExportThread: (thread) => setPendingExport({
|
|
293
|
+
threadIds: [thread.thread.id],
|
|
294
|
+
threadLabel: thread.thread.title
|
|
295
|
+
}),
|
|
296
|
+
onExportThreads: (threadIds) => {
|
|
297
|
+
const selectedThreads = lookupSelectedThreads(threadIds);
|
|
298
|
+
if (selectedThreads.length === 0) return;
|
|
299
|
+
setPendingExport({
|
|
300
|
+
threadIds: selectedThreads.map((thread) => thread.thread.id),
|
|
301
|
+
threadLabel: selectedThreads.length === 1 ? selectedThreads[0].thread.title : `${selectedThreads.length} selected threads`
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}),
|
|
305
|
+
/* @__PURE__ */ jsx(DeleteConfirmDialog, {
|
|
306
|
+
confirmLabel: deleteThreadMutation.isPending ? "Deleting..." : "Delete thread",
|
|
307
|
+
description: pendingDelete ? pendingDelete.threads.length === 1 ? `Delete the thread "${pendingDelete.threads[0].thread.title}" from the Codex database. Leave Session files unchecked if you only want to remove the current DB row.` : `Delete ${pendingDelete.threads.length} selected threads from the Codex database. Enable Delete Session files if you also want to remove their rollout JSONL files.` : "",
|
|
308
|
+
open: pendingDelete !== null,
|
|
309
|
+
showDeleteSessionFilesOption: true,
|
|
310
|
+
title: "Delete thread from Codex DB?",
|
|
311
|
+
onConfirm: ({ deleteSessionFiles }) => {
|
|
312
|
+
if (!pendingDelete) return;
|
|
313
|
+
deleteThreadMutation.mutate({
|
|
314
|
+
deleteSessionFiles,
|
|
315
|
+
threadIds: pendingDelete.threads.map((thread) => thread.thread.id)
|
|
316
|
+
});
|
|
317
|
+
},
|
|
318
|
+
onOpenChange: (open) => {
|
|
319
|
+
if (!open) setPendingDelete(null);
|
|
320
|
+
}
|
|
321
|
+
}),
|
|
322
|
+
/* @__PURE__ */ jsx(ExportDialog, {
|
|
323
|
+
open: pendingExport !== null,
|
|
324
|
+
pending: exportThreadMutation.isPending,
|
|
325
|
+
title: pendingExport ? `Export ${pendingExport.threadLabel}` : "Export thread",
|
|
326
|
+
onExport: (options) => exportThreadMutation.mutate(options),
|
|
327
|
+
onOpenChange: (open) => {
|
|
328
|
+
if (!open) setPendingExport(null);
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
]
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
335
|
+
export { ProjectDetailPage as component };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/routes/projects.$project.tsx?tsr-split=errorComponent
|
|
3
|
+
function ProjectDetailErrorComponent({ error }) {
|
|
4
|
+
const isSqlite = error.message.includes("unable to open database") || error.message.includes("database is locked");
|
|
5
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
6
|
+
className: "rounded-xl border border-[var(--border)] bg-[var(--panel)] px-6 py-10 text-center",
|
|
7
|
+
children: [
|
|
8
|
+
/* @__PURE__ */ jsx("p", {
|
|
9
|
+
className: "font-medium text-[var(--destructive)] text-sm",
|
|
10
|
+
children: isSqlite ? "Database unavailable" : "Failed to load project"
|
|
11
|
+
}),
|
|
12
|
+
/* @__PURE__ */ jsx("p", {
|
|
13
|
+
className: "mt-2 text-[var(--muted-foreground)] text-sm",
|
|
14
|
+
children: isSqlite ? "Codex may have an exclusive lock on the database. Reload to retry." : error.message
|
|
15
|
+
}),
|
|
16
|
+
/* @__PURE__ */ jsx("button", {
|
|
17
|
+
className: "mt-4 text-[var(--accent)] text-sm underline-offset-2 hover:underline",
|
|
18
|
+
type: "button",
|
|
19
|
+
onClick: () => window.location.reload(),
|
|
20
|
+
children: "Reload"
|
|
21
|
+
})
|
|
22
|
+
]
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { ProjectDetailErrorComponent as errorComponent };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { r as projectThreadsQueryOptions } from "./codex-queries-CAF6HYiG.js";
|
|
2
|
+
import { t as LoadingPanel } from "./loading-panel-DbLdvjtR.js";
|
|
3
|
+
import { createFileRoute, lazyRouteComponent } from "@tanstack/react-router";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/routes/projects.$project.tsx
|
|
6
|
+
var $$splitErrorComponentImporter = () => import("./projects._project-CLSohrBp.js");
|
|
7
|
+
var $$splitComponentImporter = () => import("./projects._project-Bwf6iJC-.js");
|
|
8
|
+
var Route = createFileRoute("/projects/$project")({
|
|
9
|
+
component: lazyRouteComponent($$splitComponentImporter, "component"),
|
|
10
|
+
errorComponent: lazyRouteComponent($$splitErrorComponentImporter, "errorComponent"),
|
|
11
|
+
loader: ({ context, params }) => context.queryClient.ensureQueryData(projectThreadsQueryOptions(params.project)),
|
|
12
|
+
pendingComponent: () => /* @__PURE__ */ jsx(LoadingPanel, {
|
|
13
|
+
description: "Loading project threads and transcript summaries. Large projects can take a moment.",
|
|
14
|
+
title: "Loading project"
|
|
15
|
+
})
|
|
16
|
+
});
|
|
17
|
+
//#endregion
|
|
18
|
+
export { Route as t };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/routes/projects.index.tsx?tsr-split=errorComponent
|
|
3
|
+
function ProjectsErrorComponent({ error }) {
|
|
4
|
+
const isSqlite = error.message.includes("unable to open database") || error.message.includes("database is locked");
|
|
5
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
6
|
+
className: "rounded-xl border border-[var(--border)] bg-[var(--panel)] px-6 py-10 text-center",
|
|
7
|
+
children: [
|
|
8
|
+
/* @__PURE__ */ jsx("p", {
|
|
9
|
+
className: "font-medium text-[var(--destructive)] text-sm",
|
|
10
|
+
children: isSqlite ? "Database unavailable" : "Failed to load projects"
|
|
11
|
+
}),
|
|
12
|
+
/* @__PURE__ */ jsx("p", {
|
|
13
|
+
className: "mt-2 text-[var(--muted-foreground)] text-sm",
|
|
14
|
+
children: isSqlite ? "Codex may have an exclusive lock on the database. Reload to retry." : error.message
|
|
15
|
+
}),
|
|
16
|
+
/* @__PURE__ */ jsx("button", {
|
|
17
|
+
className: "mt-4 text-[var(--accent)] text-sm underline-offset-2 hover:underline",
|
|
18
|
+
type: "button",
|
|
19
|
+
onClick: () => window.location.reload(),
|
|
20
|
+
children: "Reload"
|
|
21
|
+
})
|
|
22
|
+
]
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { ProjectsErrorComponent as errorComponent };
|