spiracha 1.1.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 (59) hide show
  1. package/AGENTS.md +3 -0
  2. package/README.md +14 -0
  3. package/apps/ui/dist/client/assets/{analytics-BjYaHqXk.js → analytics-CqWZmyV6.js} +1 -1
  4. package/apps/ui/dist/client/assets/{checkbox-wPoGG3of.js → checkbox-DXM4lkJq.js} +1 -1
  5. package/apps/ui/dist/client/assets/{data-table-6yDgAdtf.js → data-table-DnPYMPCD.js} +1 -1
  6. package/apps/ui/dist/client/assets/{delete-confirm-dialog-DJUAk7ha.js → delete-confirm-dialog-CcZaRX33.js} +1 -1
  7. package/apps/ui/dist/client/assets/{download-BhWd-Pm5.js → download-DOwxk-cG.js} +1 -1
  8. package/apps/ui/dist/client/assets/{es2015-BlyMI4CF.js → es2015-Bm0kEzx2.js} +1 -1
  9. package/apps/ui/dist/client/assets/{formatters-BxjZwWSE.js → formatters-C12LmYaa.js} +1 -1
  10. package/apps/ui/dist/client/assets/{index-T01rPkb4.js → index-DdJ7ahIt.js} +3 -3
  11. package/apps/ui/dist/client/assets/{input-B3YN8gzg.js → input-CEsI7EpI.js} +1 -1
  12. package/apps/ui/dist/client/assets/{metric-card-BWW7TWER.js → metric-card-9jwBF7rG.js} +1 -1
  13. package/apps/ui/dist/client/assets/{page-header-BZ8Gnxgs.js → page-header-Dr_h1CVv.js} +1 -1
  14. package/apps/ui/dist/client/assets/projects._project-uyNGnpjH.js +1 -0
  15. package/apps/ui/dist/client/assets/{projects._project-EfBhCHPY.js → projects._project-zoM8d2nH.js} +1 -1
  16. package/apps/ui/dist/client/assets/projects.index-D1CWVN-O.js +1 -0
  17. package/apps/ui/dist/client/assets/{projects.index-DzEZ4pAJ.js → projects.index-DukMuny6.js} +1 -1
  18. package/apps/ui/dist/client/assets/{routes-CWCCZykE.js → routes-Gr2Wwh83.js} +1 -1
  19. package/apps/ui/dist/client/assets/{select-DLXGsyZ4.js → select-CFim44gT.js} +1 -1
  20. package/apps/ui/dist/client/assets/{settings-b0Xthfae.js → settings-DqhyDxo2.js} +1 -1
  21. package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +1 -0
  22. package/apps/ui/dist/client/assets/{threads._threadId-CgtoCqTb.js → threads._threadId-DT75NiBa.js} +1 -1
  23. package/apps/ui/dist/client/assets/{threads._threadId-DBiDb38K.js → threads._threadId-Df5VXIuZ.js} +3 -3
  24. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-C0V305Nt.js +99 -0
  25. package/apps/ui/dist/server/assets/{analytics-Br_fZB6a.js → analytics-BMxW_bZL.js} +1 -1
  26. package/apps/ui/dist/server/assets/{codex-server-Cqh0hb93.js → codex-server-BFZq2Y2O.js} +129 -62
  27. package/apps/ui/dist/server/assets/{download-CzHmFWGk.js → download-C5rkk_Bo.js} +3 -0
  28. package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +91 -0
  29. package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +13 -0
  30. package/apps/ui/dist/server/assets/{path-transforms-DD1e7rhY.js → path-transforms-DL2IwtYd.js} +1 -1
  31. package/apps/ui/dist/server/assets/{projects._project-DdVSdfPe.js → projects._project-CJ7l0ynC.js} +1 -1
  32. package/apps/ui/dist/server/assets/{projects._project-Bwf6iJC-.js → projects._project-CcJLp_A8.js} +5 -3
  33. package/apps/ui/dist/server/assets/{projects.index-DKeVeqUZ.js → projects.index-srtogpuF.js} +2 -1
  34. package/apps/ui/dist/server/assets/{router-ve2Hrl2Y.js → router-C_w-haH6.js} +6 -6
  35. package/apps/ui/dist/server/assets/{routes-BJyx5OmO.js → routes-BhbxvJE7.js} +1 -1
  36. package/apps/ui/dist/server/assets/{routes-pkOwjjYc.js → routes-CPe-ppmC.js} +3 -2
  37. package/apps/ui/dist/server/assets/{start-BAvbjjfs.js → start-HeKLHD9b.js} +1 -1
  38. package/apps/ui/dist/server/assets/{threads._threadId-D3PYZIwl.js → threads._threadId-Ba7vv6-K.js} +1 -1
  39. package/apps/ui/dist/server/assets/{threads._threadId-D3xaWM86.js → threads._threadId-euyNckhj.js} +31 -9
  40. package/apps/ui/dist/server/server.js +15 -15
  41. package/package.json +7 -1
  42. package/src/export-chats.ts +3 -4
  43. package/src/lib/claude-exporter.ts +1 -1
  44. package/src/lib/codex-browser-db.ts +146 -59
  45. package/src/lib/codex-browser-export.ts +13 -2
  46. package/src/lib/codex-exporter-cli.ts +1 -1
  47. package/src/lib/codex-exporter-db.ts +19 -20
  48. package/src/lib/codex-exporter-transcript.ts +38 -25
  49. package/src/lib/interactive-cli.ts +6 -13
  50. package/src/lib/model-label.ts +24 -0
  51. package/src/lib/path-transforms.ts +1 -0
  52. package/src/lib/shared.ts +2 -24
  53. package/src/lib/sqlite-retry.ts +15 -1
  54. package/src/spiracha.ts +3 -4
  55. package/apps/ui/dist/client/assets/projects._project-B7XcpoLt.js +0 -1
  56. package/apps/ui/dist/client/assets/projects.index-4vfIwLjw.js +0 -1
  57. package/apps/ui/dist/client/assets/styles-8Wtc8YJw.css +0 -1
  58. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-BjsXNYgm.js +0 -99
  59. package/apps/ui/dist/server/assets/formatters-B6o5pTY9.js +0 -72
