spiracha 1.0.0 → 1.1.1

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.
Files changed (92) hide show
  1. package/AGENTS.md +31 -1
  2. package/README.md +61 -7
  3. package/apps/ui/AGENTS.md +70 -0
  4. package/apps/ui/README.md +72 -0
  5. package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +1 -0
  6. package/apps/ui/dist/client/assets/analytics-CqWZmyV6.js +1 -0
  7. package/apps/ui/dist/client/assets/checkbox-DXM4lkJq.js +1 -0
  8. package/apps/ui/dist/client/assets/data-table-DnPYMPCD.js +4 -0
  9. package/apps/ui/dist/client/assets/delete-confirm-dialog-CcZaRX33.js +11 -0
  10. package/apps/ui/dist/client/assets/download-DOwxk-cG.js +1 -0
  11. package/apps/ui/dist/client/assets/es2015-Bm0kEzx2.js +41 -0
  12. package/apps/ui/dist/client/assets/formatters-C12LmYaa.js +1 -0
  13. package/apps/ui/dist/client/assets/index-DdJ7ahIt.js +22 -0
  14. package/apps/ui/dist/client/assets/input-CEsI7EpI.js +1 -0
  15. package/apps/ui/dist/client/assets/metric-card-9jwBF7rG.js +1 -0
  16. package/apps/ui/dist/client/assets/page-header-Dr_h1CVv.js +1 -0
  17. package/apps/ui/dist/client/assets/projects._project-uyNGnpjH.js +1 -0
  18. package/apps/ui/dist/client/assets/projects._project-zoM8d2nH.js +1 -0
  19. package/apps/ui/dist/client/assets/projects.index-D1CWVN-O.js +1 -0
  20. package/apps/ui/dist/client/assets/projects.index-DukMuny6.js +1 -0
  21. package/apps/ui/dist/client/assets/routes-Gr2Wwh83.js +1 -0
  22. package/apps/ui/dist/client/assets/select-CFim44gT.js +1 -0
  23. package/apps/ui/dist/client/assets/settings-DqhyDxo2.js +1 -0
  24. package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +1 -0
  25. package/apps/ui/dist/client/assets/threads._threadId-DT75NiBa.js +1 -0
  26. package/apps/ui/dist/client/assets/threads._threadId-Df5VXIuZ.js +7 -0
  27. package/apps/ui/dist/client/favicon.ico +0 -0
  28. package/apps/ui/dist/client/logo192.png +0 -0
  29. package/apps/ui/dist/client/logo512.png +0 -0
  30. package/apps/ui/dist/client/manifest.json +25 -0
  31. package/apps/ui/dist/client/robots.txt +3 -0
  32. package/apps/ui/dist/server/assets/__23tanstack-start-plugin-adapters-BzCA6dXo.js +5 -0
  33. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-C0V305Nt.js +99 -0
  34. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +6 -0
  35. package/apps/ui/dist/server/assets/analytics-BMxW_bZL.js +139 -0
  36. package/apps/ui/dist/server/assets/button-CmTDnzOn.js +46 -0
  37. package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +19 -0
  38. package/apps/ui/dist/server/assets/codex-queries-CAF6HYiG.js +109 -0
  39. package/apps/ui/dist/server/assets/codex-server-BFZq2Y2O.js +2062 -0
  40. package/apps/ui/dist/server/assets/data-table-Cdct823O.js +189 -0
  41. package/apps/ui/dist/server/assets/delete-confirm-dialog-CWqcTXTF.js +139 -0
  42. package/apps/ui/dist/server/assets/download-C5rkk_Bo.js +289 -0
  43. package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +91 -0
  44. package/apps/ui/dist/server/assets/input-B4tEzctc.js +46 -0
  45. package/apps/ui/dist/server/assets/loading-panel-DbLdvjtR.js +27 -0
  46. package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +23 -0
  47. package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +13 -0
  48. package/apps/ui/dist/server/assets/page-header-CxdZM86z.js +25 -0
  49. package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +31 -0
  50. package/apps/ui/dist/server/assets/projects._project-CJ7l0ynC.js +18 -0
  51. package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +26 -0
  52. package/apps/ui/dist/server/assets/projects._project-CcJLp_A8.js +337 -0
  53. package/apps/ui/dist/server/assets/projects.index-CaplpeMy.js +26 -0
  54. package/apps/ui/dist/server/assets/projects.index-srtogpuF.js +172 -0
  55. package/apps/ui/dist/server/assets/router-C_w-haH6.js +307 -0
  56. package/apps/ui/dist/server/assets/routes-BhbxvJE7.js +34 -0
  57. package/apps/ui/dist/server/assets/routes-CPe-ppmC.js +169 -0
  58. package/apps/ui/dist/server/assets/select-GW76p-ld.js +76 -0
  59. package/apps/ui/dist/server/assets/settings-MvWDgc1u.js +100 -0
  60. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +52 -0
  61. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +13 -0
  62. package/apps/ui/dist/server/assets/start-HeKLHD9b.js +4 -0
  63. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +26 -0
  64. package/apps/ui/dist/server/assets/threads._threadId-Ba7vv6-K.js +18 -0
  65. package/apps/ui/dist/server/assets/threads._threadId-euyNckhj.js +1059 -0
  66. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +8 -0
  67. package/apps/ui/dist/server/server.js +5678 -0
  68. package/package.json +53 -7
  69. package/src/export-chats.ts +4 -18
  70. package/src/lib/claude-exporter.ts +1 -1
  71. package/src/lib/codex-analytics.ts +100 -0
  72. package/src/lib/codex-browser-db.ts +605 -0
  73. package/src/lib/codex-browser-export.ts +429 -0
  74. package/src/lib/codex-browser-types.ts +224 -0
  75. package/src/lib/codex-exporter-cli.ts +6 -1
  76. package/src/lib/codex-exporter-db.ts +19 -20
  77. package/src/lib/codex-exporter-transcript.ts +158 -34
  78. package/src/lib/codex-exporter-types.ts +8 -0
  79. package/src/lib/codex-thread-cache.ts +58 -0
  80. package/src/lib/codex-thread-parser.ts +604 -0
  81. package/src/lib/interactive-cli.ts +10 -25
  82. package/src/lib/model-label.ts +24 -0
  83. package/src/lib/native-open.ts +54 -0
  84. package/src/lib/path-transforms.ts +46 -0
  85. package/src/lib/shared.ts +15 -1
  86. package/src/lib/sqlite-error.ts +14 -0
  87. package/src/lib/sqlite-retry.ts +53 -0
  88. package/src/lib/ui-cache.ts +96 -0
  89. package/src/lib/ui-export-files.ts +77 -0
  90. package/src/mcp-server.ts +1 -0
  91. package/src/spiracha.ts +16 -4
  92. package/src/ui-cli.ts +310 -0
