spiracha 1.1.2 → 1.3.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 (159) hide show
  1. package/AGENTS.md +57 -14
  2. package/README.md +122 -65
  3. package/apps/ui/AGENTS.md +18 -8
  4. package/apps/ui/README.md +30 -12
  5. package/apps/ui/dist/client/assets/{analytics-CqWZmyV6.js → analytics-B_hYz65v.js} +1 -1
  6. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-qiyygB7e.js +1 -0
  7. package/apps/ui/dist/client/assets/antigravity-conversations._conversationId-z1SQC2Kg.js +1 -0
  8. package/apps/ui/dist/client/assets/antigravity-keychain-panel-dYuRWtCf.js +1 -0
  9. package/apps/ui/dist/client/assets/antigravity._workspaceKey-CliqUr7o.js +1 -0
  10. package/apps/ui/dist/client/assets/antigravity._workspaceKey-CnoBzyX6.js +1 -0
  11. package/apps/ui/dist/client/assets/antigravity.index-CakfZz_E.js +1 -0
  12. package/apps/ui/dist/client/assets/antigravity.index-DY7M1KhG.js +1 -0
  13. package/apps/ui/dist/client/assets/badge-aHE9ETVe.js +1 -0
  14. package/apps/ui/dist/client/assets/checkbox-DN3XnJaA.js +1 -0
  15. package/apps/ui/dist/client/assets/cursor-threads._composerId-BMQyx8qG.js +1 -0
  16. package/apps/ui/dist/client/assets/cursor-threads._composerId-BTlaA-tV.js +1 -0
  17. package/apps/ui/dist/client/assets/cursor._workspaceKey-CrgrfevV.js +1 -0
  18. package/apps/ui/dist/client/assets/cursor._workspaceKey-bYS2syGL.js +1 -0
  19. package/apps/ui/dist/client/assets/cursor.index-CTqZMPYU.js +1 -0
  20. package/apps/ui/dist/client/assets/cursor.index-Clsz4E_e.js +2 -0
  21. package/apps/ui/dist/client/assets/{data-table-DnPYMPCD.js → data-table-Cj-v-uyB.js} +2 -2
  22. package/apps/ui/dist/client/assets/delete-confirm-dialog-DTpzBiNK.js +11 -0
  23. package/apps/ui/dist/client/assets/dist-BNAn99Pu.js +1 -0
  24. package/apps/ui/dist/client/assets/download-P3Rp23Ad.js +1 -0
  25. package/apps/ui/dist/client/assets/dropdown-menu-3qB5j9nt.js +1 -0
  26. package/apps/ui/dist/client/assets/es2015-Dwm_turD.js +41 -0
  27. package/apps/ui/dist/client/assets/export-dialog-CazdrASq.js +1 -0
  28. package/apps/ui/dist/client/assets/formatters-BdnWuM1z.js +1 -0
  29. package/apps/ui/dist/client/assets/index-BVFnfS78.js +22 -0
  30. package/apps/ui/dist/client/assets/json-panel-DLkS30sQ.js +1 -0
  31. package/apps/ui/dist/client/assets/metadata-section-jnIkB7dB.js +1 -0
  32. package/apps/ui/dist/client/assets/{metric-card-9jwBF7rG.js → metric-card-CBZuWLzQ.js} +1 -1
  33. package/apps/ui/dist/client/assets/page-header-CnD21cPn.js +1 -0
  34. package/apps/ui/dist/client/assets/projects._project-BLszwvYL.js +1 -0
  35. package/apps/ui/dist/client/assets/projects._project-DvLxYbvk.js +1 -0
  36. package/apps/ui/dist/client/assets/projects.index-COn8woBR.js +1 -0
  37. package/apps/ui/dist/client/assets/projects.index-DYs98skV.js +3 -0
  38. package/apps/ui/dist/client/assets/refresh-ccw-BDrYXjtD.js +1 -0
  39. package/apps/ui/dist/client/assets/reload-error-panel-DLAg0AW2.js +1 -0
  40. package/apps/ui/dist/client/assets/routes-BtF5-coe.js +1 -0
  41. package/apps/ui/dist/client/assets/scroll-text-CqaFm9by.js +1 -0
  42. package/apps/ui/dist/client/assets/select-DbnpwqL6.js +1 -0
  43. package/apps/ui/dist/client/assets/settings-CGX3VleN.js +1 -0
  44. package/apps/ui/dist/client/assets/styles-Ch0r3kMZ.css +1 -0
  45. package/apps/ui/dist/client/assets/text-document-panel-DPleOmmq.js +1 -0
  46. package/apps/ui/dist/client/assets/text-filter-7M6wRo-t.js +2 -0
  47. package/apps/ui/dist/client/assets/threads._threadId-D5w76IB-.js +7 -0
  48. package/apps/ui/dist/client/assets/{threads._threadId-DT75NiBa.js → threads._threadId-Dx85sI9P.js} +1 -1
  49. package/apps/ui/dist/client/assets/useMutation-MZ3Hr9h9.js +1 -0
  50. package/apps/ui/dist/client/assets/useQuery-Cb4V0AT0.js +1 -0
  51. package/apps/ui/dist/client/icon.svg +28 -0
  52. package/apps/ui/dist/client/manifest.json +6 -16
  53. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-CBbkUXw6.js +227 -0
  54. package/apps/ui/dist/server/assets/{analytics-BMxW_bZL.js → analytics-CBNOYZwJ.js} +2 -2
  55. package/apps/ui/dist/server/assets/antigravity-conversation-state-HgzS302O.js +16 -0
  56. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-B9Rm4EXh.js +212 -0
  57. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-BIdYNy68.js +20 -0
  58. package/apps/ui/dist/server/assets/antigravity-conversations._conversationId-D426O-64.js +11 -0
  59. package/apps/ui/dist/server/assets/antigravity-db-D9gW1D8G.js +576 -0
  60. package/apps/ui/dist/server/assets/antigravity-keychain-DOiuHDwK.js +126 -0
  61. package/apps/ui/dist/server/assets/antigravity-keychain-panel-DcLyBBwd.js +55 -0
  62. package/apps/ui/dist/server/assets/antigravity-queries-CgQhlQ7J.js +37 -0
  63. package/apps/ui/dist/server/assets/antigravity-server-DFUx4Khk.js +114 -0
  64. package/apps/ui/dist/server/assets/antigravity._workspaceKey-3m_MzNFA.js +11 -0
  65. package/apps/ui/dist/server/assets/antigravity._workspaceKey-D42ixtzp.js +210 -0
  66. package/apps/ui/dist/server/assets/antigravity._workspaceKey-DnSlSC-C.js +28 -0
  67. package/apps/ui/dist/server/assets/antigravity.index-DZVT-cac.js +104 -0
  68. package/apps/ui/dist/server/assets/antigravity.index-DudTB3Tq.js +11 -0
  69. package/apps/ui/dist/server/assets/badge-EvdhKK_Z.js +26 -0
  70. package/apps/ui/dist/server/assets/{codex-queries-CAF6HYiG.js → codex-queries-eOJGfHQj.js} +6 -18
  71. package/apps/ui/dist/server/assets/{codex-server-C01sv0JJ.js → codex-server-nrETIF--.js} +166 -226
  72. package/apps/ui/dist/server/assets/createServerRpc-BtXIw2iP.js +12 -0
  73. package/apps/ui/dist/server/assets/createSsrRpc-COf5Zuye.js +16 -0
  74. package/apps/ui/dist/server/assets/cursor-db-B7agkAvM.js +643 -0
  75. package/apps/ui/dist/server/assets/cursor-exporter-types-CI3goo-c.js +34 -0
  76. package/apps/ui/dist/server/assets/cursor-queries-BMhuJeUO.js +65 -0
  77. package/apps/ui/dist/server/assets/cursor-recovery-9bJLs7vG.js +361 -0
  78. package/apps/ui/dist/server/assets/cursor-server-BgylIFgn.js +184 -0
  79. package/apps/ui/dist/server/assets/cursor-threads._composerId-BB0Y_Mao.js +11 -0
  80. package/apps/ui/dist/server/assets/cursor-threads._composerId-BsxFKzoJ.js +218 -0
  81. package/apps/ui/dist/server/assets/cursor-threads._composerId-DXffY_CK.js +18 -0
  82. package/apps/ui/dist/server/assets/cursor-transcript-2iL3KFSK.js +125 -0
  83. package/apps/ui/dist/server/assets/cursor._workspaceKey-BP2J1x_x.js +28 -0
  84. package/apps/ui/dist/server/assets/cursor._workspaceKey-BQd0e-Pd.js +399 -0
  85. package/apps/ui/dist/server/assets/cursor._workspaceKey-nmg3YIOQ.js +11 -0
  86. package/apps/ui/dist/server/assets/cursor.index-CQVxtCm8.js +189 -0
  87. package/apps/ui/dist/server/assets/cursor.index-CcsX7DG0.js +11 -0
  88. package/apps/ui/dist/server/assets/{delete-confirm-dialog-CWqcTXTF.js → delete-confirm-dialog-PCD7S0_M.js} +5 -4
  89. package/apps/ui/dist/server/assets/download-DMmiy1xf.js +92 -0
  90. package/apps/ui/dist/server/assets/{input-B4tEzctc.js → dropdown-menu-Dy_9t6TN.js} +1 -11
  91. package/apps/ui/dist/server/assets/{download-C5rkk_Bo.js → export-dialog-DaPlOGFT.js} +8 -99
  92. package/apps/ui/dist/server/assets/json-panel-RYsxWFae.js +16 -0
  93. package/apps/ui/dist/server/assets/{loading-panel-DbLdvjtR.js → loading-panel-BGFnWseS.js} +1 -1
  94. package/apps/ui/dist/server/assets/metadata-section-D6Lbc7D6.js +54 -0
  95. package/apps/ui/dist/server/assets/page-header-VNSaM3xd.js +29 -0
  96. package/apps/ui/dist/server/assets/projects._project-Bshqk7JA.js +12 -0
  97. package/apps/ui/dist/server/assets/{projects._project-CJ7l0ynC.js → projects._project-DUN3iWfg.js} +4 -4
  98. package/apps/ui/dist/server/assets/{projects._project-CcJLp_A8.js → projects._project-Dim9Y0kD.js} +54 -26
  99. package/apps/ui/dist/server/assets/projects.index-BLXOx5eL.js +12 -0
  100. package/apps/ui/dist/server/assets/{projects.index-srtogpuF.js → projects.index-DjSQK5dm.js} +23 -27
  101. package/apps/ui/dist/server/assets/{projects.index-CaplpeMy.js → reload-error-panel-BJMxY3U1.js} +5 -6
  102. package/apps/ui/dist/server/assets/{router-C_w-haH6.js → router-DrDgc-LD.js} +131 -44
  103. package/apps/ui/dist/server/assets/{routes-CPe-ppmC.js → routes-B-GlEe2C.js} +54 -39
  104. package/apps/ui/dist/server/assets/{routes-BhbxvJE7.js → routes-CNHAUMwo.js} +2 -2
  105. package/apps/ui/dist/server/assets/{settings-MvWDgc1u.js → settings-OayxIYQQ.js} +1 -1
  106. package/apps/ui/dist/server/assets/shared-CPRNYIql.js +134 -0
  107. package/apps/ui/dist/server/assets/text-document-panel-D8JbQWAn.js +23 -0
  108. package/apps/ui/dist/server/assets/text-filter-CGKxMCKt.js +36 -0
  109. package/apps/ui/dist/server/assets/{threads._threadId-Ba7vv6-K.js → threads._threadId-CJzm4KrZ.js} +3 -3
  110. package/apps/ui/dist/server/assets/{threads._threadId-euyNckhj.js → threads._threadId-DODTYddm.js} +69 -76
  111. package/apps/ui/dist/server/server.js +83 -36
  112. package/bin/codex-chats-claude.js +5 -0
  113. package/bin/codex-chats.js +5 -0
  114. package/bin/spiracha.js +5 -0
  115. package/package.json +26 -13
  116. package/src/export-cursor.ts +244 -0
  117. package/src/lib/antigravity-db.ts +936 -0
  118. package/src/lib/antigravity-exporter-types.ts +70 -0
  119. package/src/lib/antigravity-keychain.ts +203 -0
  120. package/src/lib/codex-browser-db.ts +7 -1
  121. package/src/lib/codex-browser-export.ts +2 -2
  122. package/src/lib/codex-browser-types.ts +22 -1
  123. package/src/lib/codex-exporter-cli.ts +9 -9
  124. package/src/lib/codex-exporter-transcript.ts +16 -190
  125. package/src/lib/codex-exporter-types.ts +1 -1
  126. package/src/lib/codex-exporter.ts +0 -1
  127. package/src/lib/codex-thread-recovery.ts +202 -0
  128. package/src/lib/cursor-db.ts +1096 -0
  129. package/src/lib/cursor-exporter-types.ts +190 -0
  130. package/src/lib/cursor-exporter.ts +266 -0
  131. package/src/lib/cursor-recovery.ts +543 -0
  132. package/src/lib/cursor-transcript.ts +183 -0
  133. package/src/lib/interactive-cli.ts +2 -2
  134. package/src/mcp-server.ts +2 -2
  135. package/src/spiracha.ts +16 -3
  136. package/src/ui-cli.ts +2 -2
  137. package/apps/ui/dist/client/assets/checkbox-DXM4lkJq.js +0 -1
  138. package/apps/ui/dist/client/assets/delete-confirm-dialog-CcZaRX33.js +0 -11
  139. package/apps/ui/dist/client/assets/download-DOwxk-cG.js +0 -1
  140. package/apps/ui/dist/client/assets/es2015-Bm0kEzx2.js +0 -41
  141. package/apps/ui/dist/client/assets/formatters-C12LmYaa.js +0 -1
  142. package/apps/ui/dist/client/assets/index-DdJ7ahIt.js +0 -22
  143. package/apps/ui/dist/client/assets/input-CEsI7EpI.js +0 -1
  144. package/apps/ui/dist/client/assets/page-header-Dr_h1CVv.js +0 -1
  145. package/apps/ui/dist/client/assets/projects._project-uyNGnpjH.js +0 -1
  146. package/apps/ui/dist/client/assets/projects._project-zoM8d2nH.js +0 -1
  147. package/apps/ui/dist/client/assets/projects.index-D1CWVN-O.js +0 -1
  148. package/apps/ui/dist/client/assets/projects.index-DukMuny6.js +0 -1
  149. package/apps/ui/dist/client/assets/routes-Gr2Wwh83.js +0 -1
  150. package/apps/ui/dist/client/assets/select-CFim44gT.js +0 -1
  151. package/apps/ui/dist/client/assets/settings-DqhyDxo2.js +0 -1
  152. package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +0 -1
  153. package/apps/ui/dist/client/assets/threads._threadId-Df5VXIuZ.js +0 -7
  154. package/apps/ui/dist/client/favicon.ico +0 -0
  155. package/apps/ui/dist/client/logo192.png +0 -0
  156. package/apps/ui/dist/client/logo512.png +0 -0
  157. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-C0V305Nt.js +0 -99
  158. package/apps/ui/dist/server/assets/page-header-CxdZM86z.js +0 -25
  159. package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +0 -26
