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,172 @@
1
+ import { t as Button } from "./button-CmTDnzOn.js";
2
+ import { i as projectsQueryOptions, s as deleteProjectFn } from "./codex-queries-CAF6HYiG.js";
3
+ import { t as DataTable } from "./data-table-Cdct823O.js";
4
+ import { t as PageHeader } from "./page-header-CxdZM86z.js";
5
+ import { i as formatList, o as formatNumber, r as formatDateTime, s as formatTokens } from "./formatters-FJaGZgJk.js";
6
+ import { t as DeleteConfirmDialog } from "./delete-confirm-dialog-CWqcTXTF.js";
7
+ import { a as DropdownMenuTrigger, i as DropdownMenuItem, n as DropdownMenu, r as DropdownMenuContent, t as Input } from "./input-B4tEzctc.js";
8
+ import { startTransition, useDeferredValue, useState } from "react";
9
+ import { useNavigate } from "@tanstack/react-router";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
12
+ import { MoreHorizontal, Trash2 } from "lucide-react";
13
+ import { createColumnHelper } from "@tanstack/react-table";
14
+ //#region src/components/projects-table.tsx
15
+ var columnHelper = createColumnHelper();
16
+ var columns = (onDeleteProject) => [
17
+ columnHelper.accessor("name", {
18
+ cell: (info) => /* @__PURE__ */ jsxs("div", {
19
+ className: "space-y-1",
20
+ children: [/* @__PURE__ */ jsx("p", {
21
+ className: "font-medium",
22
+ children: info.getValue()
23
+ }), /* @__PURE__ */ jsxs("p", {
24
+ className: "text-[var(--muted-foreground)] text-xs",
25
+ children: [
26
+ formatNumber(info.row.original.cwdPaths.length),
27
+ " cwd path",
28
+ info.row.original.cwdPaths.length === 1 ? "" : "s"
29
+ ]
30
+ })]
31
+ }),
32
+ header: "Project"
33
+ }),
34
+ columnHelper.accessor("threadCount", {
35
+ cell: (info) => /* @__PURE__ */ jsx("span", {
36
+ className: "font-mono text-sm",
37
+ children: formatNumber(info.getValue())
38
+ }),
39
+ header: "Threads"
40
+ }),
41
+ columnHelper.accessor("totalTokens", {
42
+ cell: (info) => /* @__PURE__ */ jsx("span", {
43
+ className: "font-mono text-sm",
44
+ children: formatTokens(info.getValue())
45
+ }),
46
+ header: "Tokens"
47
+ }),
48
+ columnHelper.accessor("lastUpdatedAtMs", {
49
+ cell: (info) => /* @__PURE__ */ jsx("span", {
50
+ className: "text-sm",
51
+ suppressHydrationWarning: true,
52
+ children: formatDateTime(info.getValue())
53
+ }),
54
+ header: "Last updated"
55
+ }),
56
+ columnHelper.display({
57
+ cell: (info) => /* @__PURE__ */ jsx("span", {
58
+ className: "text-[var(--muted-foreground)] text-sm",
59
+ children: formatList(info.row.original.modelNames)
60
+ }),
61
+ header: "Models",
62
+ id: "models"
63
+ }),
64
+ columnHelper.accessor("archivedThreadCount", {
65
+ cell: (info) => /* @__PURE__ */ jsx("span", {
66
+ className: "text-sm",
67
+ children: formatNumber(info.getValue())
68
+ }),
69
+ header: "Archived"
70
+ }),
71
+ columnHelper.display({
72
+ cell: (info) => /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
73
+ asChild: true,
74
+ children: /* @__PURE__ */ jsx(Button, {
75
+ className: "rounded-full",
76
+ size: "icon",
77
+ type: "button",
78
+ variant: "ghost",
79
+ onClick: (event) => event.stopPropagation(),
80
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" })
81
+ })
82
+ }), /* @__PURE__ */ jsx(DropdownMenuContent, {
83
+ align: "end",
84
+ children: /* @__PURE__ */ jsxs(DropdownMenuItem, {
85
+ className: "text-[var(--destructive)]",
86
+ onClick: () => onDeleteProject(info.row.original),
87
+ children: [/* @__PURE__ */ jsx(Trash2, { className: "mr-2 size-4" }), "Delete project"]
88
+ })
89
+ })] }),
90
+ header: "",
91
+ id: "actions"
92
+ })
93
+ ];
94
+ function ProjectsTable({ projects, onDeleteProject }) {
95
+ const navigate = useNavigate();
96
+ return /* @__PURE__ */ jsx(DataTable, {
97
+ columns: columns(onDeleteProject),
98
+ data: projects,
99
+ emptyMessage: "No projects match the current search.",
100
+ onRowClick: (project) => navigate({
101
+ params: { project: project.name },
102
+ to: "/projects/$project"
103
+ })
104
+ });
105
+ }
106
+ //#endregion
107
+ //#region src/routes/projects.index.tsx?tsr-split=component
108
+ function ProjectsPage() {
109
+ const queryClient = useQueryClient();
110
+ const projects = useSuspenseQuery(projectsQueryOptions()).data;
111
+ const [searchInput, setSearchInput] = useState("");
112
+ const [pendingDelete, setPendingDelete] = useState(null);
113
+ const deferredSearch = useDeferredValue(searchInput.trim().toLowerCase());
114
+ const deleteProjectMutation = useMutation({
115
+ mutationFn: (input) => deleteProjectFn({ data: input }),
116
+ onSuccess: async () => {
117
+ await Promise.all([
118
+ queryClient.invalidateQueries({ queryKey: ["analytics"] }),
119
+ queryClient.invalidateQueries({ queryKey: ["dashboard"] }),
120
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
121
+ ]);
122
+ setPendingDelete(null);
123
+ }
124
+ });
125
+ const visibleProjects = projects.filter((project) => {
126
+ if (!deferredSearch) return true;
127
+ return project.name.toLowerCase().includes(deferredSearch);
128
+ });
129
+ return /* @__PURE__ */ jsxs("div", {
130
+ className: "space-y-6",
131
+ children: [
132
+ /* @__PURE__ */ jsx(PageHeader, {
133
+ actions: /* @__PURE__ */ jsx(Input, {
134
+ className: "h-10 w-full rounded-full border-[var(--border)] bg-[var(--panel)] px-4 sm:w-[20rem]",
135
+ placeholder: "Search projects by name",
136
+ value: searchInput,
137
+ onChange: (event) => {
138
+ startTransition(() => {
139
+ setSearchInput(event.target.value);
140
+ });
141
+ }
142
+ }),
143
+ eyebrow: "Inventory",
144
+ subtitle: "Derived projects are grouped from the final basename of each thread cwd, matching the existing CLI behavior.",
145
+ title: "Projects"
146
+ }),
147
+ /* @__PURE__ */ jsx(ProjectsTable, {
148
+ projects: visibleProjects,
149
+ onDeleteProject: setPendingDelete
150
+ }),
151
+ /* @__PURE__ */ jsx(DeleteConfirmDialog, {
152
+ confirmLabel: deleteProjectMutation.isPending ? "Deleting..." : "Delete project",
153
+ description: pendingDelete ? `Delete ${pendingDelete.threadCount} thread records for the derived project "${pendingDelete.name}" from the Codex database. Enable Delete Session files to remove the rollout JSONL files too.` : "",
154
+ open: pendingDelete !== null,
155
+ showDeleteSessionFilesOption: true,
156
+ title: "Delete project from Codex DB?",
157
+ onConfirm: ({ deleteSessionFiles }) => {
158
+ if (!pendingDelete) return;
159
+ deleteProjectMutation.mutate({
160
+ deleteSessionFiles,
161
+ project: pendingDelete.name
162
+ });
163
+ },
164
+ onOpenChange: (open) => {
165
+ if (!open) setPendingDelete(null);
166
+ }
167
+ })
168
+ ]
169
+ });
170
+ }
171
+ //#endregion
172
+ export { ProjectsPage as component };
@@ -0,0 +1,307 @@
1
+ import { t as cn } from "./utils-C_uf36nf.js";
2
+ import { t as Button } from "./button-CmTDnzOn.js";
3
+ import { t as SettingsProvider } from "./settings-store-DpEJEQ7M.js";
4
+ import { i as projectsQueryOptions, t as analyticsQueryOptions } from "./codex-queries-CAF6HYiG.js";
5
+ import { t as Route$5 } from "./routes-BhbxvJE7.js";
6
+ import { t as Route$6 } from "./threads._threadId-Ba7vv6-K.js";
7
+ import { t as Route$7 } from "./projects._project-CJ7l0ynC.js";
8
+ import { useEffect, useState } from "react";
9
+ import { HeadContent, Link, Outlet, Scripts, createFileRoute, createRootRouteWithContext, createRouter, lazyRouteComponent, notFound, redirect, useRouterState } from "@tanstack/react-router";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
12
+ import { QueryClient } from "@tanstack/react-query";
13
+ import { BarChart3, FolderOpen, LayoutDashboard, Moon, Settings2, Sun } from "lucide-react";
14
+ import { Tooltip } from "radix-ui";
15
+ //#region src/integrations/tanstack-query/root-provider.tsx
16
+ function getContext() {
17
+ return { queryClient: new QueryClient() };
18
+ }
19
+ //#endregion
20
+ //#region src/components/theme-toggle.tsx
21
+ var getPreferredTheme = () => {
22
+ if (typeof window === "undefined") return "light";
23
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
24
+ };
25
+ var applyTheme = (mode) => {
26
+ document.documentElement.classList.toggle("dark", mode === "dark");
27
+ document.documentElement.classList.toggle("light", mode === "light");
28
+ document.documentElement.style.colorScheme = mode;
29
+ window.localStorage.setItem("spiracha-theme", mode);
30
+ };
31
+ function ThemeToggle() {
32
+ const [theme, setTheme] = useState("light");
33
+ useEffect(() => {
34
+ const storedTheme = window.localStorage.getItem("spiracha-theme");
35
+ const nextTheme = storedTheme === "dark" || storedTheme === "light" ? storedTheme : getPreferredTheme();
36
+ setTheme(nextTheme);
37
+ applyTheme(nextTheme);
38
+ }, []);
39
+ return /* @__PURE__ */ jsx(Button, {
40
+ "aria-label": "Toggle color theme",
41
+ className: "h-9 rounded-full border border-[var(--border)] bg-[var(--panel)] px-3 text-[var(--panel-foreground)] shadow-none",
42
+ size: "sm",
43
+ type: "button",
44
+ variant: "ghost",
45
+ onClick: () => {
46
+ const nextTheme = theme === "light" ? "dark" : "light";
47
+ setTheme(nextTheme);
48
+ applyTheme(nextTheme);
49
+ },
50
+ children: theme === "light" ? /* @__PURE__ */ jsx(Moon, { className: "size-4" }) : /* @__PURE__ */ jsx(Sun, { className: "size-4" })
51
+ });
52
+ }
53
+ //#endregion
54
+ //#region src/components/app-shell.tsx
55
+ var navItems = [
56
+ {
57
+ icon: LayoutDashboard,
58
+ label: "Dashboard",
59
+ to: "/"
60
+ },
61
+ {
62
+ icon: FolderOpen,
63
+ label: "Projects",
64
+ to: "/projects"
65
+ },
66
+ {
67
+ icon: BarChart3,
68
+ label: "Analytics",
69
+ to: "/analytics"
70
+ },
71
+ {
72
+ icon: Settings2,
73
+ label: "Settings",
74
+ to: "/settings"
75
+ }
76
+ ];
77
+ function AppShell({ children }) {
78
+ const pathname = useRouterState({ select: (state) => state.location.pathname });
79
+ return /* @__PURE__ */ jsx("div", {
80
+ className: "min-h-screen bg-[var(--background)] text-[var(--foreground)]",
81
+ children: /* @__PURE__ */ jsxs("div", {
82
+ className: "flex min-h-screen w-full flex-col lg:flex-row",
83
+ children: [/* @__PURE__ */ jsxs("aside", {
84
+ className: "border-[var(--border)] border-b bg-[var(--panel)]/90 px-5 py-5 backdrop-blur lg:sticky lg:top-0 lg:h-screen lg:w-[240px] lg:border-r lg:border-b-0 lg:px-5",
85
+ children: [/* @__PURE__ */ jsxs("div", {
86
+ className: "flex items-start justify-between gap-4 lg:flex-col lg:items-stretch",
87
+ children: [/* @__PURE__ */ jsxs("div", {
88
+ className: "space-y-1.5",
89
+ children: [/* @__PURE__ */ jsx("p", {
90
+ className: "font-semibold text-[10px] text-[var(--muted-foreground)] uppercase tracking-[0.18em]",
91
+ children: "Spiracha"
92
+ }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("h1", {
93
+ className: "font-['IBM_Plex_Sans'] font-semibold text-lg tracking-[-0.02em]",
94
+ children: "Codex Console"
95
+ }) })]
96
+ }), /* @__PURE__ */ jsx(ThemeToggle, {})]
97
+ }), /* @__PURE__ */ jsx("nav", {
98
+ className: "mt-5 grid gap-1",
99
+ children: navItems.map((item) => {
100
+ const active = pathname === item.to || pathname.startsWith(`${item.to}/`);
101
+ const Icon = item.icon;
102
+ return /* @__PURE__ */ jsxs(Link, {
103
+ className: cn("flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm transition-colors", active ? "bg-[var(--accent-muted)] font-medium text-[var(--accent-foreground)]" : "text-[var(--muted-foreground)] hover:bg-[var(--panel-secondary)] hover:text-[var(--foreground)]"),
104
+ to: item.to,
105
+ children: [/* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0" }), /* @__PURE__ */ jsx("span", { children: item.label })]
106
+ }, item.to);
107
+ })
108
+ })]
109
+ }), /* @__PURE__ */ jsx("main", {
110
+ className: "min-w-0 flex-1 px-4 py-4 sm:px-5 sm:py-5 lg:px-6",
111
+ children
112
+ })]
113
+ })
114
+ });
115
+ }
116
+ //#endregion
117
+ //#region src/components/ui/tooltip.tsx
118
+ function TooltipProvider({ delayDuration = 0, ...props }) {
119
+ return /* @__PURE__ */ jsx(Tooltip.Provider, {
120
+ "data-slot": "tooltip-provider",
121
+ delayDuration,
122
+ ...props
123
+ });
124
+ }
125
+ //#endregion
126
+ //#region src/styles.css?url
127
+ var styles_default = "/assets/styles-CMrP9Jb4.css";
128
+ //#endregion
129
+ //#region src/routes/__root.tsx
130
+ var themeInitScript = `
131
+ (() => {
132
+ try {
133
+ const stored = window.localStorage.getItem('spiracha-theme')
134
+ const preferred = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
135
+ const mode = stored === 'dark' || stored === 'light' ? stored : preferred
136
+ document.documentElement.classList.add(mode)
137
+ document.documentElement.style.colorScheme = mode
138
+ } catch {}
139
+ })()
140
+ `;
141
+ function RootErrorComponent({ error }) {
142
+ const isSqliteError = error.message.includes("unable to open database") || error.message.includes("database is locked") || error.message.includes("SQLITE_");
143
+ return /* @__PURE__ */ jsx("div", {
144
+ className: "flex min-h-screen items-center justify-center bg-[#101418] px-4 text-[#eef3f7]",
145
+ children: /* @__PURE__ */ jsxs("div", {
146
+ className: "max-w-[30rem] text-center",
147
+ children: [
148
+ /* @__PURE__ */ jsx("h1", {
149
+ className: "mb-3 font-semibold text-base",
150
+ children: isSqliteError ? "Database unavailable" : "Something went wrong"
151
+ }),
152
+ isSqliteError ? /* @__PURE__ */ jsx("p", {
153
+ className: "my-2 text-[#99a3af] text-[0.875rem] leading-6",
154
+ children: "Spiracha could not open the Codex SQLite database. Codex may have an exclusive lock on the file, or the database does not exist yet. Close Codex or wait a moment, then reload."
155
+ }) : /* @__PURE__ */ jsx("p", {
156
+ className: "my-2 text-[#99a3af] text-[0.875rem] leading-6",
157
+ children: /* @__PURE__ */ jsx("code", {
158
+ className: "rounded border border-white/10 bg-[#12181e] px-1.5 py-1 text-[0.8em]",
159
+ children: error.message
160
+ })
161
+ }),
162
+ /* @__PURE__ */ jsx("button", {
163
+ className: "mt-6 rounded-full border border-white/15 bg-[#1a222b] px-5 py-2 text-sm hover:bg-[#222e3a]",
164
+ type: "button",
165
+ onClick: () => window.location.reload(),
166
+ children: "Reload"
167
+ })
168
+ ]
169
+ })
170
+ });
171
+ }
172
+ var Route$4 = createRootRouteWithContext()({
173
+ component: RootComponent,
174
+ errorComponent: RootErrorComponent,
175
+ head: () => ({
176
+ links: [{
177
+ href: styles_default,
178
+ rel: "stylesheet"
179
+ }],
180
+ meta: [
181
+ { charSet: "utf-8" },
182
+ {
183
+ content: "width=device-width, initial-scale=1",
184
+ name: "viewport"
185
+ },
186
+ {
187
+ content: "Browse local Codex threads, projects, tool calls, and analytics through a compact workspace UI.",
188
+ name: "description"
189
+ },
190
+ { title: "Spiracha UI" }
191
+ ]
192
+ })
193
+ });
194
+ function RootComponent() {
195
+ return /* @__PURE__ */ jsx(RootDocument, { children: /* @__PURE__ */ jsx(SettingsProvider, { children: /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsx(AppShell, { children: /* @__PURE__ */ jsx(Outlet, {}) }) }) }) });
196
+ }
197
+ function RootDocument({ children }) {
198
+ return /* @__PURE__ */ jsxs("html", {
199
+ lang: "en",
200
+ suppressHydrationWarning: true,
201
+ children: [/* @__PURE__ */ jsxs("head", { children: [/* @__PURE__ */ jsx("script", { children: themeInitScript }), /* @__PURE__ */ jsx(HeadContent, {})] }), /* @__PURE__ */ jsxs("body", { children: [children, /* @__PURE__ */ jsx(Scripts, {})] })]
202
+ });
203
+ }
204
+ //#endregion
205
+ //#region src/routes/settings.tsx
206
+ var $$splitComponentImporter$3 = () => import("./settings-MvWDgc1u.js");
207
+ var Route$3 = createFileRoute("/settings")({ component: lazyRouteComponent($$splitComponentImporter$3, "component") });
208
+ //#endregion
209
+ //#region src/routes/analytics.tsx
210
+ var $$splitComponentImporter$2 = () => import("./analytics-BMxW_bZL.js");
211
+ var Route$2 = createFileRoute("/analytics")({
212
+ component: lazyRouteComponent($$splitComponentImporter$2, "component"),
213
+ loader: ({ context }) => {
214
+ return Promise.all([context.queryClient.ensureQueryData(projectsQueryOptions()), context.queryClient.ensureQueryData(analyticsQueryOptions(null))]);
215
+ }
216
+ });
217
+ //#endregion
218
+ //#region src/lib/thread-id.ts
219
+ var THREAD_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
220
+ var isCodexThreadId = (value) => THREAD_ID_PATTERN.test(value);
221
+ //#endregion
222
+ //#region src/routes/$threadId.tsx
223
+ var $$splitComponentImporter$1 = () => import("./_threadId-B6SrBR9E.js");
224
+ var Route$1 = createFileRoute("/$threadId")({
225
+ beforeLoad: ({ params }) => {
226
+ if (!isCodexThreadId(params.threadId)) throw notFound();
227
+ throw redirect({
228
+ params: { threadId: params.threadId },
229
+ to: "/threads/$threadId"
230
+ });
231
+ },
232
+ component: lazyRouteComponent($$splitComponentImporter$1, "component")
233
+ });
234
+ //#endregion
235
+ //#region src/routes/projects.index.tsx
236
+ var $$splitErrorComponentImporter = () => import("./projects.index-CaplpeMy.js");
237
+ var $$splitComponentImporter = () => import("./projects.index-srtogpuF.js");
238
+ var Route = createFileRoute("/projects/")({
239
+ component: lazyRouteComponent($$splitComponentImporter, "component"),
240
+ errorComponent: lazyRouteComponent($$splitErrorComponentImporter, "errorComponent"),
241
+ loader: ({ context }) => context.queryClient.ensureQueryData(projectsQueryOptions())
242
+ });
243
+ //#endregion
244
+ //#region src/routeTree.gen.ts
245
+ var SettingsRoute = Route$3.update({
246
+ id: "/settings",
247
+ path: "/settings",
248
+ getParentRoute: () => Route$4
249
+ });
250
+ var AnalyticsRoute = Route$2.update({
251
+ id: "/analytics",
252
+ path: "/analytics",
253
+ getParentRoute: () => Route$4
254
+ });
255
+ var ThreadIdRoute = Route$1.update({
256
+ id: "/$threadId",
257
+ path: "/$threadId",
258
+ getParentRoute: () => Route$4
259
+ });
260
+ var IndexRoute = Route$5.update({
261
+ id: "/",
262
+ path: "/",
263
+ getParentRoute: () => Route$4
264
+ });
265
+ var ProjectsIndexRoute = Route.update({
266
+ id: "/projects/",
267
+ path: "/projects/",
268
+ getParentRoute: () => Route$4
269
+ });
270
+ var ThreadsThreadIdRoute = Route$6.update({
271
+ id: "/threads/$threadId",
272
+ path: "/threads/$threadId",
273
+ getParentRoute: () => Route$4
274
+ });
275
+ var rootRouteChildren = {
276
+ IndexRoute,
277
+ ThreadIdRoute,
278
+ AnalyticsRoute,
279
+ SettingsRoute,
280
+ ProjectsProjectRoute: Route$7.update({
281
+ id: "/projects/$project",
282
+ path: "/projects/$project",
283
+ getParentRoute: () => Route$4
284
+ }),
285
+ ThreadsThreadIdRoute,
286
+ ProjectsIndexRoute
287
+ };
288
+ var routeTree = Route$4._addFileChildren(rootRouteChildren)._addFileTypes();
289
+ //#endregion
290
+ //#region src/router.tsx
291
+ function getRouter() {
292
+ const context = getContext();
293
+ const router = createRouter({
294
+ context,
295
+ defaultPreload: "intent",
296
+ defaultPreloadStaleTime: 0,
297
+ routeTree,
298
+ scrollRestoration: true
299
+ });
300
+ setupRouterSsrQueryIntegration({
301
+ queryClient: context.queryClient,
302
+ router
303
+ });
304
+ return router;
305
+ }
306
+ //#endregion
307
+ export { getRouter };
@@ -0,0 +1,34 @@
1
+ import { n as dashboardQueryOptions } from "./codex-queries-CAF6HYiG.js";
2
+ import { createFileRoute, lazyRouteComponent } from "@tanstack/react-router";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/routes/index.tsx
5
+ var $$splitComponentImporter = () => import("./routes-CPe-ppmC.js");
6
+ var Route = createFileRoute("/")({
7
+ component: lazyRouteComponent($$splitComponentImporter, "component"),
8
+ loader: ({ context }) => context.queryClient.ensureQueryData(dashboardQueryOptions())
9
+ });
10
+ function DashboardErrorComponent({ error }) {
11
+ const isSqlite = error.message.includes("unable to open database") || error.message.includes("database is locked");
12
+ return /* @__PURE__ */ jsxs("div", {
13
+ className: "rounded-xl border border-[var(--border)] bg-[var(--panel)] px-6 py-10 text-center",
14
+ children: [
15
+ /* @__PURE__ */ jsx("p", {
16
+ className: "font-medium text-[var(--destructive)] text-sm",
17
+ children: isSqlite ? "Database unavailable" : "Failed to load dashboard"
18
+ }),
19
+ /* @__PURE__ */ jsx("p", {
20
+ className: "mt-2 text-[var(--muted-foreground)] text-sm",
21
+ children: isSqlite ? "Codex may have an exclusive lock on the database. Reload to retry." : error.message
22
+ }),
23
+ /* @__PURE__ */ jsx("button", {
24
+ className: "mt-4 text-[var(--accent)] text-sm underline-offset-2 hover:underline",
25
+ type: "button",
26
+ onClick: () => window.location.reload(),
27
+ children: "Reload"
28
+ })
29
+ ]
30
+ });
31
+ }
32
+ Route.update({ errorComponent: DashboardErrorComponent });
33
+ //#endregion
34
+ export { Route as t };
@@ -0,0 +1,169 @@
1
+ import { n as dashboardQueryOptions } from "./codex-queries-CAF6HYiG.js";
2
+ import { t as Route } from "./routes-BhbxvJE7.js";
3
+ import { t as MetricCard } from "./metric-card-ByEeLu0r.js";
4
+ import { t as PageHeader } from "./page-header-CxdZM86z.js";
5
+ import { o as formatNumber, r as formatDateTime, s as formatTokens } from "./formatters-FJaGZgJk.js";
6
+ import { Link } from "@tanstack/react-router";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { useSuspenseQuery } from "@tanstack/react-query";
9
+ //#region src/routes/index.tsx?tsr-split=component
10
+ function DashboardErrorComponent({ error }) {
11
+ const isSqlite = error.message.includes("unable to open database") || error.message.includes("database is locked");
12
+ return /* @__PURE__ */ jsxs("div", {
13
+ className: "rounded-xl border border-[var(--border)] bg-[var(--panel)] px-6 py-10 text-center",
14
+ children: [
15
+ /* @__PURE__ */ jsx("p", {
16
+ className: "font-medium text-[var(--destructive)] text-sm",
17
+ children: isSqlite ? "Database unavailable" : "Failed to load dashboard"
18
+ }),
19
+ /* @__PURE__ */ jsx("p", {
20
+ className: "mt-2 text-[var(--muted-foreground)] text-sm",
21
+ children: isSqlite ? "Codex may have an exclusive lock on the database. Reload to retry." : error.message
22
+ }),
23
+ /* @__PURE__ */ jsx("button", {
24
+ className: "mt-4 text-[var(--accent)] text-sm underline-offset-2 hover:underline",
25
+ type: "button",
26
+ onClick: () => window.location.reload(),
27
+ children: "Reload"
28
+ })
29
+ ]
30
+ });
31
+ }
32
+ Route.update({ errorComponent: DashboardErrorComponent });
33
+ function DashboardPage() {
34
+ const dashboard = useSuspenseQuery(dashboardQueryOptions()).data;
35
+ return /* @__PURE__ */ jsxs("div", {
36
+ className: "space-y-6",
37
+ children: [
38
+ /* @__PURE__ */ jsx(PageHeader, {
39
+ eyebrow: "Overview",
40
+ subtitle: "A compact readout of local Codex activity, project distribution, and recent thread movement.",
41
+ title: "Dashboard"
42
+ }),
43
+ /* @__PURE__ */ jsxs("div", {
44
+ className: "grid gap-3 md:grid-cols-2 xl:grid-cols-4",
45
+ children: [
46
+ /* @__PURE__ */ jsx(MetricCard, {
47
+ label: "Threads",
48
+ value: formatNumber(dashboard.totalThreads)
49
+ }),
50
+ /* @__PURE__ */ jsx(MetricCard, {
51
+ label: "Projects",
52
+ value: formatNumber(dashboard.totalProjects)
53
+ }),
54
+ /* @__PURE__ */ jsx(MetricCard, {
55
+ label: "Tokens",
56
+ value: formatTokens(dashboard.totalTokens)
57
+ }),
58
+ /* @__PURE__ */ jsx(MetricCard, {
59
+ helper: `${formatNumber(dashboard.archivedThreads)} archived`,
60
+ label: "Active",
61
+ value: formatNumber(dashboard.activeThreads)
62
+ })
63
+ ]
64
+ }),
65
+ /* @__PURE__ */ jsxs("div", {
66
+ className: "grid min-w-0 gap-4 xl:grid-cols-[1.4fr_1fr]",
67
+ children: [/* @__PURE__ */ jsxs("section", {
68
+ className: "min-w-0 overflow-hidden rounded-[1.8rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]",
69
+ children: [/* @__PURE__ */ jsxs("div", {
70
+ className: "flex items-center justify-between gap-3",
71
+ children: [/* @__PURE__ */ jsxs("div", {
72
+ className: "min-w-0",
73
+ children: [/* @__PURE__ */ jsx("p", {
74
+ className: "font-semibold text-sm",
75
+ children: "Recent threads"
76
+ }), /* @__PURE__ */ jsx("p", {
77
+ className: "mt-0.5 text-[var(--muted-foreground)] text-xs",
78
+ children: "Most recently updated threads across the local Codex database."
79
+ })]
80
+ }), /* @__PURE__ */ jsx(Link, {
81
+ className: "shrink-0 font-medium text-[var(--accent)] text-sm",
82
+ to: "/projects",
83
+ children: "View projects"
84
+ })]
85
+ }), /* @__PURE__ */ jsx("div", {
86
+ className: "mt-4 space-y-2",
87
+ children: dashboard.recentThreads.map((thread) => /* @__PURE__ */ jsxs(Link, {
88
+ className: "block overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3.5 transition-colors hover:border-[var(--accent)]/30 hover:bg-[var(--accent-muted)]",
89
+ params: { threadId: thread.id },
90
+ to: "/threads/$threadId",
91
+ children: [/* @__PURE__ */ jsxs("div", {
92
+ className: "flex min-w-0 flex-wrap items-start justify-between gap-2",
93
+ children: [/* @__PURE__ */ jsxs("div", {
94
+ className: "min-w-0 shrink",
95
+ children: [/* @__PURE__ */ jsx("p", {
96
+ className: "truncate font-medium text-sm",
97
+ children: thread.title
98
+ }), /* @__PURE__ */ jsx("p", {
99
+ className: "mt-0.5 line-clamp-1 text-[var(--muted-foreground)] text-xs",
100
+ children: thread.preview
101
+ })]
102
+ }), /* @__PURE__ */ jsx("p", {
103
+ className: "shrink-0 whitespace-nowrap font-mono text-[var(--muted-foreground)] text-xs",
104
+ suppressHydrationWarning: true,
105
+ children: formatDateTime(thread.updated_at_ms ?? thread.updated_at * 1e3)
106
+ })]
107
+ }), /* @__PURE__ */ jsxs("div", {
108
+ className: "mt-2 flex flex-wrap gap-3 text-[var(--muted-foreground)] text-xs",
109
+ children: [
110
+ /* @__PURE__ */ jsx("span", {
111
+ className: "font-mono",
112
+ children: thread.model ?? "unknown model"
113
+ }),
114
+ /* @__PURE__ */ jsx("span", { children: formatTokens(thread.tokens_used) }),
115
+ /* @__PURE__ */ jsx("span", { children: thread.thread_source ?? "n/a" })
116
+ ]
117
+ })]
118
+ }, thread.id))
119
+ })]
120
+ }), /* @__PURE__ */ jsxs("div", {
121
+ className: "grid min-w-0 gap-4",
122
+ children: [/* @__PURE__ */ jsxs("section", {
123
+ className: "min-w-0 overflow-hidden rounded-[1.8rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]",
124
+ children: [/* @__PURE__ */ jsx("p", {
125
+ className: "font-semibold text-sm",
126
+ children: "Top projects by thread count"
127
+ }), /* @__PURE__ */ jsx("div", {
128
+ className: "mt-3 space-y-2",
129
+ children: dashboard.topProjectsByThreadCount.map((project) => /* @__PURE__ */ jsxs(Link, {
130
+ className: "flex items-center justify-between gap-3 rounded-lg border border-[var(--border)] bg-[var(--panel-secondary)] px-3.5 py-2.5 text-sm",
131
+ params: { project: project.name },
132
+ to: "/projects/$project",
133
+ children: [/* @__PURE__ */ jsx("span", {
134
+ className: "truncate",
135
+ children: project.name
136
+ }), /* @__PURE__ */ jsx("span", {
137
+ className: "shrink-0 font-mono text-[var(--muted-foreground)]",
138
+ children: formatNumber(project.threadCount)
139
+ })]
140
+ }, project.name))
141
+ })]
142
+ }), /* @__PURE__ */ jsxs("section", {
143
+ className: "min-w-0 overflow-hidden rounded-[1.8rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]",
144
+ children: [/* @__PURE__ */ jsx("p", {
145
+ className: "font-semibold text-sm",
146
+ children: "Top projects by tokens"
147
+ }), /* @__PURE__ */ jsx("div", {
148
+ className: "mt-3 space-y-2",
149
+ children: dashboard.topProjectsByTokens.map((project) => /* @__PURE__ */ jsxs(Link, {
150
+ className: "flex items-center justify-between gap-3 rounded-lg border border-[var(--border)] bg-[var(--panel-secondary)] px-3.5 py-2.5 text-sm",
151
+ params: { project: project.name },
152
+ to: "/projects/$project",
153
+ children: [/* @__PURE__ */ jsx("span", {
154
+ className: "truncate",
155
+ children: project.name
156
+ }), /* @__PURE__ */ jsx("span", {
157
+ className: "shrink-0 font-mono text-[var(--muted-foreground)]",
158
+ children: formatTokens(project.totalTokens)
159
+ })]
160
+ }, project.name))
161
+ })]
162
+ })]
163
+ })]
164
+ })
165
+ ]
166
+ });
167
+ }
168
+ //#endregion
169
+ export { DashboardPage as component };