@@ -1,6 +1,7 @@
1
1
  import { n as TSS_SERVER_FUNCTION, t as createServerFn } from "../server.js";
2
+ import { t as formatModelLabel$1 } from "./model-label-B1NWGc65.js";
2
3
  import { t as isRetryableSqliteError } from "./sqlite-error-LZDrnxdd.js";
3
- import { t as applyPathTransforms } from "./path-transforms-DD1e7rhY.js";
4
+ import { t as applyPathTransforms } from "./path-transforms-DL2IwtYd.js";
4
5
  import { finished } from "node:stream/promises";
5
6
  import { Database } from "bun:sqlite";
6
7
  import { mkdir, mkdtemp, readdir, rename, rm, stat } from "node:fs/promises";
@@ -40,16 +41,7 @@ var cleanInlineTitle = (value) => {
40
41
  var cleanExtractedText = (text) => {
41
42
  return text.replace(/^\s*<\/?image>\s*$/gm, "").replace(/\n{3,}/g, "\n\n");
42
43
  };
43
- var formatModelLabel = (value) => {
44
- if (!value) return "Assistant";
45
- return value.split(/[-_\s]+/u).filter(Boolean).map((part) => {
46
- const lower = part.toLowerCase();
47
- if (lower === "gpt") return "GPT";
48
- if (/^[a-z]\d$/u.test(lower)) return lower.toUpperCase();
49
- if (/^\d+(\.\d+)*$/u.test(part)) return part;
50
- return `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`;
51
- }).join(" ");
52
- };
44
+ var formatModelLabel = formatModelLabel$1;
53
45
  var asObject = (value) => {
54
46
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
55
47
  return value;
@@ -598,18 +590,74 @@ var sleepSync = (delayMs) => {
598
590
  if (delayMs <= 0) return;
599
591
  Atomics.wait(SLEEP_BUFFER, 0, 0, delayMs);
600
592
  };
593
+ var toRetryExhaustedError = (attemptCount, error) => {
594
+ const message = error instanceof Error ? error.message : String(error);
595
+ return new Error(`SQLite operation failed after ${attemptCount} attempts: ${message}`, { cause: error });
596
+ };
597
+ var shouldRetrySqliteError = (error, attempt, delaysMs) => {
598
+ return isRetryableSqliteError(error) && attempt < delaysMs.length;
599
+ };
601
600
  var runWithSqliteRetry = ({ action, delaysMs = DEFAULT_RETRY_DELAYS_MS, sleep = sleepSync }) => {
602
601
  let attempt = 0;
603
602
  while (true) try {
604
603
  return action();
605
604
  } catch (error) {
606
- if (!isRetryableSqliteError(error) || attempt >= delaysMs.length) throw error;
605
+ if (!shouldRetrySqliteError(error, attempt, delaysMs)) {
606
+ if (isRetryableSqliteError(error)) throw toRetryExhaustedError(attempt + 1, error);
607
+ throw error;
608
+ }
607
609
  sleep(delaysMs[attempt] ?? 0);
608
610
  attempt += 1;
609
611
  }
610
612
  };
611
613
  //#endregion
612
614
  //#region ../../src/lib/codex-browser-db.ts
615
+ var SQLITE_DELETE_BATCH_SIZE = 400;
616
+ var SESSION_FILE_DELETE_CONCURRENCY = 16;
617
+ var THREAD_LIST_IO_CONCURRENCY = 8;
618
+ var chunkValues = (values, chunkSize) => {
619
+ const chunks = [];
620
+ for (let index = 0; index < values.length; index += chunkSize) chunks.push(values.slice(index, index + chunkSize));
621
+ return chunks;
622
+ };
623
+ var isPromiseLike = (value) => {
624
+ if (typeof value !== "object" && typeof value !== "function" || value === null) return false;
625
+ return "then" in value && typeof value.then === "function";
626
+ };
627
+ var mapWithConcurrency = async (values, limit, mapper) => {
628
+ const results = new Array(values.length);
629
+ let nextIndex = 0;
630
+ const worker = async () => {
631
+ while (true) {
632
+ const currentIndex = nextIndex;
633
+ nextIndex += 1;
634
+ if (currentIndex >= values.length) return;
635
+ results[currentIndex] = await mapper(values[currentIndex], currentIndex);
636
+ }
637
+ };
638
+ await Promise.all(Array.from({ length: Math.min(limit, values.length) }, () => worker()));
639
+ return results;
640
+ };
641
+ var openReadonlyDb = (dbPath, busyTimeoutMs) => {
642
+ const db = new Database(dbPath, { readonly: true });
643
+ try {
644
+ db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
645
+ return db;
646
+ } catch (error) {
647
+ db.close();
648
+ throw error;
649
+ }
650
+ };
651
+ var openWritableDb = (dbPath, busyTimeoutMs) => {
652
+ const db = new Database(dbPath);
653
+ try {
654
+ db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
655
+ return db;
656
+ } catch (error) {
657
+ db.close();
658
+ throw error;
659
+ }
660
+ };
613
661
  var toTimestampMs = (thread) => {
614
662
  return thread.updated_at_ms ?? thread.updated_at * 1e3;
615
663
  };
@@ -634,10 +682,11 @@ var parseJsonSafely$1 = (value) => {
634
682
  };
635
683
  var withReadonlyDb = (dbPath, callback) => {
636
684
  return runWithSqliteRetry({ action: () => {
637
- const db = new Database(dbPath, { readonly: true });
638
- db.exec("PRAGMA busy_timeout = 5000");
685
+ const db = openReadonlyDb(dbPath, 5e3);
639
686
  try {
640
- return callback(db);
687
+ const result = callback(db);
688
+ if (isPromiseLike(result)) throw new Error("Database callbacks must be synchronous");
689
+ return result;
641
690
  } finally {
642
691
  db.close();
643
692
  }
@@ -645,12 +694,12 @@ var withReadonlyDb = (dbPath, callback) => {
645
694
  };
646
695
  var withWritableDb = (dbPath, callback) => {
647
696
  const db = runWithSqliteRetry({ action: () => {
648
- const connection = new Database(dbPath);
649
- connection.exec("PRAGMA busy_timeout = 5000");
650
- return connection;
697
+ return openWritableDb(dbPath, 5e3);
651
698
  } });
652
699
  try {
653
- return callback(db);
700
+ const result = callback(db);
701
+ if (isPromiseLike(result)) throw new Error("Database callbacks must be synchronous");
702
+ return result;
654
703
  } finally {
655
704
  db.close();
656
705
  }
@@ -665,9 +714,7 @@ var resolveCodexThreadDbPath = () => {
665
714
  ];
666
715
  for (const candidate of candidates) try {
667
716
  runWithSqliteRetry({ action: () => {
668
- const connection = new Database(candidate, { readonly: true });
669
- connection.exec("PRAGMA busy_timeout = 1500");
670
- return connection;
717
+ return openReadonlyDb(candidate, 1500);
671
718
  } }).close();
672
719
  return candidate;
673
720
  } catch {}
@@ -739,8 +786,12 @@ var getExistingTableNames = (db) => {
739
786
  };
740
787
  var getThreadDeleteTargets = (db, threadIds) => {
741
788
  if (threadIds.length === 0) return [];
742
- const placeholders = threadIds.map(() => "?").join(", ");
743
- return db.query(`SELECT id, rollout_path FROM threads WHERE id IN (${placeholders})`).all(...threadIds);
789
+ const targets = [];
790
+ for (const threadIdChunk of chunkValues(threadIds, SQLITE_DELETE_BATCH_SIZE)) {
791
+ const placeholders = threadIdChunk.map(() => "?").join(", ");
792
+ targets.push(...db.query(`SELECT id, rollout_path FROM threads WHERE id IN (${placeholders})`).all(...threadIdChunk));
793
+ }
794
+ return targets;
744
795
  };
745
796
  var deleteThreadIds = (db, threadIds) => {
746
797
  if (threadIds.length === 0) return {
@@ -755,12 +806,14 @@ var deleteThreadIds = (db, threadIds) => {
755
806
  deletedThreadIds: []
756
807
  };
757
808
  db.transaction((ids) => {
758
- const placeholders = ids.map(() => "?").join(", ");
759
- if (existingTableNames.has("thread_dynamic_tools")) db.query(`DELETE FROM thread_dynamic_tools WHERE thread_id IN (${placeholders})`).run(...ids);
760
- if (existingTableNames.has("thread_goals")) db.query(`DELETE FROM thread_goals WHERE thread_id IN (${placeholders})`).run(...ids);
761
- if (existingTableNames.has("stage1_outputs")) db.query(`DELETE FROM stage1_outputs WHERE thread_id IN (${placeholders})`).run(...ids);
762
- if (existingTableNames.has("thread_spawn_edges")) db.query(`DELETE FROM thread_spawn_edges WHERE parent_thread_id IN (${placeholders}) OR child_thread_id IN (${placeholders})`).run(...ids, ...ids);
763
- db.query(`DELETE FROM threads WHERE id IN (${placeholders})`).run(...ids);
809
+ for (const threadIdChunk of chunkValues(ids, SQLITE_DELETE_BATCH_SIZE)) {
810
+ const placeholders = threadIdChunk.map(() => "?").join(", ");
811
+ if (existingTableNames.has("thread_dynamic_tools")) db.query(`DELETE FROM thread_dynamic_tools WHERE thread_id IN (${placeholders})`).run(...threadIdChunk);
812
+ if (existingTableNames.has("thread_goals")) db.query(`DELETE FROM thread_goals WHERE thread_id IN (${placeholders})`).run(...threadIdChunk);
813
+ if (existingTableNames.has("stage1_outputs")) db.query(`DELETE FROM stage1_outputs WHERE thread_id IN (${placeholders})`).run(...threadIdChunk);
814
+ if (existingTableNames.has("thread_spawn_edges")) db.query(`DELETE FROM thread_spawn_edges WHERE parent_thread_id IN (${placeholders}) OR child_thread_id IN (${placeholders})`).run(...threadIdChunk, ...threadIdChunk);
815
+ db.query(`DELETE FROM threads WHERE id IN (${placeholders})`).run(...threadIdChunk);
816
+ }
764
817
  })(existingIds);
765
818
  return {
766
819
  deletedSessionFiles: threadTargets.map((target) => target.rollout_path),
@@ -769,7 +822,10 @@ var deleteThreadIds = (db, threadIds) => {
769
822
  };
770
823
  var deleteThreadSessionFiles = async (sessionFiles) => {
771
824
  const uniqueSessionFiles = [...new Set(sessionFiles)];
772
- await Promise.all(uniqueSessionFiles.map((sessionFile) => rm(sessionFile, { force: true })));
825
+ await mapWithConcurrency(uniqueSessionFiles, SESSION_FILE_DELETE_CONCURRENCY, async (sessionFile) => {
826
+ await rm(sessionFile, { force: true });
827
+ return sessionFile;
828
+ });
773
829
  return uniqueSessionFiles;
774
830
  };
775
831
  var listCodexProjects = (dbPath) => {
@@ -783,8 +839,7 @@ var compactThreadListRow = (thread) => {
783
839
  };
784
840
  };
785
841
  var listProjectThreads = async (dbPath, projectName, options = {}) => {
786
- const threads = filterThreadsByProject(readAllThreads(dbPath), projectName);
787
- return (await Promise.all(threads.map(async (thread) => {
842
+ return (await mapWithConcurrency(filterThreadsByProject(readAllThreads(dbPath), projectName), THREAD_LIST_IO_CONCURRENCY, async (thread) => {
788
843
  const rollout = await getThreadRolloutLoadState(thread.rollout_path, options.largeTranscriptThresholdBytes);
789
844
  if (rollout.shouldDeferTranscriptLoad) return {
790
845
  project: projectName,
@@ -809,7 +864,7 @@ var listProjectThreads = async (dbPath, projectName, options = {}) => {
809
864
  },
810
865
  thread: compactThreadListRow(thread)
811
866
  };
812
- }))).sort((left, right) => toTimestampMs(right.thread) - toTimestampMs(left.thread));
867
+ })).sort((left, right) => toTimestampMs(right.thread) - toTimestampMs(left.thread));
813
868
  };
814
869
  var getThreadBrowseData = (dbPath, threadId) => {
815
870
  return withReadonlyDb(dbPath, (db) => {
@@ -1005,7 +1060,7 @@ var matchesProjectFilter = (value, projectFilter) => {
1005
1060
  var convertSessionFile = async (target, options) => {
1006
1061
  let transcriptState;
1007
1062
  try {
1008
- transcriptState = await collectCodexTranscript(target.sessionFile, options);
1063
+ transcriptState = await collectCodexTranscript(target.sessionFile, options, target.thread?.model ?? null);
1009
1064
  } catch (error) {
1010
1065
  const message = error instanceof Error ? error.message : String(error);
1011
1066
  throw new Error(`Failed to read Codex transcript ${target.sessionFile}: ${message}`);
@@ -1024,7 +1079,7 @@ var convertSessionFile = async (target, options) => {
1024
1079
  };
1025
1080
  var writeSessionFileExport = async (target, options, outputPath, transform = (text) => text) => {
1026
1081
  const transcriptOutputPath = `${outputPath}.transcript.tmp`;
1027
- const transcriptStream = await createExportWriteStream(transcriptOutputPath);
1082
+ let transcriptStream = null;
1028
1083
  const state = {
1029
1084
  assistantModel: target.thread?.model ?? null,
1030
1085
  sections: [],
@@ -1033,6 +1088,7 @@ var writeSessionFileExport = async (target, options, outputPath, transform = (te
1033
1088
  };
1034
1089
  let wroteSection = false;
1035
1090
  try {
1091
+ transcriptStream = await createExportWriteStream(transcriptOutputPath);
1036
1092
  for await (const parsed of readJsonlObjects(target.sessionFile)) {
1037
1093
  captureSessionMeta(parsed, state.sessionMeta);
1038
1094
  const block = renderCodexTranscriptRecord(parsed, options, state);
@@ -1040,29 +1096,33 @@ var writeSessionFileExport = async (target, options, outputPath, transform = (te
1040
1096
  transcriptStream.write(transform(wroteSection ? `${getSectionSeparator(options)}${block}` : block));
1041
1097
  wroteSection = true;
1042
1098
  }
1099
+ await finalizeExportWriteStream(transcriptStream);
1100
+ transcriptStream = null;
1101
+ if (!matchesFilters(target.thread?.cwd ?? state.sessionMeta.cwd ?? null, options) || !wroteSection) return false;
1102
+ const outputStream = await createExportWriteStream(outputPath);
1103
+ try {
1104
+ const prefix = buildStreamExportPrefix(target, state.sessionMeta, options);
1105
+ if (prefix) outputStream.write(transform(prefix));
1106
+ const transcriptReadStream = createReadStream(transcriptOutputPath, { encoding: "utf8" });
1107
+ transcriptReadStream.pipe(outputStream, { end: false });
1108
+ await finished(transcriptReadStream);
1109
+ outputStream.write("\n");
1110
+ await finalizeExportWriteStream(outputStream);
1111
+ } catch (error) {
1112
+ outputStream.destroy();
1113
+ throw error;
1114
+ }
1115
+ return true;
1043
1116
  } catch (error) {
1044
- transcriptStream.destroy();
1117
+ if (transcriptStream) transcriptStream.destroy();
1045
1118
  throw error;
1046
- }
1047
- await finalizeExportWriteStream(transcriptStream);
1048
- if (!matchesFilters(target.thread?.cwd ?? state.sessionMeta.cwd ?? null, options) || !wroteSection) {
1119
+ } finally {
1049
1120
  await rm(transcriptOutputPath, { force: true });
1050
- return false;
1051
1121
  }
1052
- const outputStream = await createExportWriteStream(outputPath);
1053
- const prefix = buildStreamExportPrefix(target, state.sessionMeta, options);
1054
- if (prefix) outputStream.write(transform(prefix));
1055
- const transcriptReadStream = createReadStream(transcriptOutputPath, { encoding: "utf8" });
1056
- transcriptReadStream.pipe(outputStream, { end: false });
1057
- await finished(transcriptReadStream);
1058
- outputStream.write("\n");
1059
- await finalizeExportWriteStream(outputStream);
1060
- await rm(transcriptOutputPath, { force: true });
1061
- return true;
1062
- };
1063
- var collectCodexTranscript = async (sessionFile, options) => {
1122
+ };
1123
+ var collectCodexTranscript = async (sessionFile, options, assistantModel = null) => {
1064
1124
  const state = {
1065
- assistantModel: null,
1125
+ assistantModel,
1066
1126
  sections: [],
1067
1127
  sessionMeta: {},
1068
1128
  startedTranscript: false
@@ -1623,6 +1683,19 @@ var logRolloutChangeIfDetected = (threadId, rolloutPath, beforeSnapshot, afterSn
1623
1683
  threadId
1624
1684
  });
1625
1685
  };
1686
+ var cleanupExportWorkspace = async (workspacePath) => {
1687
+ try {
1688
+ await rm(workspacePath, {
1689
+ force: true,
1690
+ recursive: true
1691
+ });
1692
+ } catch (error) {
1693
+ logExportEvent("warn", "workspace_cleanup_failed", {
1694
+ error: error instanceof Error ? error.message : String(error),
1695
+ workspacePath
1696
+ });
1697
+ }
1698
+ };
1626
1699
  var zipExportFile = async (sourcePath, zipPath) => {
1627
1700
  const proc = Bun.spawn([
1628
1701
  "zip",
@@ -1695,10 +1768,7 @@ var renderCodexThreadDownload = async (input) => {
1695
1768
  }, toDownloadOptions(input), savedPath, transform)) throw new Error(`Thread ${input.threadId} produced no exportable content`);
1696
1769
  await zipExportFile(savedPath, zipPath);
1697
1770
  } finally {
1698
- await rm(workspaceDir, {
1699
- force: true,
1700
- recursive: true
1701
- });
1771
+ await cleanupExportWorkspace(workspaceDir);
1702
1772
  }
1703
1773
  const rolloutSnapshotAfter = await getRolloutSnapshot(browseData.thread.rollout_path);
1704
1774
  logRolloutChangeIfDetected(input.threadId, browseData.thread.rollout_path, rolloutSnapshotBefore, rolloutSnapshotAfter);
@@ -1801,10 +1871,7 @@ var renderCodexThreadsDownload = async (input) => {
1801
1871
  });
1802
1872
  throw error;
1803
1873
  } finally {
1804
- await rm(bundleDirectory, {
1805
- force: true,
1806
- recursive: true
1807
- });
1874
+ await cleanupExportWorkspace(bundleDirectory);
1808
1875
  }
1809
1876
  const zipStat = await Bun.file(zipPath).stat();
1810
1877
  logExportEvent("info", "batch_ready", {
@@ -124,6 +124,7 @@ 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",
127
128
  checked: optimized,
128
129
  onCheckedChange: (checked) => setOptimized(checked === true)
129
130
  }), /* @__PURE__ */ jsxs("span", {
@@ -140,6 +141,7 @@ function ExportDialog({ open, pending = false, title = "Export thread", onExport
140
141
  /* @__PURE__ */ jsxs("div", {
141
142
  className: "flex items-center gap-3 rounded-2xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3",
142
143
  children: [/* @__PURE__ */ jsx(Checkbox$1, {
144
+ "aria-label": "Include commentary",
143
145
  checked: includeCommentary,
144
146
  onCheckedChange: (checked) => setIncludeCommentary(checked === true)
145
147
  }), /* @__PURE__ */ jsxs("span", {
@@ -156,6 +158,7 @@ function ExportDialog({ open, pending = false, title = "Export thread", onExport
156
158
  /* @__PURE__ */ jsxs("div", {
157
159
  className: "flex items-center gap-3 rounded-2xl border border-[var(--border)] bg-[var(--panel-secondary)] p-3",
158
160
  children: [/* @__PURE__ */ jsx(Checkbox$1, {
161
+ "aria-label": "Include tool calls",
159
162
  checked: includeTools,
160
163
  onCheckedChange: (checked) => setIncludeTools(checked === true)
161
164
  }), /* @__PURE__ */ jsxs("span", {
@@ -0,0 +1,91 @@
1
+ import { t as formatModelLabel$1 } from "./model-label-B1NWGc65.js";
2
+ //#region src/lib/formatters.ts
3
+ var DATE_TIME_FORMATTERS = /* @__PURE__ */ new Map();
4
+ var getDateTimeFormatters = (timeZone) => {
5
+ const cacheKey = timeZone ?? "local";
6
+ const cached = DATE_TIME_FORMATTERS.get(cacheKey);
7
+ if (cached) return cached;
8
+ const created = {
9
+ dayKeyFormatter: new Intl.DateTimeFormat("en-CA", {
10
+ day: "2-digit",
11
+ month: "2-digit",
12
+ timeZone,
13
+ year: "numeric"
14
+ }),
15
+ timePartsFormatter: new Intl.DateTimeFormat("en-US", {
16
+ day: "numeric",
17
+ hour: "numeric",
18
+ hour12: true,
19
+ minute: "2-digit",
20
+ month: "short",
21
+ timeZone,
22
+ year: "numeric"
23
+ })
24
+ };
25
+ DATE_TIME_FORMATTERS.set(cacheKey, created);
26
+ return created;
27
+ };
28
+ var buildDayKey = (date, timeZone) => {
29
+ return getDateTimeFormatters(timeZone).dayKeyFormatter.format(date);
30
+ };
31
+ var formatTimeParts = (date, timeZone) => {
32
+ const parts = getDateTimeFormatters(timeZone).timePartsFormatter.formatToParts(date);
33
+ const partMap = new Map(parts.map((part) => [part.type, part.value]));
34
+ const hour = partMap.get("hour");
35
+ const minute = partMap.get("minute");
36
+ const dayPeriod = partMap.get("dayPeriod");
37
+ if (!hour || !minute || !dayPeriod) return null;
38
+ return {
39
+ day: partMap.get("day") ?? "",
40
+ month: partMap.get("month") ?? "",
41
+ time: `${hour}:${minute} ${dayPeriod}`.trim(),
42
+ year: partMap.get("year") ?? ""
43
+ };
44
+ };
45
+ var formatNumber = (value) => {
46
+ return new Intl.NumberFormat("en-US").format(value);
47
+ };
48
+ var formatTokens = (value) => {
49
+ return `${formatNumber(value)} tokens`;
50
+ };
51
+ var formatBytes = (value) => {
52
+ if (!value || value <= 0) return "0 B";
53
+ const units = [
54
+ "B",
55
+ "KB",
56
+ "MB",
57
+ "GB",
58
+ "TB"
59
+ ];
60
+ let size = value;
61
+ let unitIndex = 0;
62
+ while (size >= 1024 && unitIndex < units.length - 1) {
63
+ size /= 1024;
64
+ unitIndex += 1;
65
+ }
66
+ const fractionDigits = size >= 100 || unitIndex === 0 ? 0 : 1;
67
+ return `${size.toFixed(fractionDigits)} ${units[unitIndex]}`;
68
+ };
69
+ var formatDateTime = (value, options = {}) => {
70
+ if (value === null || value === void 0 || value === "") return "n/a";
71
+ const date = new Date(value);
72
+ if (Number.isNaN(date.getTime())) return "n/a";
73
+ const now = options.now ?? /* @__PURE__ */ new Date();
74
+ const parts = formatTimeParts(date, options.timeZone);
75
+ if (!parts) return "n/a";
76
+ const { day, month, time, year } = parts;
77
+ if (buildDayKey(date, options.timeZone) === buildDayKey(now, options.timeZone)) return time;
78
+ const currentYear = formatTimeParts(now, options.timeZone)?.year;
79
+ if (year && currentYear && year !== currentYear) return `${month} ${day}, ${year} · ${time}`;
80
+ return `${month} ${day} · ${time}`;
81
+ };
82
+ var formatList = (values) => {
83
+ if (values.length === 0) return "n/a";
84
+ return values.join(", ");
85
+ };
86
+ var formatBooleanLabel = (value) => {
87
+ return value ? "Yes" : "No";
88
+ };
89
+ var formatModelLabel = formatModelLabel$1;
90
+ //#endregion
91
+ export { formatModelLabel as a, formatList as i, formatBytes as n, formatNumber as o, formatDateTime as r, formatTokens as s, formatBooleanLabel as t };
@@ -0,0 +1,13 @@
1
+ //#region ../../src/lib/model-label.ts
2
+ var formatModelLabel = (value) => {
3
+ if (!value) return "Assistant";
4
+ return value.split(/[-_\s]+/u).filter(Boolean).map((part) => {
5
+ const lower = part.toLowerCase();
6
+ if (lower === "gpt") return "GPT";
7
+ if (/^[a-z]\d$/u.test(lower)) return lower.toUpperCase();
8
+ if (/^\d+(\.\d+)*$/u.test(part)) return part;
9
+ return `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`;
10
+ }).join(" ");
11
+ };
12
+ //#endregion
13
+ export { formatModelLabel as t };
@@ -19,7 +19,7 @@ var replaceExactProjectPath = (text, projectPath) => {
19
19
  return result;
20
20
  };
21
21
  var redactRemainingUsernames = (text) => {
22
- return text.replace(/\/Users\/[^/\\]+(?=\/|$)/gu, "~").replace(/[A-Za-z]:[\\/]+Users[\\/]+[^\\/]+(?=[\\/]|$)/gu, "~");
22
+ return text.replace(/\/home\/[^/\\]+(?=\/|$)/gu, "~").replace(/\/Users\/[^/\\]+(?=\/|$)/gu, "~").replace(/[A-Za-z]:[\\/]+Users[\\/]+[^\\/]+(?=[\\/]|$)/gu, "~");
23
23
  };
24
24
  var applyPathTransforms = (text, settings) => {
25
25
  let result = text;
@@ -4,7 +4,7 @@ import { createFileRoute, lazyRouteComponent } from "@tanstack/react-router";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  //#region src/routes/projects.$project.tsx
6
6
  var $$splitErrorComponentImporter = () => import("./projects._project-CLSohrBp.js");
7
- var $$splitComponentImporter = () => import("./projects._project-Bwf6iJC-.js");
7
+ var $$splitComponentImporter = () => import("./projects._project-CcJLp_A8.js");
8
8
  var Route = createFileRoute("/projects/$project")({
9
9
  component: lazyRouteComponent($$splitComponentImporter, "component"),
10
10
  errorComponent: lazyRouteComponent($$splitErrorComponentImporter, "errorComponent"),
@@ -1,12 +1,12 @@
1
1
  import { t as Button } from "./button-CmTDnzOn.js";
2
2
  import { n as useSettings } from "./settings-store-DpEJEQ7M.js";
3
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";
4
+ import { t as Route } from "./projects._project-CJ7l0ynC.js";
5
5
  import { t as DataTable } from "./data-table-Cdct823O.js";
6
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";
7
+ import { n as formatBytes, o as formatNumber, r as formatDateTime, s as formatTokens } from "./formatters-FJaGZgJk.js";
8
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";
9
+ import { n as downloadUrlFile, r as ExportDialog, t as downloadTextFile } from "./download-C5rkk_Bo.js";
10
10
  import { a as DropdownMenuTrigger, i as DropdownMenuItem, n as DropdownMenu, r as DropdownMenuContent, t as Input } from "./input-B4tEzctc.js";
11
11
  import { startTransition, useDeferredValue, useState } from "react";
12
12
  import { Link } from "@tanstack/react-router";
@@ -40,6 +40,7 @@ var columns = (onDeleteThread, onExportThread) => [
40
40
  columnHelper.accessor((row) => row.thread.updated_at_ms ?? row.thread.updated_at * 1e3, {
41
41
  cell: (info) => /* @__PURE__ */ jsx("span", {
42
42
  className: "whitespace-nowrap text-sm",
43
+ suppressHydrationWarning: true,
43
44
  children: formatDateTime(info.getValue())
44
45
  }),
45
46
  header: "Updated",
@@ -48,6 +49,7 @@ var columns = (onDeleteThread, onExportThread) => [
48
49
  columnHelper.accessor((row) => row.thread.created_at_ms ?? row.thread.created_at * 1e3, {
49
50
  cell: (info) => /* @__PURE__ */ jsx("span", {
50
51
  className: "whitespace-nowrap text-sm",
52
+ suppressHydrationWarning: true,
51
53
  children: formatDateTime(info.getValue())
52
54
  }),
53
55
  header: "Created",
@@ -2,7 +2,7 @@ import { t as Button } from "./button-CmTDnzOn.js";
2
2
  import { i as projectsQueryOptions, s as deleteProjectFn } from "./codex-queries-CAF6HYiG.js";
3
3
  import { t as DataTable } from "./data-table-Cdct823O.js";
4
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-B6o5pTY9.js";
5
+ import { i as formatList, o as formatNumber, r as formatDateTime, s as formatTokens } from "./formatters-FJaGZgJk.js";
6
6
  import { t as DeleteConfirmDialog } from "./delete-confirm-dialog-CWqcTXTF.js";
7
7
  import { a as DropdownMenuTrigger, i as DropdownMenuItem, n as DropdownMenu, r as DropdownMenuContent, t as Input } from "./input-B4tEzctc.js";
8
8
  import { startTransition, useDeferredValue, useState } from "react";
@@ -48,6 +48,7 @@ var columns = (onDeleteProject) => [
48
48
  columnHelper.accessor("lastUpdatedAtMs", {
49
49
  cell: (info) => /* @__PURE__ */ jsx("span", {
50
50
  className: "text-sm",
51
+ suppressHydrationWarning: true,
51
52
  children: formatDateTime(info.getValue())
52
53
  }),
53
54
  header: "Last updated"
@@ -2,9 +2,9 @@ import { t as cn } from "./utils-C_uf36nf.js";
2
2
  import { t as Button } from "./button-CmTDnzOn.js";
3
3
  import { t as SettingsProvider } from "./settings-store-DpEJEQ7M.js";
4
4
  import { i as projectsQueryOptions, t as analyticsQueryOptions } from "./codex-queries-CAF6HYiG.js";
5
- import { t as Route$5 } from "./routes-BJyx5OmO.js";
6
- import { t as Route$6 } from "./threads._threadId-D3PYZIwl.js";
7
- import { t as Route$7 } from "./projects._project-DdVSdfPe.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
8
  import { useEffect, useState } from "react";
9
9
  import { HeadContent, Link, Outlet, Scripts, createFileRoute, createRootRouteWithContext, createRouter, lazyRouteComponent, notFound, redirect, useRouterState } from "@tanstack/react-router";
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -124,7 +124,7 @@ function TooltipProvider({ delayDuration = 0, ...props }) {
124
124
  }
125
125
  //#endregion
126
126
  //#region src/styles.css?url
127
- var styles_default = "/assets/styles-8Wtc8YJw.css";
127
+ var styles_default = "/assets/styles-CMrP9Jb4.css";
128
128
  //#endregion
129
129
  //#region src/routes/__root.tsx
130
130
  var themeInitScript = `
@@ -207,7 +207,7 @@ var $$splitComponentImporter$3 = () => import("./settings-MvWDgc1u.js");
207
207
  var Route$3 = createFileRoute("/settings")({ component: lazyRouteComponent($$splitComponentImporter$3, "component") });
208
208
  //#endregion
209
209
  //#region src/routes/analytics.tsx
210
- var $$splitComponentImporter$2 = () => import("./analytics-Br_fZB6a.js");
210
+ var $$splitComponentImporter$2 = () => import("./analytics-BMxW_bZL.js");
211
211
  var Route$2 = createFileRoute("/analytics")({
212
212
  component: lazyRouteComponent($$splitComponentImporter$2, "component"),
213
213
  loader: ({ context }) => {
@@ -234,7 +234,7 @@ var Route$1 = createFileRoute("/$threadId")({
234
234
  //#endregion
235
235
  //#region src/routes/projects.index.tsx
236
236
  var $$splitErrorComponentImporter = () => import("./projects.index-CaplpeMy.js");
237
- var $$splitComponentImporter = () => import("./projects.index-DKeVeqUZ.js");
237
+ var $$splitComponentImporter = () => import("./projects.index-srtogpuF.js");
238
238
  var Route = createFileRoute("/projects/")({
239
239
  component: lazyRouteComponent($$splitComponentImporter, "component"),
240
240
  errorComponent: lazyRouteComponent($$splitErrorComponentImporter, "errorComponent"),
@@ -2,7 +2,7 @@ import { n as dashboardQueryOptions } from "./codex-queries-CAF6HYiG.js";
2
2
  import { createFileRoute, lazyRouteComponent } from "@tanstack/react-router";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/routes/index.tsx
5
- var $$splitComponentImporter = () => import("./routes-pkOwjjYc.js");
5
+ var $$splitComponentImporter = () => import("./routes-CPe-ppmC.js");
6
6
  var Route = createFileRoute("/")({
7
7
  component: lazyRouteComponent($$splitComponentImporter, "component"),
8
8
  loader: ({ context }) => context.queryClient.ensureQueryData(dashboardQueryOptions())
@@ -1,8 +1,8 @@
1
1
  import { n as dashboardQueryOptions } from "./codex-queries-CAF6HYiG.js";
2
- import { t as Route } from "./routes-BJyx5OmO.js";
2
+ import { t as Route } from "./routes-BhbxvJE7.js";
3
3
  import { t as MetricCard } from "./metric-card-ByEeLu0r.js";
4
4
  import { t as PageHeader } from "./page-header-CxdZM86z.js";
5
- import { o as formatNumber, r as formatDateTime, s as formatTokens } from "./formatters-B6o5pTY9.js";
5
+ import { o as formatNumber, r as formatDateTime, s as formatTokens } from "./formatters-FJaGZgJk.js";
6
6
  import { Link } from "@tanstack/react-router";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  import { useSuspenseQuery } from "@tanstack/react-query";
@@ -101,6 +101,7 @@ function DashboardPage() {
101
101
  })]
102
102
  }), /* @__PURE__ */ jsx("p", {
103
103
  className: "shrink-0 whitespace-nowrap font-mono text-[var(--muted-foreground)] text-xs",
104
+ suppressHydrationWarning: true,
104
105
  children: formatDateTime(thread.updated_at_ms ?? thread.updated_at * 1e3)
105
106
  })]
106
107
  }), /* @__PURE__ */ jsxs("div", {
@@ -1,4 +1,4 @@
1
- //#region ../../node_modules/.bun/@tanstack+react-start@1.168.11+1cc525de2aa52ed1/node_modules/@tanstack/react-start/dist/plugin/default-entry/start.ts
1
+ //#region ../../node_modules/.bun/@tanstack+react-start@1.168.11+5cefe02f681c7619/node_modules/@tanstack/react-start/dist/plugin/default-entry/start.ts
2
2
  var startInstance = void 0;
3
3
  //#endregion
4
4
  export { startInstance };
@@ -4,7 +4,7 @@ import { createFileRoute, lazyRouteComponent } from "@tanstack/react-router";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  //#region src/routes/threads.$threadId.tsx
6
6
  var $$splitErrorComponentImporter = () => import("./threads._threadId-BSSK4nkI.js");
7
- var $$splitComponentImporter = () => import("./threads._threadId-D3xaWM86.js");
7
+ var $$splitComponentImporter = () => import("./threads._threadId-euyNckhj.js");
8
8
  var Route = createFileRoute("/threads/$threadId")({
9
9
  component: lazyRouteComponent($$splitComponentImporter, "component"),
10
10
  errorComponent: lazyRouteComponent($$splitErrorComponentImporter, "errorComponent"),