@@ -0,0 +1,91 @@
1
+ import { t as formatModelLabel$1 } from "./model-label-B1NWGc65.js";
2
+ //#region src/lib/formatters.ts
3
+ var DATE_TIME_FORMATTERS = /* @__PURE__ */ new Map();
4
+ var getDateTimeFormatters = (timeZone) => {
5
+ const cacheKey = timeZone ?? "local";
6
+ const cached = DATE_TIME_FORMATTERS.get(cacheKey);
7
+ if (cached) return cached;
8
+ const created = {
9
+ dayKeyFormatter: new Intl.DateTimeFormat("en-CA", {
10
+ day: "2-digit",
11
+ month: "2-digit",
12
+ timeZone,
13
+ year: "numeric"
14
+ }),
15
+ timePartsFormatter: new Intl.DateTimeFormat("en-US", {
16
+ day: "numeric",
17
+ hour: "numeric",
18
+ hour12: true,
19
+ minute: "2-digit",
20
+ month: "short",
21
+ timeZone,
22
+ year: "numeric"
23
+ })
24
+ };
25
+ DATE_TIME_FORMATTERS.set(cacheKey, created);
26
+ return created;
27
+ };
28
+ var buildDayKey = (date, timeZone) => {
29
+ return getDateTimeFormatters(timeZone).dayKeyFormatter.format(date);
30
+ };
31
+ var formatTimeParts = (date, timeZone) => {
32
+ const parts = getDateTimeFormatters(timeZone).timePartsFormatter.formatToParts(date);
33
+ const partMap = new Map(parts.map((part) => [part.type, part.value]));
34
+ const hour = partMap.get("hour");
35
+ const minute = partMap.get("minute");
36
+ const dayPeriod = partMap.get("dayPeriod");
37
+ if (!hour || !minute || !dayPeriod) return null;
38
+ return {
39
+ day: partMap.get("day") ?? "",
40
+ month: partMap.get("month") ?? "",
41
+ time: `${hour}:${minute} ${dayPeriod}`.trim(),
42
+ year: partMap.get("year") ?? ""
43
+ };
44
+ };
45
+ var formatNumber = (value) => {
46
+ return new Intl.NumberFormat("en-US").format(value);
47
+ };
48
+ var formatTokens = (value) => {
49
+ return `${formatNumber(value)} tokens`;
50
+ };
51
+ var formatBytes = (value) => {
52
+ if (!value || value <= 0) return "0 B";
53
+ const units = [
54
+ "B",
55
+ "KB",
56
+ "MB",
57
+ "GB",
58
+ "TB"
59
+ ];
60
+ let size = value;
61
+ let unitIndex = 0;
62
+ while (size >= 1024 && unitIndex < units.length - 1) {
63
+ size /= 1024;
64
+ unitIndex += 1;
65
+ }
66
+ const fractionDigits = size >= 100 || unitIndex === 0 ? 0 : 1;
67
+ return `${size.toFixed(fractionDigits)} ${units[unitIndex]}`;
68
+ };
69
+ var formatDateTime = (value, options = {}) => {
70
+ if (value === null || value === void 0 || value === "") return "n/a";
71
+ const date = new Date(value);
72
+ if (Number.isNaN(date.getTime())) return "n/a";
73
+ const now = options.now ?? /* @__PURE__ */ new Date();
74
+ const parts = formatTimeParts(date, options.timeZone);
75
+ if (!parts) return "n/a";
76
+ const { day, month, time, year } = parts;
77
+ if (buildDayKey(date, options.timeZone) === buildDayKey(now, options.timeZone)) return time;
78
+ const currentYear = formatTimeParts(now, options.timeZone)?.year;
79
+ if (year && currentYear && year !== currentYear) return `${month} ${day}, ${year} · ${time}`;
80
+ return `${month} ${day} · ${time}`;
81
+ };
82
+ var formatList = (values) => {
83
+ if (values.length === 0) return "n/a";
84
+ return values.join(", ");
85
+ };
86
+ var formatBooleanLabel = (value) => {
87
+ return value ? "Yes" : "No";
88
+ };
89
+ var formatModelLabel = formatModelLabel$1;
90
+ //#endregion
91
+ export { formatModelLabel as a, formatList as i, formatBytes as n, formatNumber as o, formatDateTime as r, formatTokens as s, formatBooleanLabel as t };
@@ -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,13 @@
1
+ //#region ../../src/lib/model-label.ts
2
+ var formatModelLabel = (value) => {
3
+ if (!value) return "Assistant";
4
+ return value.split(/[-_\s]+/u).filter(Boolean).map((part) => {
5
+ const lower = part.toLowerCase();
6
+ if (lower === "gpt") return "GPT";
7
+ if (/^[a-z]\d$/u.test(lower)) return lower.toUpperCase();
8
+ if (/^\d+(\.\d+)*$/u.test(part)) return part;
9
+ return `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`;
10
+ }).join(" ");
11
+ };
12
+ //#endregion
13
+ export { formatModelLabel 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(/\/home\/[^/\\]+(?=\/|$)/gu, "~").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,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-CcJLp_A8.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.$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,337 @@
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-CJ7l0ynC.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-FJaGZgJk.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-C5rkk_Bo.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
+ suppressHydrationWarning: true,
44
+ children: formatDateTime(info.getValue())
45
+ }),
46
+ header: "Updated",
47
+ id: "updatedAt"
48
+ }),
49
+ columnHelper.accessor((row) => row.thread.created_at_ms ?? row.thread.created_at * 1e3, {
50
+ cell: (info) => /* @__PURE__ */ jsx("span", {
51
+ className: "whitespace-nowrap text-sm",
52
+ suppressHydrationWarning: true,
53
+ children: formatDateTime(info.getValue())
54
+ }),
55
+ header: "Created",
56
+ id: "createdAt"
57
+ }),
58
+ columnHelper.accessor((row) => row.thread.model ?? "unknown", {
59
+ cell: (info) => /* @__PURE__ */ jsx("span", {
60
+ className: "truncate font-mono text-sm",
61
+ children: info.getValue()
62
+ }),
63
+ header: "Model",
64
+ id: "model"
65
+ }),
66
+ columnHelper.accessor((row) => row.thread.tokens_used, {
67
+ cell: (info) => /* @__PURE__ */ jsx("span", {
68
+ className: "whitespace-nowrap font-mono text-sm",
69
+ children: formatTokens(info.getValue())
70
+ }),
71
+ header: "Tokens",
72
+ id: "tokens"
73
+ }),
74
+ columnHelper.accessor((row) => row.rolloutSizeBytes, {
75
+ cell: (info) => /* @__PURE__ */ jsx("span", {
76
+ className: "whitespace-nowrap font-mono text-sm",
77
+ children: formatBytes(info.getValue() ?? 0)
78
+ }),
79
+ header: "Size",
80
+ id: "size"
81
+ }),
82
+ columnHelper.accessor((row) => row.stats.toolCallCount, {
83
+ cell: (info) => info.row.original.stats.deferred ? /* @__PURE__ */ jsx("span", {
84
+ className: "text-[var(--muted-foreground)] text-sm",
85
+ children: "Deferred"
86
+ }) : /* @__PURE__ */ jsx("span", {
87
+ className: "font-mono text-sm",
88
+ children: formatNumber(info.getValue())
89
+ }),
90
+ header: "Tools",
91
+ id: "tools"
92
+ }),
93
+ columnHelper.accessor((row) => row.thread.archived, {
94
+ cell: (info) => /* @__PURE__ */ jsx("span", {
95
+ className: "text-sm",
96
+ children: info.getValue() ? "Archived" : "Active"
97
+ }),
98
+ header: "State",
99
+ id: "state"
100
+ }),
101
+ columnHelper.display({
102
+ cell: (info) => /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
103
+ asChild: true,
104
+ children: /* @__PURE__ */ jsx(Button, {
105
+ className: "rounded-full",
106
+ size: "icon",
107
+ type: "button",
108
+ variant: "ghost",
109
+ onClick: (event) => event.stopPropagation(),
110
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" })
111
+ })
112
+ }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
113
+ align: "end",
114
+ children: [/* @__PURE__ */ jsxs(DropdownMenuItem, {
115
+ onClick: () => onExportThread(info.row.original),
116
+ children: [/* @__PURE__ */ jsx(Download, { className: "mr-2 size-4" }), "Export thread"]
117
+ }), /* @__PURE__ */ jsxs(DropdownMenuItem, {
118
+ className: "text-[var(--destructive)]",
119
+ onClick: () => onDeleteThread(info.row.original),
120
+ children: [/* @__PURE__ */ jsx(Trash2, { className: "mr-2 size-4" }), "Delete thread"]
121
+ })]
122
+ })] }),
123
+ enableSorting: false,
124
+ header: "",
125
+ id: "actions"
126
+ })
127
+ ];
128
+ function ThreadsTable({ threads, onDeleteThread, onDeleteThreads, onExportThread, onExportThreads }) {
129
+ return /* @__PURE__ */ jsx(DataTable, {
130
+ columns: columns(onDeleteThread, onExportThread),
131
+ data: threads,
132
+ emptyMessage: "No threads match the current project filter.",
133
+ enableRowSelection: true,
134
+ getRowId: (row) => row.thread.id,
135
+ initialSorting: defaultSorting,
136
+ renderToolbar: ({ clearSelection, selectedRows }) => {
137
+ if (selectedRows.length === 0) return /* @__PURE__ */ jsx("p", {
138
+ className: "text-[var(--muted-foreground)] text-sm",
139
+ children: "Select threads to export or delete them in a batch."
140
+ });
141
+ const selectedThreadIds = selectedRows.map((row) => row.thread.id);
142
+ return /* @__PURE__ */ jsxs("div", {
143
+ className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
144
+ children: [/* @__PURE__ */ jsxs("p", {
145
+ className: "text-sm",
146
+ children: [
147
+ selectedRows.length,
148
+ " thread",
149
+ selectedRows.length === 1 ? "" : "s",
150
+ " selected"
151
+ ]
152
+ }), /* @__PURE__ */ jsxs("div", {
153
+ className: "flex flex-wrap gap-2",
154
+ children: [
155
+ /* @__PURE__ */ jsxs(Button, {
156
+ className: "rounded-full",
157
+ size: "sm",
158
+ type: "button",
159
+ variant: "outline",
160
+ onClick: () => onExportThreads(selectedThreadIds),
161
+ children: [/* @__PURE__ */ jsx(Download, { className: "mr-2 size-4" }), "Export selected threads"]
162
+ }),
163
+ /* @__PURE__ */ jsxs(Button, {
164
+ className: "rounded-full border-[var(--destructive)]/20 text-[var(--destructive)]",
165
+ size: "sm",
166
+ type: "button",
167
+ variant: "outline",
168
+ onClick: () => onDeleteThreads(selectedThreadIds),
169
+ children: [/* @__PURE__ */ jsx(Trash2, { className: "mr-2 size-4" }), "Delete selected threads"]
170
+ }),
171
+ /* @__PURE__ */ jsxs(Button, {
172
+ className: "rounded-full",
173
+ size: "sm",
174
+ type: "button",
175
+ variant: "ghost",
176
+ onClick: clearSelection,
177
+ children: [/* @__PURE__ */ jsx(X, { className: "mr-2 size-4" }), "Clear selection"]
178
+ })
179
+ ]
180
+ })]
181
+ });
182
+ }
183
+ });
184
+ }
185
+ //#endregion
186
+ //#region src/routes/projects.$project.tsx?tsr-split=component
187
+ function ProjectDetailPage() {
188
+ const params = Route.useParams();
189
+ const queryClient = useQueryClient();
190
+ const threads = useSuspenseQuery(projectThreadsQueryOptions(params.project)).data;
191
+ const { settings } = useSettings();
192
+ const [searchInput, setSearchInput] = useState("");
193
+ const [pendingDelete, setPendingDelete] = useState(null);
194
+ const [pendingExport, setPendingExport] = useState(null);
195
+ const deferredSearch = useDeferredValue(searchInput.trim().toLowerCase());
196
+ const deleteThreadMutation = useMutation({
197
+ mutationFn: (input) => {
198
+ if (input.threadIds.length === 1) return deleteThreadFn({ data: {
199
+ deleteSessionFiles: input.deleteSessionFiles,
200
+ threadId: input.threadIds[0]
201
+ } });
202
+ return deleteThreadsFn({ data: input });
203
+ },
204
+ onSuccess: async () => {
205
+ await Promise.all([
206
+ queryClient.invalidateQueries({ queryKey: ["analytics"] }),
207
+ queryClient.invalidateQueries({ queryKey: ["dashboard"] }),
208
+ queryClient.invalidateQueries({ queryKey: ["project-threads", params.project] }),
209
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
210
+ ]);
211
+ setPendingDelete(null);
212
+ }
213
+ });
214
+ const exportThreadMutation = useMutation({
215
+ mutationFn: async (options) => {
216
+ if (!pendingExport) throw new Error("No thread selected for export");
217
+ console.info("[spiracha:export-ui] request", {
218
+ outputFormat: options.outputFormat,
219
+ project: params.project,
220
+ selectedThreadCount: pendingExport.threadIds.length,
221
+ selectedThreadIds: pendingExport.threadIds
222
+ });
223
+ const download = pendingExport.threadIds.length === 1 ? await exportThreadFn({ data: {
224
+ ...options,
225
+ ...settings,
226
+ threadId: pendingExport.threadIds[0]
227
+ } }) : await exportThreadsFn({ data: {
228
+ ...options,
229
+ ...settings,
230
+ threadIds: pendingExport.threadIds
231
+ } });
232
+ console.info("[spiracha:export-ui] response", {
233
+ downloadUrl: download.mode === "download_url" ? download.downloadUrl : null,
234
+ fileName: download.fileName,
235
+ mode: download.mode,
236
+ project: params.project,
237
+ selectedThreadCount: pendingExport.threadIds.length
238
+ });
239
+ if (download.mode === "download") {
240
+ downloadTextFile(download.fileName, download.content, download.mimeType);
241
+ return;
242
+ }
243
+ await downloadUrlFile(download.fileName, download.downloadUrl);
244
+ },
245
+ onError: (error) => {
246
+ console.error("[spiracha:export-ui] failed", {
247
+ error: error instanceof Error ? error.message : String(error),
248
+ project: params.project,
249
+ selectedThreadCount: pendingExport?.threadIds.length ?? 0,
250
+ selectedThreadIds: pendingExport?.threadIds ?? []
251
+ });
252
+ },
253
+ onSuccess: () => {
254
+ setPendingExport(null);
255
+ }
256
+ });
257
+ const visibleThreads = [...threads].filter((thread) => {
258
+ if (!deferredSearch) return true;
259
+ return `${thread.thread.title}\n${thread.thread.preview}`.toLowerCase().includes(deferredSearch);
260
+ });
261
+ const lookupSelectedThreads = (threadIds) => {
262
+ const threadIdSet = new Set(threadIds);
263
+ return visibleThreads.filter((thread) => threadIdSet.has(thread.thread.id));
264
+ };
265
+ return /* @__PURE__ */ jsxs("div", {
266
+ className: "space-y-6",
267
+ children: [
268
+ /* @__PURE__ */ jsx(PageHeader, {
269
+ actions: /* @__PURE__ */ jsx("div", {
270
+ className: "flex flex-col gap-2 sm:flex-row",
271
+ children: /* @__PURE__ */ jsx(Input, {
272
+ className: "h-10 w-full rounded-full border-[var(--border)] bg-[var(--panel)] px-4 sm:w-[20rem]",
273
+ placeholder: "Search thread title or preview",
274
+ value: searchInput,
275
+ onChange: (event) => {
276
+ startTransition(() => {
277
+ setSearchInput(event.target.value);
278
+ });
279
+ }
280
+ })
281
+ }),
282
+ eyebrow: "Project",
283
+ subtitle: "Sort by any column, inspect tool call volume, and manage thread records for this derived project.",
284
+ title: params.project
285
+ }),
286
+ /* @__PURE__ */ jsx(ThreadsTable, {
287
+ threads: visibleThreads,
288
+ onDeleteThread: (thread) => setPendingDelete({ threads: [thread] }),
289
+ onDeleteThreads: (threadIds) => {
290
+ const selectedThreads = lookupSelectedThreads(threadIds);
291
+ if (selectedThreads.length === 0) return;
292
+ setPendingDelete({ threads: selectedThreads });
293
+ },
294
+ onExportThread: (thread) => setPendingExport({
295
+ threadIds: [thread.thread.id],
296
+ threadLabel: thread.thread.title
297
+ }),
298
+ onExportThreads: (threadIds) => {
299
+ const selectedThreads = lookupSelectedThreads(threadIds);
300
+ if (selectedThreads.length === 0) return;
301
+ setPendingExport({
302
+ threadIds: selectedThreads.map((thread) => thread.thread.id),
303
+ threadLabel: selectedThreads.length === 1 ? selectedThreads[0].thread.title : `${selectedThreads.length} selected threads`
304
+ });
305
+ }
306
+ }),
307
+ /* @__PURE__ */ jsx(DeleteConfirmDialog, {
308
+ confirmLabel: deleteThreadMutation.isPending ? "Deleting..." : "Delete thread",
309
+ 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.` : "",
310
+ open: pendingDelete !== null,
311
+ showDeleteSessionFilesOption: true,
312
+ title: "Delete thread from Codex DB?",
313
+ onConfirm: ({ deleteSessionFiles }) => {
314
+ if (!pendingDelete) return;
315
+ deleteThreadMutation.mutate({
316
+ deleteSessionFiles,
317
+ threadIds: pendingDelete.threads.map((thread) => thread.thread.id)
318
+ });
319
+ },
320
+ onOpenChange: (open) => {
321
+ if (!open) setPendingDelete(null);
322
+ }
323
+ }),
324
+ /* @__PURE__ */ jsx(ExportDialog, {
325
+ open: pendingExport !== null,
326
+ pending: exportThreadMutation.isPending,
327
+ title: pendingExport ? `Export ${pendingExport.threadLabel}` : "Export thread",
328
+ onExport: (options) => exportThreadMutation.mutate(options),
329
+ onOpenChange: (open) => {
330
+ if (!open) setPendingExport(null);
331
+ }
332
+ })
333
+ ]
334
+ });
335
+ }
336
+ //#endregion
337
+ export { ProjectDetailPage as component };
@@ -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 };