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.
Files changed (88) hide show
  1. package/AGENTS.md +28 -1
  2. package/README.md +47 -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-BjYaHqXk.js +1 -0
  7. package/apps/ui/dist/client/assets/checkbox-wPoGG3of.js +1 -0
  8. package/apps/ui/dist/client/assets/data-table-6yDgAdtf.js +4 -0
  9. package/apps/ui/dist/client/assets/delete-confirm-dialog-DJUAk7ha.js +11 -0
  10. package/apps/ui/dist/client/assets/download-BhWd-Pm5.js +1 -0
  11. package/apps/ui/dist/client/assets/es2015-BlyMI4CF.js +41 -0
  12. package/apps/ui/dist/client/assets/formatters-BxjZwWSE.js +1 -0
  13. package/apps/ui/dist/client/assets/index-T01rPkb4.js +22 -0
  14. package/apps/ui/dist/client/assets/input-B3YN8gzg.js +1 -0
  15. package/apps/ui/dist/client/assets/metric-card-BWW7TWER.js +1 -0
  16. package/apps/ui/dist/client/assets/page-header-BZ8Gnxgs.js +1 -0
  17. package/apps/ui/dist/client/assets/projects._project-B7XcpoLt.js +1 -0
  18. package/apps/ui/dist/client/assets/projects._project-EfBhCHPY.js +1 -0
  19. package/apps/ui/dist/client/assets/projects.index-4vfIwLjw.js +1 -0
  20. package/apps/ui/dist/client/assets/projects.index-DzEZ4pAJ.js +1 -0
  21. package/apps/ui/dist/client/assets/routes-CWCCZykE.js +1 -0
  22. package/apps/ui/dist/client/assets/select-DLXGsyZ4.js +1 -0
  23. package/apps/ui/dist/client/assets/settings-b0Xthfae.js +1 -0
  24. package/apps/ui/dist/client/assets/styles-8Wtc8YJw.css +1 -0
  25. package/apps/ui/dist/client/assets/threads._threadId-CgtoCqTb.js +1 -0
  26. package/apps/ui/dist/client/assets/threads._threadId-DBiDb38K.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-BjsXNYgm.js +99 -0
  34. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +6 -0
  35. package/apps/ui/dist/server/assets/analytics-Br_fZB6a.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-Cqh0hb93.js +1995 -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-CzHmFWGk.js +286 -0
  43. package/apps/ui/dist/server/assets/formatters-B6o5pTY9.js +72 -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/page-header-CxdZM86z.js +25 -0
  48. package/apps/ui/dist/server/assets/path-transforms-DD1e7rhY.js +31 -0
  49. package/apps/ui/dist/server/assets/projects._project-Bwf6iJC-.js +335 -0
  50. package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +26 -0
  51. package/apps/ui/dist/server/assets/projects._project-DdVSdfPe.js +18 -0
  52. package/apps/ui/dist/server/assets/projects.index-CaplpeMy.js +26 -0
  53. package/apps/ui/dist/server/assets/projects.index-DKeVeqUZ.js +171 -0
  54. package/apps/ui/dist/server/assets/router-ve2Hrl2Y.js +307 -0
  55. package/apps/ui/dist/server/assets/routes-BJyx5OmO.js +34 -0
  56. package/apps/ui/dist/server/assets/routes-pkOwjjYc.js +168 -0
  57. package/apps/ui/dist/server/assets/select-GW76p-ld.js +76 -0
  58. package/apps/ui/dist/server/assets/settings-MvWDgc1u.js +100 -0
  59. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +52 -0
  60. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +13 -0
  61. package/apps/ui/dist/server/assets/start-BAvbjjfs.js +4 -0
  62. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +26 -0
  63. package/apps/ui/dist/server/assets/threads._threadId-D3PYZIwl.js +18 -0
  64. package/apps/ui/dist/server/assets/threads._threadId-D3xaWM86.js +1037 -0
  65. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +8 -0
  66. package/apps/ui/dist/server/server.js +5678 -0
  67. package/package.json +47 -7
  68. package/src/export-chats.ts +1 -14
  69. package/src/lib/codex-analytics.ts +100 -0
  70. package/src/lib/codex-browser-db.ts +518 -0
  71. package/src/lib/codex-browser-export.ts +418 -0
  72. package/src/lib/codex-browser-types.ts +224 -0
  73. package/src/lib/codex-exporter-cli.ts +5 -0
  74. package/src/lib/codex-exporter-transcript.ts +143 -32
  75. package/src/lib/codex-exporter-types.ts +8 -0
  76. package/src/lib/codex-thread-cache.ts +58 -0
  77. package/src/lib/codex-thread-parser.ts +604 -0
  78. package/src/lib/interactive-cli.ts +5 -13
  79. package/src/lib/native-open.ts +54 -0
  80. package/src/lib/path-transforms.ts +45 -0
  81. package/src/lib/shared.ts +37 -1
  82. package/src/lib/sqlite-error.ts +14 -0
  83. package/src/lib/sqlite-retry.ts +39 -0
  84. package/src/lib/ui-cache.ts +96 -0
  85. package/src/lib/ui-export-files.ts +77 -0
  86. package/src/mcp-server.ts +1 -0
  87. package/src/spiracha.ts +14 -1
  88. 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 };