@@ -0,0 +1,92 @@
1
+ //#region src/lib/download.ts
2
+ var DEFAULT_DOWNLOAD_ATTEMPTS = 6;
3
+ var DEFAULT_DOWNLOAD_RETRY_DELAY_MS = 250;
4
+ var DEFAULT_INLINE_REVOKE_DELAY_MS = 3e4;
5
+ var logDownloadEvent = (logger, level, event, details) => {
6
+ logger[level](`[spiracha:download] ${event}`, details);
7
+ };
8
+ var delay = (delayMs) => new Promise((resolve) => {
9
+ window.setTimeout(resolve, delayMs);
10
+ });
11
+ var triggerAnchorDownload = (documentRef, href, fileName) => {
12
+ const link = documentRef.createElement("a");
13
+ link.href = href;
14
+ link.download = fileName;
15
+ documentRef.body.append(link);
16
+ link.click();
17
+ link.remove();
18
+ };
19
+ var isReadyStatus = (status) => {
20
+ return status >= 200 && status < 400 || status === 405;
21
+ };
22
+ var waitForDownloadUrlAvailability = async (downloadUrl, fileName, { fetchImpl = fetch, logger = console, maxAttempts = DEFAULT_DOWNLOAD_ATTEMPTS, retryDelayMs = DEFAULT_DOWNLOAD_RETRY_DELAY_MS, sleep = delay } = {}) => {
23
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
24
+ try {
25
+ const response = await fetchImpl(downloadUrl, {
26
+ cache: "no-store",
27
+ method: "HEAD"
28
+ });
29
+ if (isReadyStatus(response.status)) {
30
+ logDownloadEvent(logger, "info", "url_ready", {
31
+ attempt,
32
+ downloadUrl,
33
+ fileName,
34
+ status: response.status
35
+ });
36
+ return;
37
+ }
38
+ logDownloadEvent(logger, "warn", "url_not_ready", {
39
+ attempt,
40
+ downloadUrl,
41
+ fileName,
42
+ status: response.status
43
+ });
44
+ } catch (error) {
45
+ logDownloadEvent(logger, "warn", "url_probe_failed", {
46
+ attempt,
47
+ downloadUrl,
48
+ error: error instanceof Error ? error.message : String(error),
49
+ fileName
50
+ });
51
+ }
52
+ if (attempt < maxAttempts) await sleep(retryDelayMs);
53
+ }
54
+ throw new Error(`Download file was not available after ${maxAttempts} attempts: ${fileName}`);
55
+ };
56
+ var downloadUrlFile = async (fileName, downloadUrl, { documentRef = document, fetchImpl = fetch, logger = console, maxAttempts = DEFAULT_DOWNLOAD_ATTEMPTS, retryDelayMs = DEFAULT_DOWNLOAD_RETRY_DELAY_MS, sleep = delay } = {}) => {
57
+ logDownloadEvent(logger, "info", "start", {
58
+ downloadUrl,
59
+ fileName
60
+ });
61
+ await waitForDownloadUrlAvailability(downloadUrl, fileName, {
62
+ fetchImpl,
63
+ logger,
64
+ maxAttempts,
65
+ retryDelayMs,
66
+ sleep
67
+ });
68
+ triggerAnchorDownload(documentRef, downloadUrl, fileName);
69
+ logDownloadEvent(logger, "info", "triggered", {
70
+ downloadUrl,
71
+ fileName
72
+ });
73
+ };
74
+ var downloadTextFile = (fileName, content, mimeType, { createObjectUrl = (blob) => URL.createObjectURL(blob), documentRef = document, logger = console, revokeDelayMs = DEFAULT_INLINE_REVOKE_DELAY_MS, revokeObjectUrl = (url) => URL.revokeObjectURL(url), schedule = (callback, delayMs) => {
75
+ window.setTimeout(callback, delayMs);
76
+ } } = {}) => {
77
+ logDownloadEvent(logger, "info", "inline_start", {
78
+ fileName,
79
+ mimeType,
80
+ sizeBytes: content.length
81
+ });
82
+ const url = createObjectUrl(new Blob([content], { type: mimeType }));
83
+ triggerAnchorDownload(documentRef, url, fileName);
84
+ schedule(() => revokeObjectUrl(url), revokeDelayMs);
85
+ logDownloadEvent(logger, "info", "inline_triggered", {
86
+ fileName,
87
+ mimeType,
88
+ sizeBytes: content.length
89
+ });
90
+ };
91
+ //#endregion
92
+ export { downloadUrlFile as n, downloadTextFile as t };
@@ -33,14 +33,4 @@ function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
33
33
  });
34
34
  }
35
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 };
36
+ export { DropdownMenuTrigger as i, DropdownMenuContent as n, DropdownMenuItem as r, DropdownMenu$1 as t };
@@ -83,7 +83,7 @@ function DialogDescription({ className, ...props }) {
83
83
  //#region src/components/export-dialog.tsx
84
84
  function ExportDialog({ open, pending = false, title = "Export thread", onExport, onOpenChange }) {
85
85
  const [outputFormat, setOutputFormat] = useState("md");
86
- const [optimized, setOptimized] = useState(false);
86
+ const [includeMetadata, setIncludeMetadata] = useState(true);
87
87
  const [includeCommentary, setIncludeCommentary] = useState(false);
88
88
  const [includeTools, setIncludeTools] = useState(true);
89
89
  return /* @__PURE__ */ jsx(Dialog$1, {
@@ -124,17 +124,17 @@ function ExportDialog({ open, pending = false, title = "Export thread", onExport
124
124
  /* @__PURE__ */ jsxs("div", {
125
125
  className: "flex items-center gap-3 rounded-2xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3",
126
126
  children: [/* @__PURE__ */ jsx(Checkbox$1, {
127
- "aria-label": "Optimized transcript",
128
- checked: optimized,
129
- onCheckedChange: (checked) => setOptimized(checked === true)
127
+ "aria-label": "Include metadata",
128
+ checked: includeMetadata,
129
+ onCheckedChange: (checked) => setIncludeMetadata(checked === true)
130
130
  }), /* @__PURE__ */ jsxs("span", {
131
131
  className: "space-y-1",
132
132
  children: [/* @__PURE__ */ jsx("span", {
133
133
  className: "block font-medium text-sm",
134
- children: "Optimized transcript"
134
+ children: "Include metadata"
135
135
  }), /* @__PURE__ */ jsx("span", {
136
136
  className: "block text-[var(--muted-foreground)] text-sm",
137
- children: "Removes metadata and condenses the transcript for readability and token efficiency."
137
+ children: "Includes the chat metadata section at the top of the exported transcript."
138
138
  })]
139
139
  })]
140
140
  }),
@@ -184,8 +184,8 @@ function ExportDialog({ open, pending = false, title = "Export thread", onExport
184
184
  disabled: pending,
185
185
  onClick: () => onExport({
186
186
  includeCommentary,
187
+ includeMetadata,
187
188
  includeTools,
188
- optimized,
189
189
  outputFormat
190
190
  }),
191
191
  children: pending ? "Exporting..." : "Download export"
@@ -195,95 +195,4 @@ function ExportDialog({ open, pending = false, title = "Export thread", onExport
195
195
  });
196
196
  }
197
197
  //#endregion
198
- //#region src/lib/download.ts
199
- var DEFAULT_DOWNLOAD_ATTEMPTS = 6;
200
- var DEFAULT_DOWNLOAD_RETRY_DELAY_MS = 250;
201
- var DEFAULT_INLINE_REVOKE_DELAY_MS = 3e4;
202
- var logDownloadEvent = (logger, level, event, details) => {
203
- logger[level](`[spiracha:download] ${event}`, details);
204
- };
205
- var delay = (delayMs) => new Promise((resolve) => {
206
- window.setTimeout(resolve, delayMs);
207
- });
208
- var triggerAnchorDownload = (documentRef, href, fileName) => {
209
- const link = documentRef.createElement("a");
210
- link.href = href;
211
- link.download = fileName;
212
- documentRef.body.append(link);
213
- link.click();
214
- link.remove();
215
- };
216
- var isReadyStatus = (status) => {
217
- return status >= 200 && status < 400 || status === 405;
218
- };
219
- var waitForDownloadUrlAvailability = async (downloadUrl, fileName, { fetchImpl = fetch, logger = console, maxAttempts = DEFAULT_DOWNLOAD_ATTEMPTS, retryDelayMs = DEFAULT_DOWNLOAD_RETRY_DELAY_MS, sleep = delay } = {}) => {
220
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
221
- try {
222
- const response = await fetchImpl(downloadUrl, {
223
- cache: "no-store",
224
- method: "HEAD"
225
- });
226
- if (isReadyStatus(response.status)) {
227
- logDownloadEvent(logger, "info", "url_ready", {
228
- attempt,
229
- downloadUrl,
230
- fileName,
231
- status: response.status
232
- });
233
- return;
234
- }
235
- logDownloadEvent(logger, "warn", "url_not_ready", {
236
- attempt,
237
- downloadUrl,
238
- fileName,
239
- status: response.status
240
- });
241
- } catch (error) {
242
- logDownloadEvent(logger, "warn", "url_probe_failed", {
243
- attempt,
244
- downloadUrl,
245
- error: error instanceof Error ? error.message : String(error),
246
- fileName
247
- });
248
- }
249
- if (attempt < maxAttempts) await sleep(retryDelayMs);
250
- }
251
- throw new Error(`Download file was not available after ${maxAttempts} attempts: ${fileName}`);
252
- };
253
- var downloadUrlFile = async (fileName, downloadUrl, { documentRef = document, fetchImpl = fetch, logger = console, maxAttempts = DEFAULT_DOWNLOAD_ATTEMPTS, retryDelayMs = DEFAULT_DOWNLOAD_RETRY_DELAY_MS, sleep = delay } = {}) => {
254
- logDownloadEvent(logger, "info", "start", {
255
- downloadUrl,
256
- fileName
257
- });
258
- await waitForDownloadUrlAvailability(downloadUrl, fileName, {
259
- fetchImpl,
260
- logger,
261
- maxAttempts,
262
- retryDelayMs,
263
- sleep
264
- });
265
- triggerAnchorDownload(documentRef, downloadUrl, fileName);
266
- logDownloadEvent(logger, "info", "triggered", {
267
- downloadUrl,
268
- fileName
269
- });
270
- };
271
- var downloadTextFile = (fileName, content, mimeType, { createObjectUrl = (blob) => URL.createObjectURL(blob), documentRef = document, logger = console, revokeDelayMs = DEFAULT_INLINE_REVOKE_DELAY_MS, revokeObjectUrl = (url) => URL.revokeObjectURL(url), schedule = (callback, delayMs) => {
272
- window.setTimeout(callback, delayMs);
273
- } } = {}) => {
274
- logDownloadEvent(logger, "info", "inline_start", {
275
- fileName,
276
- mimeType,
277
- sizeBytes: content.length
278
- });
279
- const url = createObjectUrl(new Blob([content], { type: mimeType }));
280
- triggerAnchorDownload(documentRef, url, fileName);
281
- schedule(() => revokeObjectUrl(url), revokeDelayMs);
282
- logDownloadEvent(logger, "info", "inline_triggered", {
283
- fileName,
284
- mimeType,
285
- sizeBytes: content.length
286
- });
287
- };
288
- //#endregion
289
- export { downloadUrlFile as n, ExportDialog as r, downloadTextFile as t };
198
+ export { ExportDialog as t };
@@ -0,0 +1,16 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ //#region src/components/json-panel.tsx
3
+ function JsonPanel({ title, value }) {
4
+ return /* @__PURE__ */ jsxs("section", {
5
+ className: "rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]",
6
+ children: [/* @__PURE__ */ jsx("h3", {
7
+ className: "font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]",
8
+ children: title
9
+ }), /* @__PURE__ */ jsx("pre", {
10
+ className: "mt-4 overflow-x-auto rounded-2xl border border-[var(--border)] bg-[var(--code-background)] p-4 text-[var(--code-foreground)] text-xs leading-5",
11
+ children: JSON.stringify(value, null, 2)
12
+ })]
13
+ });
14
+ }
15
+ //#endregion
16
+ export { JsonPanel as t };
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Loader2 } from "lucide-react";
3
3
  //#region src/components/loading-panel.tsx
4
- function LoadingPanel({ description = "Fetching local Codex data. Larger projects can take a moment.", title = "Loading" }) {
4
+ function LoadingPanel({ description = "Fetching local data. Larger workspaces can take a moment.", title = "Loading" }) {
5
5
  return /* @__PURE__ */ jsxs("div", {
6
6
  className: "rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] px-6 py-10 text-center shadow-[var(--panel-shadow)]",
7
7
  children: [
@@ -0,0 +1,54 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { ChevronRight } from "lucide-react";
4
+ //#region src/components/breadcrumbs.tsx
5
+ var isLinkItem = (item) => {
6
+ return "to" in item;
7
+ };
8
+ var Breadcrumbs = ({ items }) => {
9
+ return /* @__PURE__ */ jsx("nav", {
10
+ "aria-label": "Breadcrumb",
11
+ className: "flex flex-wrap items-center gap-1 text-sm",
12
+ children: items.map((item, index) => {
13
+ const content = isLinkItem(item) ? /* @__PURE__ */ jsx(Link, {
14
+ className: "text-[var(--muted-foreground)] transition hover:text-[var(--foreground)]",
15
+ params: item.params,
16
+ to: item.to,
17
+ children: item.label
18
+ }) : /* @__PURE__ */ jsx("span", {
19
+ "aria-current": "page",
20
+ className: "font-medium text-[var(--foreground)]",
21
+ children: item.label
22
+ });
23
+ return /* @__PURE__ */ jsxs("div", {
24
+ className: "flex items-center gap-1",
25
+ children: [index > 0 ? /* @__PURE__ */ jsx(ChevronRight, { className: "size-3.5 text-[var(--muted-foreground)]" }) : null, content]
26
+ }, isLinkItem(item) ? `${item.label}-${item.to}-${index}` : `${item.label}-current-${index}`);
27
+ })
28
+ });
29
+ };
30
+ //#endregion
31
+ //#region src/components/metadata-section.tsx
32
+ function MetadataSection({ items, title }) {
33
+ return /* @__PURE__ */ jsxs("section", {
34
+ className: "rounded-[1.6rem] border border-[var(--border)] bg-[var(--panel)] p-5 shadow-[var(--panel-shadow)]",
35
+ children: [/* @__PURE__ */ jsx("h3", {
36
+ className: "font-semibold text-[var(--muted-foreground)] text-sm uppercase tracking-[0.18em]",
37
+ children: title
38
+ }), /* @__PURE__ */ jsx("dl", {
39
+ className: "mt-4 space-y-3",
40
+ children: items.map((item) => /* @__PURE__ */ jsxs("div", {
41
+ className: "grid gap-1 sm:grid-cols-[11rem_1fr] sm:items-start",
42
+ children: [/* @__PURE__ */ jsx("dt", {
43
+ className: "font-medium text-[var(--muted-foreground)] text-xs uppercase tracking-[0.14em]",
44
+ children: item.label
45
+ }), /* @__PURE__ */ jsx("dd", {
46
+ className: "min-w-0 text-sm leading-6",
47
+ children: item.value
48
+ })]
49
+ }, item.label))
50
+ })]
51
+ });
52
+ }
53
+ //#endregion
54
+ export { Breadcrumbs as n, MetadataSection as t };
@@ -0,0 +1,29 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ //#region src/components/page-header.tsx
3
+ function PageHeader({ actions, breadcrumb, 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: [
9
+ breadcrumb ? /* @__PURE__ */ jsx("div", { children: breadcrumb }) : null,
10
+ eyebrow ? /* @__PURE__ */ jsx("p", {
11
+ className: "font-semibold text-[11px] text-[var(--muted-foreground)] uppercase tracking-[0.18em]",
12
+ children: eyebrow
13
+ }) : null,
14
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
15
+ className: "font-semibold text-2xl tracking-[-0.03em] sm:text-[2rem]",
16
+ children: title
17
+ }), subtitle ? /* @__PURE__ */ jsx("p", {
18
+ className: "mt-2 max-w-[60rem] whitespace-pre-wrap break-words text-[var(--muted-foreground)] text-sm",
19
+ children: subtitle
20
+ }) : null] })
21
+ ]
22
+ }), actions ? /* @__PURE__ */ jsx("div", {
23
+ className: "flex flex-wrap items-center gap-2",
24
+ children: actions
25
+ }) : null]
26
+ });
27
+ }
28
+ //#endregion
29
+ export { PageHeader as t };
@@ -0,0 +1,12 @@
1
+ import { t as ReloadErrorPanel } from "./reload-error-panel-BJMxY3U1.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/routes/projects.$project.tsx?tsr-split=errorComponent
4
+ function ProjectDetailErrorComponent({ error }) {
5
+ const isSqlite = error.message.includes("unable to open database") || error.message.includes("database is locked");
6
+ return /* @__PURE__ */ jsx(ReloadErrorPanel, {
7
+ description: isSqlite ? "Codex may have an exclusive lock on the database. Reload to retry." : error.message,
8
+ title: isSqlite ? "Database unavailable" : "Failed to load Codex project"
9
+ });
10
+ }
11
+ //#endregion
12
+ export { ProjectDetailErrorComponent as errorComponent };
@@ -1,10 +1,10 @@
1
- import { r as projectThreadsQueryOptions } from "./codex-queries-CAF6HYiG.js";
2
- import { t as LoadingPanel } from "./loading-panel-DbLdvjtR.js";
1
+ import { r as projectThreadsQueryOptions } from "./codex-queries-eOJGfHQj.js";
2
+ import { t as LoadingPanel } from "./loading-panel-BGFnWseS.js";
3
3
  import { createFileRoute, lazyRouteComponent } from "@tanstack/react-router";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  //#region src/routes/projects.$project.tsx
6
- var $$splitErrorComponentImporter = () => import("./projects._project-CLSohrBp.js");
7
- var $$splitComponentImporter = () => import("./projects._project-CcJLp_A8.js");
6
+ var $$splitErrorComponentImporter = () => import("./projects._project-Bshqk7JA.js");
7
+ var $$splitComponentImporter = () => import("./projects._project-Dim9Y0kD.js");
8
8
  var Route = createFileRoute("/projects/$project")({
9
9
  component: lazyRouteComponent($$splitComponentImporter, "component"),
10
10
  errorComponent: lazyRouteComponent($$splitErrorComponentImporter, "errorComponent"),
@@ -1,18 +1,20 @@
1
1
  import { t as Button } from "./button-CmTDnzOn.js";
2
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";
3
+ import { c as deleteThreadFn, d as exportThreadsFn, f as recoverProjectThreadsFn, l as deleteThreadsFn, r as projectThreadsQueryOptions, u as exportThreadFn } from "./codex-queries-eOJGfHQj.js";
4
+ import { t as Route } from "./projects._project-DUN3iWfg.js";
5
5
  import { t as DataTable } from "./data-table-Cdct823O.js";
6
- import { t as PageHeader } from "./page-header-CxdZM86z.js";
6
+ import { t as PageHeader } from "./page-header-VNSaM3xd.js";
7
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";
8
+ import { n as downloadUrlFile, t as downloadTextFile } from "./download-DMmiy1xf.js";
9
+ import { i as DropdownMenuTrigger, n as DropdownMenuContent, r as DropdownMenuItem, t as DropdownMenu } from "./dropdown-menu-Dy_9t6TN.js";
10
+ import { n as ListSearchInput, t as matchesTextQuery } from "./text-filter-CGKxMCKt.js";
11
+ import { t as DeleteConfirmDialog } from "./delete-confirm-dialog-PCD7S0_M.js";
12
+ import { t as ExportDialog } from "./export-dialog-DaPlOGFT.js";
13
+ import { useDeferredValue, useMemo, useState } from "react";
12
14
  import { Link } from "@tanstack/react-router";
13
15
  import { jsx, jsxs } from "react/jsx-runtime";
14
16
  import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
15
- import { Download, MoreHorizontal, Trash2, X } from "lucide-react";
17
+ import { Download, MoreHorizontal, RefreshCcw, Trash2, X } from "lucide-react";
16
18
  import { createColumnHelper } from "@tanstack/react-table";
17
19
  //#region src/components/threads-table.tsx
18
20
  var columnHelper = createColumnHelper();
@@ -211,6 +213,17 @@ function ProjectDetailPage() {
211
213
  setPendingDelete(null);
212
214
  }
213
215
  });
216
+ const recoverProjectMutation = useMutation({
217
+ mutationFn: () => recoverProjectThreadsFn({ data: { project: params.project } }),
218
+ onSuccess: async () => {
219
+ await Promise.all([
220
+ queryClient.invalidateQueries({ queryKey: ["analytics"] }),
221
+ queryClient.invalidateQueries({ queryKey: ["dashboard"] }),
222
+ queryClient.invalidateQueries({ queryKey: ["project-threads", params.project] }),
223
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
224
+ ]);
225
+ }
226
+ });
214
227
  const exportThreadMutation = useMutation({
215
228
  mutationFn: async (options) => {
216
229
  if (!pendingExport) throw new Error("No thread selected for export");
@@ -255,34 +268,48 @@ function ProjectDetailPage() {
255
268
  }
256
269
  });
257
270
  const visibleThreads = [...threads].filter((thread) => {
258
- if (!deferredSearch) return true;
259
- return `${thread.thread.title}\n${thread.thread.preview}`.toLowerCase().includes(deferredSearch);
271
+ return matchesTextQuery(deferredSearch, [
272
+ thread.thread.title,
273
+ thread.thread.preview,
274
+ thread.thread.model,
275
+ thread.thread.id
276
+ ]);
260
277
  });
278
+ const visibleThreadsById = useMemo(() => new Map(visibleThreads.map((thread) => [thread.thread.id, thread])), [visibleThreads]);
261
279
  const lookupSelectedThreads = (threadIds) => {
262
- const threadIdSet = new Set(threadIds);
263
- return visibleThreads.filter((thread) => threadIdSet.has(thread.thread.id));
280
+ return threadIds.map((threadId) => visibleThreadsById.get(threadId) ?? null).filter((thread) => thread !== null);
264
281
  };
265
282
  return /* @__PURE__ */ jsxs("div", {
266
283
  className: "space-y-6",
267
284
  children: [
268
285
  /* @__PURE__ */ jsx(PageHeader, {
269
- actions: /* @__PURE__ */ jsx("div", {
286
+ actions: /* @__PURE__ */ jsxs("div", {
270
287
  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",
288
+ children: [/* @__PURE__ */ jsxs(Button, {
289
+ className: "rounded-full",
290
+ disabled: recoverProjectMutation.isPending,
291
+ type: "button",
292
+ variant: "outline",
293
+ onClick: () => recoverProjectMutation.mutate(),
294
+ children: [/* @__PURE__ */ jsx(RefreshCcw, { className: "mr-2 size-4" }), recoverProjectMutation.isPending ? "Recovering..." : "Recover"]
295
+ }), /* @__PURE__ */ jsx(ListSearchInput, {
296
+ placeholder: "Search thread title, preview, or model",
274
297
  value: searchInput,
275
- onChange: (event) => {
276
- startTransition(() => {
277
- setSearchInput(event.target.value);
278
- });
279
- }
280
- })
298
+ onValueChange: setSearchInput
299
+ })]
281
300
  }),
282
- eyebrow: "Project",
283
- subtitle: "Sort by any column, inspect tool call volume, and manage thread records for this derived project.",
301
+ eyebrow: "Codex project",
302
+ subtitle: "Sort by any column, inspect tool call volume, manage thread records, or repair stale Codex thread metadata for this derived project.",
284
303
  title: params.project
285
304
  }),
305
+ recoverProjectMutation.isError ? /* @__PURE__ */ jsx("p", {
306
+ className: "text-[var(--destructive)] text-sm",
307
+ children: recoverProjectMutation.error instanceof Error ? recoverProjectMutation.error.message : "Project recovery failed"
308
+ }) : null,
309
+ recoverProjectMutation.isSuccess ? /* @__PURE__ */ jsx("p", {
310
+ className: "text-[var(--success)] text-sm",
311
+ children: "Project thread metadata recovery completed."
312
+ }) : null,
286
313
  /* @__PURE__ */ jsx(ThreadsTable, {
287
314
  threads: visibleThreads,
288
315
  onDeleteThread: (thread) => setPendingDelete({ threads: [thread] }),
@@ -305,11 +332,12 @@ function ProjectDetailPage() {
305
332
  }
306
333
  }),
307
334
  /* @__PURE__ */ jsx(DeleteConfirmDialog, {
308
- confirmLabel: deleteThreadMutation.isPending ? "Deleting..." : "Delete thread",
335
+ confirmLabel: deleteThreadMutation.isPending ? "Deleting..." : pendingDelete && pendingDelete.threads.length > 1 ? "Delete threads" : "Delete thread",
336
+ defaultDeleteSessionFiles: true,
309
337
  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
338
  open: pendingDelete !== null,
311
339
  showDeleteSessionFilesOption: true,
312
- title: "Delete thread from Codex DB?",
340
+ title: pendingDelete && pendingDelete.threads.length > 1 ? `Delete ${pendingDelete.threads.length} Codex threads?` : "Delete Codex thread?",
313
341
  onConfirm: ({ deleteSessionFiles }) => {
314
342
  if (!pendingDelete) return;
315
343
  deleteThreadMutation.mutate({
@@ -0,0 +1,12 @@
1
+ import { t as ReloadErrorPanel } from "./reload-error-panel-BJMxY3U1.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/routes/projects.index.tsx?tsr-split=errorComponent
4
+ function ProjectsErrorComponent({ error }) {
5
+ const isSqlite = error.message.includes("unable to open database") || error.message.includes("database is locked");
6
+ return /* @__PURE__ */ jsx(ReloadErrorPanel, {
7
+ description: isSqlite ? "Codex may have an exclusive lock on the database. Reload to retry." : error.message,
8
+ title: isSqlite ? "Database unavailable" : "Failed to load Codex inventory"
9
+ });
10
+ }
11
+ //#endregion
12
+ export { ProjectsErrorComponent as errorComponent };
@@ -1,12 +1,13 @@
1
1
  import { t as Button } from "./button-CmTDnzOn.js";
2
- import { i as projectsQueryOptions, s as deleteProjectFn } from "./codex-queries-CAF6HYiG.js";
2
+ import { i as projectsQueryOptions, s as deleteProjectFn } from "./codex-queries-eOJGfHQj.js";
3
3
  import { t as DataTable } from "./data-table-Cdct823O.js";
4
- import { t as PageHeader } from "./page-header-CxdZM86z.js";
4
+ import { t as PageHeader } from "./page-header-VNSaM3xd.js";
5
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";
6
+ import { i as DropdownMenuTrigger, n as DropdownMenuContent, r as DropdownMenuItem, t as DropdownMenu } from "./dropdown-menu-Dy_9t6TN.js";
7
+ import { n as ListSearchInput, t as matchesTextQuery } from "./text-filter-CGKxMCKt.js";
8
+ import { t as DeleteConfirmDialog } from "./delete-confirm-dialog-PCD7S0_M.js";
9
+ import { useDeferredValue, useState } from "react";
10
+ import { Link } from "@tanstack/react-router";
10
11
  import { jsx, jsxs } from "react/jsx-runtime";
11
12
  import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
12
13
  import { MoreHorizontal, Trash2 } from "lucide-react";
@@ -15,10 +16,12 @@ import { createColumnHelper } from "@tanstack/react-table";
15
16
  var columnHelper = createColumnHelper();
16
17
  var columns = (onDeleteProject) => [
17
18
  columnHelper.accessor("name", {
18
- cell: (info) => /* @__PURE__ */ jsxs("div", {
19
- className: "space-y-1",
19
+ cell: (info) => /* @__PURE__ */ jsxs(Link, {
20
+ className: "block w-[14rem] max-w-[18rem] space-y-1 rounded-md outline-none transition hover:opacity-80 focus-visible:ring-2 focus-visible:ring-[var(--accent)] lg:w-auto",
21
+ params: { project: info.row.original.name },
22
+ to: "/projects/$project",
20
23
  children: [/* @__PURE__ */ jsx("p", {
21
- className: "font-medium",
24
+ className: "font-medium underline-offset-2 hover:underline",
22
25
  children: info.getValue()
23
26
  }), /* @__PURE__ */ jsxs("p", {
24
27
  className: "text-[var(--muted-foreground)] text-xs",
@@ -92,15 +95,10 @@ var columns = (onDeleteProject) => [
92
95
  })
93
96
  ];
94
97
  function ProjectsTable({ projects, onDeleteProject }) {
95
- const navigate = useNavigate();
96
98
  return /* @__PURE__ */ jsx(DataTable, {
97
99
  columns: columns(onDeleteProject),
98
100
  data: projects,
99
- emptyMessage: "No projects match the current search.",
100
- onRowClick: (project) => navigate({
101
- params: { project: project.name },
102
- to: "/projects/$project"
103
- })
101
+ emptyMessage: "No projects match the current search."
104
102
  });
105
103
  }
106
104
  //#endregion
@@ -123,26 +121,24 @@ function ProjectsPage() {
123
121
  }
124
122
  });
125
123
  const visibleProjects = projects.filter((project) => {
126
- if (!deferredSearch) return true;
127
- return project.name.toLowerCase().includes(deferredSearch);
124
+ return matchesTextQuery(deferredSearch, [
125
+ project.name,
126
+ project.cwdPaths.join("\n"),
127
+ project.modelNames.join("\n")
128
+ ]);
128
129
  });
129
130
  return /* @__PURE__ */ jsxs("div", {
130
131
  className: "space-y-6",
131
132
  children: [
132
133
  /* @__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",
134
+ actions: /* @__PURE__ */ jsx(ListSearchInput, {
135
+ placeholder: "Search project name, cwd, or model",
136
136
  value: searchInput,
137
- onChange: (event) => {
138
- startTransition(() => {
139
- setSearchInput(event.target.value);
140
- });
141
- }
137
+ onValueChange: setSearchInput
142
138
  }),
143
139
  eyebrow: "Inventory",
144
140
  subtitle: "Derived projects are grouped from the final basename of each thread cwd, matching the existing CLI behavior.",
145
- title: "Projects"
141
+ title: "Codex"
146
142
  }),
147
143
  /* @__PURE__ */ jsx(ProjectsTable, {
148
144
  projects: visibleProjects,
@@ -153,7 +149,7 @@ function ProjectsPage() {
153
149
  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
150
  open: pendingDelete !== null,
155
151
  showDeleteSessionFilesOption: true,
156
- title: "Delete project from Codex DB?",
152
+ title: "Delete Codex project?",
157
153
  onConfirm: ({ deleteSessionFiles }) => {
158
154
  if (!pendingDelete) return;
159
155
  deleteProjectMutation.mutate({