bertrand 0.20.0 → 0.21.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.
@@ -15,17 +15,12 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // src/tui/run-screen.tsx
18
- import { writeFileSync as writeFileSync2 } from "fs";
18
+ import { writeFileSync as writeFileSync2, appendFileSync } from "fs";
19
19
  import { render } from "@orchetron/storm";
20
20
 
21
- // src/tui/screens/startup/index.tsx
22
- import { useState as useState4 } from "react";
23
- import { useTui } from "@orchetron/storm";
24
-
25
- // src/tui/screens/project-picker/index.tsx
26
- import { useMemo as useMemo2 } from "react";
27
- import { Box as Box5, Text as Text6 } from "@orchetron/storm";
28
- import { existsSync as existsSync2 } from "fs";
21
+ // src/tui/screens/launch/index.tsx
22
+ import { useMemo as useMemo2, useState as useState3 } from "react";
23
+ import { Box as Box5, Text as Text6, useTui } from "@orchetron/storm";
29
24
 
30
25
  // src/tui/components/app-details.tsx
31
26
  import { Box, Text } from "@orchetron/storm";
@@ -565,6 +560,16 @@ function StatusDot({ status }) {
565
560
  children: "\u25CF"
566
561
  }, undefined, false, undefined, this);
567
562
  }
563
+ // src/db/queries/sessions.ts
564
+ import { eq, and, inArray, sql as sql2 } from "drizzle-orm";
565
+
566
+ // src/db/client.ts
567
+ import { Database } from "bun:sqlite";
568
+ import { drizzle } from "drizzle-orm/bun-sqlite";
569
+ import { migrate } from "drizzle-orm/bun-sqlite/migrator";
570
+ import { mkdirSync as mkdirSync2 } from "fs";
571
+ import { dirname } from "path";
572
+
568
573
  // src/lib/projects/registry.ts
569
574
  import {
570
575
  existsSync,
@@ -575,7 +580,6 @@ import {
575
580
  statSync,
576
581
  writeFileSync
577
582
  } from "fs";
578
- import { randomBytes } from "crypto";
579
583
  import { join as join2 } from "path";
580
584
 
581
585
  // src/lib/paths.ts
@@ -586,6 +590,7 @@ var paths = {
586
590
  root: join(homedir(), BERTRAND_DIR),
587
591
  hooks: join(homedir(), BERTRAND_DIR, "hooks"),
588
592
  sessions: join(homedir(), BERTRAND_DIR, "sessions"),
593
+ runtime: join(homedir(), BERTRAND_DIR, "run"),
589
594
  db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
590
595
  syncEnv: join(homedir(), BERTRAND_DIR, "sync.env")
591
596
  };
@@ -616,14 +621,6 @@ function readRegistry() {
616
621
  return null;
617
622
  }
618
623
  }
619
- function writeRegistry(registry) {
620
- const path = registryPath();
621
- mkdirSync(_registryDir, { recursive: true });
622
- const tmp = `${path}.tmp-${process.pid}-${process.hrtime.bigint()}-${randomBytes(4).toString("hex")}`;
623
- writeFileSync(tmp, JSON.stringify(registry, null, 2) + `
624
- `);
625
- renameSync(tmp, path);
626
- }
627
624
  function recoverFromDisk() {
628
625
  const dir = projectsDir();
629
626
  if (!existsSync(dir))
@@ -673,52 +670,6 @@ function listProjects() {
673
670
  function getActiveProjectSlug() {
674
671
  return loadRegistry()?.activeProjectSlug ?? DEFAULT_PROJECT_SLUG;
675
672
  }
676
- function setActiveProjectSlug(slug) {
677
- const registry = loadRegistry();
678
- if (!registry) {
679
- throw new Error(`No registry to update \u2014 create a project first (no ${registryPath()} and no ${projectsDir()}/* to recover from)`);
680
- }
681
- if (!registry.projects.some((p) => p.slug === slug)) {
682
- throw new Error(`Unknown project slug "${slug}"`);
683
- }
684
- registry.activeProjectSlug = slug;
685
- const idx = registry.projects.findIndex((p) => p.slug === slug);
686
- if (idx !== -1) {
687
- registry.projects[idx].lastUsedAt = new Date().toISOString();
688
- }
689
- writeRegistry(registry);
690
- }
691
- function registerProject(opts) {
692
- const registry = loadRegistry() ?? {
693
- activeProjectSlug: opts.slug,
694
- projects: []
695
- };
696
- if (registry.projects.some((p) => p.slug === opts.slug)) {
697
- throw new Error(`Project "${opts.slug}" already exists`);
698
- }
699
- const now = new Date().toISOString();
700
- const entry = {
701
- slug: opts.slug,
702
- name: opts.name,
703
- color: opts.color,
704
- createdAt: now,
705
- lastUsedAt: now
706
- };
707
- registry.projects.push(entry);
708
- writeRegistry(registry);
709
- return entry;
710
- }
711
- function removeProject(slug) {
712
- const registry = loadRegistry();
713
- if (!registry)
714
- return;
715
- registry.projects = registry.projects.filter((p) => p.slug !== slug);
716
- if (registry.activeProjectSlug === slug) {
717
- const survivor = [...registry.projects].sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt))[0];
718
- registry.activeProjectSlug = survivor?.slug ?? DEFAULT_PROJECT_SLUG;
719
- }
720
- writeRegistry(registry);
721
- }
722
673
  function isRegistry(value) {
723
674
  if (typeof value !== "object" || value === null)
724
675
  return false;
@@ -747,13 +698,6 @@ function projectPaths(slug) {
747
698
  };
748
699
  }
749
700
 
750
- // src/db/client.ts
751
- import { Database } from "bun:sqlite";
752
- import { drizzle } from "drizzle-orm/bun-sqlite";
753
- import { migrate } from "drizzle-orm/bun-sqlite/migrator";
754
- import { mkdirSync as mkdirSync2 } from "fs";
755
- import { dirname } from "path";
756
-
757
701
  // src/lib/projects/resolve.ts
758
702
  var _cached = null;
759
703
  function resolveActiveProject() {
@@ -771,9 +715,6 @@ function resolveActiveProject() {
771
715
  _cached = { slug, name, ...projectPaths(slug) };
772
716
  return _cached;
773
717
  }
774
- function _resetActiveProjectCache() {
775
- _cached = null;
776
- }
777
718
 
778
719
  // src/db/schema.ts
779
720
  var exports_schema = {};
@@ -936,207 +877,6 @@ function hasSessionsTable(sqlite) {
936
877
  return row !== null;
937
878
  }
938
879
 
939
- // src/lib/format.ts
940
- var SECOND = 1000;
941
- var MINUTE = 60 * SECOND;
942
- var HOUR = 60 * MINUTE;
943
- var DAY = 24 * HOUR;
944
- function formatDuration(ms) {
945
- if (ms < MINUTE)
946
- return `${Math.round(ms / SECOND)}s`;
947
- const days = Math.floor(ms / DAY);
948
- const hours = Math.floor(ms % DAY / HOUR);
949
- const minutes = Math.floor(ms % HOUR / MINUTE);
950
- if (days > 0)
951
- return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
952
- if (hours > 0)
953
- return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
954
- return `${minutes}m`;
955
- }
956
- function formatAgo(isoOrDate) {
957
- const date = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
958
- const ms = Date.now() - date.getTime();
959
- if (ms < MINUTE)
960
- return "just now";
961
- if (ms < HOUR)
962
- return `${Math.floor(ms / MINUTE)}m ago`;
963
- if (ms < DAY)
964
- return `${Math.floor(ms / HOUR)}h ago`;
965
- if (ms < 2 * DAY)
966
- return "yesterday";
967
- if (ms < 7 * DAY)
968
- return `${Math.floor(ms / DAY)}d ago`;
969
- return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
970
- }
971
-
972
- // src/tui/screens/project-picker/index.tsx
973
- import { jsxDEV as jsxDEV6, Fragment } from "react/jsx-dev-runtime";
974
- var SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
975
- function statsFor(slug) {
976
- const dbFile = projectPaths(slug).db;
977
- if (!existsSync2(dbFile))
978
- return { total: 0, active: 0, unreadable: false };
979
- try {
980
- const db = getDbForProject(slug);
981
- const all = db.select({ status: sessions.status }).from(sessions).all();
982
- return {
983
- total: all.length,
984
- active: all.filter((s) => s.status === "active" || s.status === "waiting").length,
985
- unreadable: false
986
- };
987
- } catch {
988
- return { total: 0, active: 0, unreadable: true };
989
- }
990
- }
991
- function projectRow(entry, isActive) {
992
- const stats = statsFor(entry.slug);
993
- const sessionsLabel = stats.unreadable ? "?" : `${stats.active}/${stats.total}`;
994
- const lastUsed = formatAgo(entry.lastUsedAt);
995
- return {
996
- value: entry.slug,
997
- label: `${entry.slug} ${entry.name} ${sessionsLabel}`,
998
- meta: lastUsed,
999
- display: (isCursor) => {
1000
- const cursorColor = "green";
1001
- const slugColor = isCursor ? cursorColor : undefined;
1002
- const nameColor = isCursor ? cursorColor : undefined;
1003
- return /* @__PURE__ */ jsxDEV6(Fragment, {
1004
- children: [
1005
- /* @__PURE__ */ jsxDEV6(Text6, {
1006
- color: cursorColor,
1007
- bold: true,
1008
- children: isCursor ? "\u276F " : " "
1009
- }, undefined, false, undefined, this),
1010
- /* @__PURE__ */ jsxDEV6(Text6, {
1011
- bold: isActive,
1012
- color: isActive ? "gold" : slugColor,
1013
- children: isActive ? "\u25CF " : " "
1014
- }, undefined, false, undefined, this),
1015
- /* @__PURE__ */ jsxDEV6(Text6, {
1016
- color: slugColor,
1017
- bold: isCursor || isActive,
1018
- children: entry.slug.padEnd(16)
1019
- }, undefined, false, undefined, this),
1020
- /* @__PURE__ */ jsxDEV6(Text6, {
1021
- color: nameColor,
1022
- dim: !isCursor,
1023
- children: entry.name.padEnd(20)
1024
- }, undefined, false, undefined, this),
1025
- /* @__PURE__ */ jsxDEV6(Text6, {
1026
- dim: true,
1027
- children: stats.unreadable ? " (unreadable)" : ` ${sessionsLabel} active/total`
1028
- }, undefined, false, undefined, this)
1029
- ]
1030
- }, undefined, true, undefined, this);
1031
- }
1032
- };
1033
- }
1034
- function ProjectPicker({ onSelect }) {
1035
- const allProjects = useMemo2(() => listProjects(), []);
1036
- const activeSlug = useMemo2(() => getActiveProjectSlug(), []);
1037
- const items = useMemo2(() => {
1038
- return allProjects.slice().sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt)).map((p) => projectRow(p, p.slug === activeSlug));
1039
- }, [allProjects, activeSlug]);
1040
- const suggestions = useMemo2(() => allProjects.map((p) => p.slug), [allProjects]);
1041
- const knownSlugs = useMemo2(() => new Set(allProjects.map((p) => p.slug)), [allProjects]);
1042
- const select = (selection) => {
1043
- onSelect(selection);
1044
- };
1045
- const handleSubmit = (value) => {
1046
- const trimmed = value.trim();
1047
- if (!trimmed)
1048
- return;
1049
- if (knownSlugs.has(trimmed)) {
1050
- select({ type: "select", slug: trimmed });
1051
- return;
1052
- }
1053
- if (!SLUG_PATTERN.test(trimmed)) {
1054
- return;
1055
- }
1056
- select({ type: "create", slug: trimmed });
1057
- };
1058
- return /* @__PURE__ */ jsxDEV6(Box5, {
1059
- flexDirection: "column",
1060
- paddingY: 1,
1061
- gap: 1,
1062
- children: [
1063
- /* @__PURE__ */ jsxDEV6(Box5, {
1064
- marginX: 1,
1065
- children: /* @__PURE__ */ jsxDEV6(Logo, {}, undefined, false, undefined, this)
1066
- }, undefined, false, undefined, this),
1067
- /* @__PURE__ */ jsxDEV6(Box5, {
1068
- flexDirection: "column",
1069
- marginX: 2,
1070
- gap: 1,
1071
- children: [
1072
- /* @__PURE__ */ jsxDEV6(AppDetails, {}, undefined, false, undefined, this),
1073
- /* @__PURE__ */ jsxDEV6(Box5, {
1074
- flexDirection: "column",
1075
- gap: 1,
1076
- children: [
1077
- /* @__PURE__ */ jsxDEV6(Box5, {
1078
- flexDirection: "row",
1079
- gap: 1,
1080
- children: [
1081
- /* @__PURE__ */ jsxDEV6(Text6, {
1082
- bold: true,
1083
- children: "Projects"
1084
- }, undefined, false, undefined, this),
1085
- allProjects.length > 0 ? /* @__PURE__ */ jsxDEV6(Fragment, {
1086
- children: [
1087
- /* @__PURE__ */ jsxDEV6(Text6, {
1088
- dim: true,
1089
- children: "\xB7"
1090
- }, undefined, false, undefined, this),
1091
- /* @__PURE__ */ jsxDEV6(Text6, {
1092
- dim: true,
1093
- children: [
1094
- allProjects.length,
1095
- " total",
1096
- activeSlug ? ` \xB7 active: ${activeSlug}` : ""
1097
- ]
1098
- }, undefined, true, undefined, this)
1099
- ]
1100
- }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV6(Text6, {
1101
- dim: true,
1102
- children: "\xB7 none \u2014 type a slug to create your first project"
1103
- }, undefined, false, undefined, this)
1104
- ]
1105
- }, undefined, true, undefined, this),
1106
- /* @__PURE__ */ jsxDEV6(Picker, {
1107
- mode: "single",
1108
- items,
1109
- isFocused: true,
1110
- maxVisible: 16,
1111
- suggest: suggestions,
1112
- placeholder: "Filter or type a new slug to create\u2026",
1113
- emptyHint: "No projects. Type a slug to create one.",
1114
- onSubmit: handleSubmit,
1115
- onKey: (e) => {
1116
- if (e.key === "c" && e.ctrl) {
1117
- select({ type: "quit" });
1118
- }
1119
- }
1120
- }, undefined, false, undefined, this),
1121
- /* @__PURE__ */ jsxDEV6(Text6, {
1122
- dim: true,
1123
- children: "\u2191\u2193 navigate \xB7 enter select \xB7 type to filter or create new \xB7 ctrl+c quit"
1124
- }, undefined, false, undefined, this)
1125
- ]
1126
- }, undefined, true, undefined, this)
1127
- ]
1128
- }, undefined, true, undefined, this)
1129
- ]
1130
- }, undefined, true, undefined, this);
1131
- }
1132
-
1133
- // src/tui/screens/launch/index.tsx
1134
- import { useMemo as useMemo3, useState as useState3 } from "react";
1135
- import { Box as Box6, Text as Text7 } from "@orchetron/storm";
1136
-
1137
- // src/db/queries/sessions.ts
1138
- import { eq, and, inArray, sql as sql2 } from "drizzle-orm";
1139
-
1140
880
  // src/lib/id.ts
1141
881
  import { nanoid } from "nanoid";
1142
882
 
@@ -1186,6 +926,39 @@ function unarchiveSession(id) {
1186
926
  return { ok: true, session: updated };
1187
927
  }
1188
928
 
929
+ // src/lib/format.ts
930
+ var SECOND = 1000;
931
+ var MINUTE = 60 * SECOND;
932
+ var HOUR = 60 * MINUTE;
933
+ var DAY = 24 * HOUR;
934
+ function formatDuration(ms) {
935
+ if (ms < MINUTE)
936
+ return `${Math.round(ms / SECOND)}s`;
937
+ const days = Math.floor(ms / DAY);
938
+ const hours = Math.floor(ms % DAY / HOUR);
939
+ const minutes = Math.floor(ms % HOUR / MINUTE);
940
+ if (days > 0)
941
+ return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
942
+ if (hours > 0)
943
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
944
+ return `${minutes}m`;
945
+ }
946
+ function formatAgo(isoOrDate) {
947
+ const date = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
948
+ const ms = Date.now() - date.getTime();
949
+ if (ms < MINUTE)
950
+ return "just now";
951
+ if (ms < HOUR)
952
+ return `${Math.floor(ms / MINUTE)}m ago`;
953
+ if (ms < DAY)
954
+ return `${Math.floor(ms / HOUR)}h ago`;
955
+ if (ms < 2 * DAY)
956
+ return "yesterday";
957
+ if (ms < 7 * DAY)
958
+ return `${Math.floor(ms / DAY)}d ago`;
959
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
960
+ }
961
+
1189
962
  // src/lib/parse-session-name.ts
1190
963
  function parseSessionName(input) {
1191
964
  const trimmed = input.trim().replace(/^\/+|\/+$/g, "");
@@ -1207,7 +980,7 @@ function parseSessionName(input) {
1207
980
  }
1208
981
 
1209
982
  // src/tui/screens/launch/index.tsx
1210
- import { jsxDEV as jsxDEV7, Fragment as Fragment2 } from "react/jsx-dev-runtime";
983
+ import { jsxDEV as jsxDEV6, Fragment } from "react/jsx-dev-runtime";
1211
984
  var STATUS_COLOR = {
1212
985
  paused: "gold",
1213
986
  waiting: "red",
@@ -1241,14 +1014,14 @@ function sessionRow(s) {
1241
1014
  const textColor = isCursor ? cursorColor : color;
1242
1015
  const slugColor = isCursor ? cursorColor : undefined;
1243
1016
  const dimText = !isCursor && (disabled || isArchived);
1244
- return /* @__PURE__ */ jsxDEV7(Fragment2, {
1017
+ return /* @__PURE__ */ jsxDEV6(Fragment, {
1245
1018
  children: [
1246
- /* @__PURE__ */ jsxDEV7(Text7, {
1019
+ /* @__PURE__ */ jsxDEV6(Text6, {
1247
1020
  color: cursorColor,
1248
1021
  bold: true,
1249
1022
  children: isCursor ? "\u276F " : " "
1250
1023
  }, undefined, false, undefined, this),
1251
- /* @__PURE__ */ jsxDEV7(Text7, {
1024
+ /* @__PURE__ */ jsxDEV6(Text6, {
1252
1025
  color: dotColor,
1253
1026
  bold: isCursor,
1254
1027
  children: [
@@ -1256,17 +1029,17 @@ function sessionRow(s) {
1256
1029
  " "
1257
1030
  ]
1258
1031
  }, undefined, true, undefined, this),
1259
- /* @__PURE__ */ jsxDEV7(Text7, {
1032
+ /* @__PURE__ */ jsxDEV6(Text6, {
1260
1033
  color: textColor,
1261
1034
  bold: isCursor,
1262
1035
  dim: dimText,
1263
1036
  children: status.padEnd(8)
1264
1037
  }, undefined, false, undefined, this),
1265
- /* @__PURE__ */ jsxDEV7(Text7, {
1038
+ /* @__PURE__ */ jsxDEV6(Text6, {
1266
1039
  color: slugColor,
1267
1040
  children: " "
1268
1041
  }, undefined, false, undefined, this),
1269
- /* @__PURE__ */ jsxDEV7(Text7, {
1042
+ /* @__PURE__ */ jsxDEV6(Text6, {
1270
1043
  color: slugColor,
1271
1044
  bold: isCursor,
1272
1045
  dim: dimText,
@@ -1285,12 +1058,13 @@ function categoryHeader(categoryPath) {
1285
1058
  };
1286
1059
  }
1287
1060
  function Launch({ onSelect }) {
1061
+ const { exit } = useTui();
1288
1062
  const [error, setError] = useState3(null);
1289
1063
  const [notice, setNotice] = useState3(null);
1290
1064
  const [showArchived, setShowArchived] = useState3(false);
1291
1065
  const [refreshKey, setRefreshKey] = useState3(0);
1292
- const allSessions = useMemo3(() => getAllSessions({ excludeArchived: !showArchived }), [showArchived, refreshKey]);
1293
- const visibleSessions = useMemo3(() => {
1066
+ const allSessions = useMemo2(() => getAllSessions({ excludeArchived: !showArchived }), [showArchived, refreshKey]);
1067
+ const visibleSessions = useMemo2(() => {
1294
1068
  return allSessions.filter((s) => {
1295
1069
  const st = s.session.status;
1296
1070
  if (st === "paused" || st === "waiting")
@@ -1308,7 +1082,7 @@ function Launch({ onSelect }) {
1308
1082
  return recencyKey(b).localeCompare(recencyKey(a));
1309
1083
  });
1310
1084
  }, [allSessions, showArchived]);
1311
- const items = useMemo3(() => {
1085
+ const items = useMemo2(() => {
1312
1086
  const rows = [];
1313
1087
  let lastCategory = null;
1314
1088
  for (const s of visibleSessions) {
@@ -1320,7 +1094,7 @@ function Launch({ onSelect }) {
1320
1094
  }
1321
1095
  return rows;
1322
1096
  }, [visibleSessions]);
1323
- const suggestions = useMemo3(() => {
1097
+ const suggestions = useMemo2(() => {
1324
1098
  const categories2 = new Set;
1325
1099
  const names = [];
1326
1100
  for (const s of allSessions) {
@@ -1329,7 +1103,7 @@ function Launch({ onSelect }) {
1329
1103
  }
1330
1104
  return [...categories2, ...names];
1331
1105
  }, [allSessions]);
1332
- const sessionByValue = useMemo3(() => {
1106
+ const sessionByValue = useMemo2(() => {
1333
1107
  const map = new Map;
1334
1108
  for (const s of allSessions) {
1335
1109
  map.set(`${s.categoryPath}/${s.session.slug}`, s);
@@ -1363,6 +1137,7 @@ function Launch({ onSelect }) {
1363
1137
  };
1364
1138
  const select = (selection) => {
1365
1139
  onSelect(selection);
1140
+ exit();
1366
1141
  };
1367
1142
  const handleSubmit = (value) => {
1368
1143
  const existing = sessionByValue.get(value);
@@ -1381,7 +1156,7 @@ function Launch({ onSelect }) {
1381
1156
  setError(e instanceof Error ? e.message : "Invalid name");
1382
1157
  }
1383
1158
  };
1384
- const counts = useMemo3(() => {
1159
+ const counts = useMemo2(() => {
1385
1160
  let paused = 0;
1386
1161
  let waiting = 0;
1387
1162
  let archived = 0;
@@ -1395,56 +1170,56 @@ function Launch({ onSelect }) {
1395
1170
  }
1396
1171
  return { paused, waiting, archived };
1397
1172
  }, [visibleSessions]);
1398
- return /* @__PURE__ */ jsxDEV7(Box6, {
1173
+ return /* @__PURE__ */ jsxDEV6(Box5, {
1399
1174
  flexDirection: "column",
1400
1175
  paddingY: 1,
1401
1176
  gap: 1,
1402
1177
  children: [
1403
- /* @__PURE__ */ jsxDEV7(Box6, {
1178
+ /* @__PURE__ */ jsxDEV6(Box5, {
1404
1179
  marginX: 1,
1405
- children: /* @__PURE__ */ jsxDEV7(Logo, {}, undefined, false, undefined, this)
1180
+ children: /* @__PURE__ */ jsxDEV6(Logo, {}, undefined, false, undefined, this)
1406
1181
  }, undefined, false, undefined, this),
1407
- /* @__PURE__ */ jsxDEV7(Box6, {
1182
+ /* @__PURE__ */ jsxDEV6(Box5, {
1408
1183
  flexDirection: "column",
1409
1184
  marginX: 2,
1410
1185
  gap: 1,
1411
1186
  children: [
1412
- /* @__PURE__ */ jsxDEV7(AppDetails, {}, undefined, false, undefined, this),
1413
- /* @__PURE__ */ jsxDEV7(Box6, {
1187
+ /* @__PURE__ */ jsxDEV6(AppDetails, {}, undefined, false, undefined, this),
1188
+ /* @__PURE__ */ jsxDEV6(Box5, {
1414
1189
  flexDirection: "column",
1415
1190
  gap: 1,
1416
1191
  children: [
1417
- /* @__PURE__ */ jsxDEV7(Box6, {
1192
+ /* @__PURE__ */ jsxDEV6(Box5, {
1418
1193
  flexDirection: "row",
1419
1194
  gap: 1,
1420
1195
  children: [
1421
- /* @__PURE__ */ jsxDEV7(Text7, {
1196
+ /* @__PURE__ */ jsxDEV6(Text6, {
1422
1197
  bold: true,
1423
1198
  children: "Sessions"
1424
1199
  }, undefined, false, undefined, this),
1425
- visibleSessions.length === 0 ? /* @__PURE__ */ jsxDEV7(Text7, {
1200
+ visibleSessions.length === 0 ? /* @__PURE__ */ jsxDEV6(Text6, {
1426
1201
  dim: true,
1427
1202
  children: "\xB7 none \u2014 type category/slug to create"
1428
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV7(Fragment2, {
1203
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV6(Fragment, {
1429
1204
  children: [
1430
- /* @__PURE__ */ jsxDEV7(Text7, {
1205
+ /* @__PURE__ */ jsxDEV6(Text6, {
1431
1206
  dim: true,
1432
1207
  children: "\xB7"
1433
1208
  }, undefined, false, undefined, this),
1434
- /* @__PURE__ */ jsxDEV7(Text7, {
1209
+ /* @__PURE__ */ jsxDEV6(Text6, {
1435
1210
  color: "gold",
1436
1211
  children: [
1437
1212
  counts.paused,
1438
1213
  " paused"
1439
1214
  ]
1440
1215
  }, undefined, true, undefined, this),
1441
- counts.waiting > 0 && /* @__PURE__ */ jsxDEV7(Fragment2, {
1216
+ counts.waiting > 0 && /* @__PURE__ */ jsxDEV6(Fragment, {
1442
1217
  children: [
1443
- /* @__PURE__ */ jsxDEV7(Text7, {
1218
+ /* @__PURE__ */ jsxDEV6(Text6, {
1444
1219
  dim: true,
1445
1220
  children: "\xB7"
1446
1221
  }, undefined, false, undefined, this),
1447
- /* @__PURE__ */ jsxDEV7(Text7, {
1222
+ /* @__PURE__ */ jsxDEV6(Text6, {
1448
1223
  color: "red",
1449
1224
  dim: true,
1450
1225
  children: [
@@ -1454,13 +1229,13 @@ function Launch({ onSelect }) {
1454
1229
  }, undefined, true, undefined, this)
1455
1230
  ]
1456
1231
  }, undefined, true, undefined, this),
1457
- showArchived && counts.archived > 0 && /* @__PURE__ */ jsxDEV7(Fragment2, {
1232
+ showArchived && counts.archived > 0 && /* @__PURE__ */ jsxDEV6(Fragment, {
1458
1233
  children: [
1459
- /* @__PURE__ */ jsxDEV7(Text7, {
1234
+ /* @__PURE__ */ jsxDEV6(Text6, {
1460
1235
  dim: true,
1461
1236
  children: "\xB7"
1462
1237
  }, undefined, false, undefined, this),
1463
- /* @__PURE__ */ jsxDEV7(Text7, {
1238
+ /* @__PURE__ */ jsxDEV6(Text6, {
1464
1239
  color: "purple",
1465
1240
  dim: true,
1466
1241
  children: [
@@ -1474,7 +1249,7 @@ function Launch({ onSelect }) {
1474
1249
  }, undefined, true, undefined, this)
1475
1250
  ]
1476
1251
  }, undefined, true, undefined, this),
1477
- /* @__PURE__ */ jsxDEV7(Picker, {
1252
+ /* @__PURE__ */ jsxDEV6(Picker, {
1478
1253
  mode: "single",
1479
1254
  items,
1480
1255
  isFocused: true,
@@ -1495,15 +1270,15 @@ function Launch({ onSelect }) {
1495
1270
  }
1496
1271
  }
1497
1272
  }, undefined, false, undefined, this),
1498
- notice && /* @__PURE__ */ jsxDEV7(Text7, {
1273
+ notice && /* @__PURE__ */ jsxDEV6(Text6, {
1499
1274
  color: "green",
1500
1275
  children: notice
1501
1276
  }, undefined, false, undefined, this),
1502
- error && /* @__PURE__ */ jsxDEV7(Text7, {
1277
+ error && /* @__PURE__ */ jsxDEV6(Text6, {
1503
1278
  color: "red",
1504
1279
  children: error
1505
1280
  }, undefined, false, undefined, this),
1506
- /* @__PURE__ */ jsxDEV7(Text7, {
1281
+ /* @__PURE__ */ jsxDEV6(Text6, {
1507
1282
  dim: true,
1508
1283
  children: [
1509
1284
  "\u2191\u2193 navigate \xB7 \u2190\u2192 skip category \xB7 enter continue/create \xB7 ctrl+a",
@@ -1523,104 +1298,9 @@ function Launch({ onSelect }) {
1523
1298
  }, undefined, true, undefined, this);
1524
1299
  }
1525
1300
 
1526
- // src/lib/projects/create.ts
1527
- import { mkdirSync as mkdirSync4 } from "fs";
1528
-
1529
- // src/db/migrate.ts
1530
- import { Database as Database2 } from "bun:sqlite";
1531
- import { drizzle as drizzle2 } from "drizzle-orm/bun-sqlite";
1532
- import { migrate as migrate2 } from "drizzle-orm/bun-sqlite/migrator";
1533
- import { mkdirSync as mkdirSync3 } from "fs";
1534
- import { dirname as dirname2 } from "path";
1535
- var MIGRATIONS_FOLDER2 = import.meta.dir + "/migrations";
1536
- function runMigrations(dbPath) {
1537
- const target = dbPath ?? resolveActiveProject().db;
1538
- mkdirSync3(dirname2(target), { recursive: true });
1539
- const sqlite = new Database2(target);
1540
- sqlite.exec("PRAGMA busy_timeout = 5000");
1541
- sqlite.exec("PRAGMA journal_mode = WAL");
1542
- sqlite.exec("PRAGMA foreign_keys = ON");
1543
- const db = drizzle2(sqlite);
1544
- migrate2(db, { migrationsFolder: MIGRATIONS_FOLDER2 });
1545
- sqlite.close();
1546
- }
1547
- if (false) {}
1548
-
1549
- // src/lib/projects/create.ts
1550
- function createProject(opts) {
1551
- registerProject({ slug: opts.slug, name: opts.name ?? opts.slug });
1552
- try {
1553
- const paths2 = projectPaths(opts.slug);
1554
- mkdirSync4(paths2.root, { recursive: true });
1555
- runMigrations(paths2.db);
1556
- } catch (err) {
1557
- try {
1558
- removeProject(opts.slug);
1559
- } catch {}
1560
- throw err;
1561
- }
1562
- }
1563
-
1564
- // src/tui/screens/startup/index.tsx
1565
- import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
1566
- function Startup({
1567
- skipProjectPicker,
1568
- initialProjectSlug,
1569
- onSelect
1570
- }) {
1571
- const { exit } = useTui();
1572
- const [activeSlug, setActiveSlug] = useState4(skipProjectPicker ? initialProjectSlug : null);
1573
- const finish = (s) => {
1574
- onSelect(s);
1575
- exit();
1576
- };
1577
- const handleProjectSelect = (s) => {
1578
- switch (s.type) {
1579
- case "quit":
1580
- finish({ type: "quit" });
1581
- return;
1582
- case "create":
1583
- createProject({ slug: s.slug });
1584
- setActiveProjectSlug(s.slug);
1585
- _resetActiveProjectCache();
1586
- setActiveSlug(s.slug);
1587
- return;
1588
- case "select":
1589
- setActiveProjectSlug(s.slug);
1590
- _resetActiveProjectCache();
1591
- setActiveSlug(s.slug);
1592
- return;
1593
- }
1594
- };
1595
- const handleLaunchSelect = (s) => {
1596
- if (s.type === "quit" || activeSlug === null) {
1597
- finish({ type: "quit" });
1598
- return;
1599
- }
1600
- if (s.type === "create") {
1601
- finish({
1602
- type: "create",
1603
- projectSlug: activeSlug,
1604
- categoryPath: s.categoryPath,
1605
- slug: s.slug
1606
- });
1607
- return;
1608
- }
1609
- finish({ type: "pick", projectSlug: activeSlug, sessionId: s.sessionId });
1610
- };
1611
- if (activeSlug === null) {
1612
- return /* @__PURE__ */ jsxDEV8(ProjectPicker, {
1613
- onSelect: handleProjectSelect
1614
- }, undefined, false, undefined, this);
1615
- }
1616
- return /* @__PURE__ */ jsxDEV8(Launch, {
1617
- onSelect: handleLaunchSelect
1618
- }, undefined, false, undefined, this);
1619
- }
1620
-
1621
1301
  // src/tui/screens/Exit.tsx
1622
- import { useState as useState5 } from "react";
1623
- import { Box as Box7, Text as Text8, useInput as useInput3, useTui as useTui2 } from "@orchetron/storm";
1302
+ import { useState as useState4 } from "react";
1303
+ import { Box as Box6, Text as Text7, useInput as useInput3, useTui as useTui2 } from "@orchetron/storm";
1624
1304
 
1625
1305
  // src/db/queries/conversations.ts
1626
1306
  import { eq as eq2, and as and2, desc, sql as sql3 } from "drizzle-orm";
@@ -1629,7 +1309,7 @@ function getConversationsBySession(sessionId) {
1629
1309
  }
1630
1310
 
1631
1311
  // src/tui/screens/Exit.tsx
1632
- import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
1312
+ import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
1633
1313
  var OPTIONS = [
1634
1314
  { action: "save", label: "Save", hint: "Keep session paused for later" },
1635
1315
  {
@@ -1645,12 +1325,12 @@ var OPTIONS = [
1645
1325
  {
1646
1326
  action: "resume",
1647
1327
  label: "Resume",
1648
- hint: "Start a new conversation in this session"
1328
+ hint: "Continue a conversation or start a new one"
1649
1329
  }
1650
1330
  ];
1651
1331
  function Exit({ sessionId, onAction }) {
1652
1332
  const { exit } = useTui2();
1653
- const [cursor, setCursor] = useState5(0);
1333
+ const [cursor, setCursor] = useState4(0);
1654
1334
  const session = getSession(sessionId);
1655
1335
  const conversations2 = session ? getConversationsBySession(session.id) : [];
1656
1336
  useInput3((e) => {
@@ -1670,37 +1350,37 @@ function Exit({ sessionId, onAction }) {
1670
1350
  }
1671
1351
  });
1672
1352
  if (!session) {
1673
- return /* @__PURE__ */ jsxDEV9(Text8, {
1353
+ return /* @__PURE__ */ jsxDEV7(Text7, {
1674
1354
  color: "red",
1675
1355
  children: "Session not found"
1676
1356
  }, undefined, false, undefined, this);
1677
1357
  }
1678
1358
  const duration = session.endedAt && session.startedAt ? formatDuration(new Date(session.endedAt).getTime() - new Date(session.startedAt).getTime()) : null;
1679
- return /* @__PURE__ */ jsxDEV9(Box7, {
1359
+ return /* @__PURE__ */ jsxDEV7(Box6, {
1680
1360
  flexDirection: "column",
1681
1361
  padding: 1,
1682
1362
  gap: 1,
1683
1363
  children: [
1684
- /* @__PURE__ */ jsxDEV9(Text8, {
1364
+ /* @__PURE__ */ jsxDEV7(Text7, {
1685
1365
  bold: true,
1686
1366
  color: "#82AAFF",
1687
1367
  children: "Session ended"
1688
1368
  }, undefined, false, undefined, this),
1689
- /* @__PURE__ */ jsxDEV9(Box7, {
1369
+ /* @__PURE__ */ jsxDEV7(Box6, {
1690
1370
  flexDirection: "column",
1691
1371
  children: [
1692
- /* @__PURE__ */ jsxDEV9(Box7, {
1372
+ /* @__PURE__ */ jsxDEV7(Box6, {
1693
1373
  flexDirection: "row",
1694
1374
  gap: 1,
1695
1375
  children: [
1696
- /* @__PURE__ */ jsxDEV9(StatusDot, {
1376
+ /* @__PURE__ */ jsxDEV7(StatusDot, {
1697
1377
  status: session.status
1698
1378
  }, undefined, false, undefined, this),
1699
- /* @__PURE__ */ jsxDEV9(Text8, {
1379
+ /* @__PURE__ */ jsxDEV7(Text7, {
1700
1380
  bold: true,
1701
1381
  children: session.name
1702
1382
  }, undefined, false, undefined, this),
1703
- duration && /* @__PURE__ */ jsxDEV9(Text8, {
1383
+ duration && /* @__PURE__ */ jsxDEV7(Text7, {
1704
1384
  dim: true,
1705
1385
  children: [
1706
1386
  "(",
@@ -1710,7 +1390,7 @@ function Exit({ sessionId, onAction }) {
1710
1390
  }, undefined, true, undefined, this)
1711
1391
  ]
1712
1392
  }, undefined, true, undefined, this),
1713
- /* @__PURE__ */ jsxDEV9(Text8, {
1393
+ /* @__PURE__ */ jsxDEV7(Text7, {
1714
1394
  dim: true,
1715
1395
  children: [
1716
1396
  conversations2.length,
@@ -1720,28 +1400,28 @@ function Exit({ sessionId, onAction }) {
1720
1400
  }, undefined, true, undefined, this)
1721
1401
  ]
1722
1402
  }, undefined, true, undefined, this),
1723
- /* @__PURE__ */ jsxDEV9(Box7, {
1403
+ /* @__PURE__ */ jsxDEV7(Box6, {
1724
1404
  flexDirection: "column",
1725
- children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV9(Box7, {
1405
+ children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV7(Box6, {
1726
1406
  flexDirection: "row",
1727
1407
  gap: 1,
1728
1408
  children: [
1729
- /* @__PURE__ */ jsxDEV9(Text8, {
1409
+ /* @__PURE__ */ jsxDEV7(Text7, {
1730
1410
  children: i === cursor ? "\u276F" : " "
1731
1411
  }, undefined, false, undefined, this),
1732
- /* @__PURE__ */ jsxDEV9(Text8, {
1412
+ /* @__PURE__ */ jsxDEV7(Text7, {
1733
1413
  bold: i === cursor,
1734
1414
  children: opt.label
1735
1415
  }, undefined, false, undefined, this),
1736
- /* @__PURE__ */ jsxDEV9(Text8, {
1416
+ /* @__PURE__ */ jsxDEV7(Text7, {
1737
1417
  dim: true,
1738
1418
  children: opt.hint
1739
1419
  }, undefined, false, undefined, this)
1740
1420
  ]
1741
1421
  }, opt.action, true, undefined, this))
1742
1422
  }, undefined, false, undefined, this),
1743
- /* @__PURE__ */ jsxDEV9(Box7, {
1744
- children: /* @__PURE__ */ jsxDEV9(Text8, {
1423
+ /* @__PURE__ */ jsxDEV7(Box6, {
1424
+ children: /* @__PURE__ */ jsxDEV7(Text7, {
1745
1425
  dim: true,
1746
1426
  children: "\u2191\u2193 navigate \xB7 enter select \xB7 q save & quit"
1747
1427
  }, undefined, false, undefined, this)
@@ -1751,12 +1431,12 @@ function Exit({ sessionId, onAction }) {
1751
1431
  }
1752
1432
 
1753
1433
  // src/tui/screens/Resume.tsx
1754
- import { useState as useState6 } from "react";
1755
- import { Box as Box8, Text as Text9, useInput as useInput4, useTui as useTui3 } from "@orchetron/storm";
1756
- import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
1434
+ import { useState as useState5 } from "react";
1435
+ import { Box as Box7, Text as Text8, useInput as useInput4, useTui as useTui3 } from "@orchetron/storm";
1436
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
1757
1437
  function Resume({ sessionId, onSelect }) {
1758
1438
  const { exit } = useTui3();
1759
- const [cursor, setCursor] = useState6(0);
1439
+ const [cursor, setCursor] = useState5(0);
1760
1440
  const session = getSession(sessionId);
1761
1441
  const conversations2 = getConversationsBySession(sessionId);
1762
1442
  const totalOptions = conversations2.length + 1;
@@ -1783,17 +1463,17 @@ function Resume({ sessionId, onSelect }) {
1783
1463
  }
1784
1464
  });
1785
1465
  if (!session) {
1786
- return /* @__PURE__ */ jsxDEV10(Text9, {
1466
+ return /* @__PURE__ */ jsxDEV8(Text8, {
1787
1467
  color: "red",
1788
1468
  children: "Session not found"
1789
1469
  }, undefined, false, undefined, this);
1790
1470
  }
1791
- return /* @__PURE__ */ jsxDEV10(Box8, {
1471
+ return /* @__PURE__ */ jsxDEV8(Box7, {
1792
1472
  flexDirection: "column",
1793
1473
  padding: 1,
1794
1474
  gap: 1,
1795
1475
  children: [
1796
- /* @__PURE__ */ jsxDEV10(Text9, {
1476
+ /* @__PURE__ */ jsxDEV8(Text8, {
1797
1477
  bold: true,
1798
1478
  color: "#82AAFF",
1799
1479
  children: [
@@ -1801,17 +1481,17 @@ function Resume({ sessionId, onSelect }) {
1801
1481
  session.name
1802
1482
  ]
1803
1483
  }, undefined, true, undefined, this),
1804
- /* @__PURE__ */ jsxDEV10(Box8, {
1484
+ /* @__PURE__ */ jsxDEV8(Box7, {
1805
1485
  flexDirection: "column",
1806
1486
  children: [
1807
- /* @__PURE__ */ jsxDEV10(Box8, {
1487
+ /* @__PURE__ */ jsxDEV8(Box7, {
1808
1488
  flexDirection: "row",
1809
1489
  gap: 1,
1810
1490
  children: [
1811
- /* @__PURE__ */ jsxDEV10(Text9, {
1491
+ /* @__PURE__ */ jsxDEV8(Text8, {
1812
1492
  children: cursor === 0 ? "\u276F" : " "
1813
1493
  }, undefined, false, undefined, this),
1814
- /* @__PURE__ */ jsxDEV10(Text9, {
1494
+ /* @__PURE__ */ jsxDEV8(Text8, {
1815
1495
  bold: cursor === 0,
1816
1496
  color: "#34D399",
1817
1497
  children: "+ New conversation"
@@ -1823,42 +1503,42 @@ function Resume({ sessionId, onSelect }) {
1823
1503
  const isSelected = cursor === idx;
1824
1504
  const duration = conv.endedAt ? formatDuration(new Date(conv.endedAt).getTime() - new Date(conv.startedAt).getTime()) : "active";
1825
1505
  const ago = formatAgo(conv.startedAt);
1826
- return /* @__PURE__ */ jsxDEV10(Box8, {
1506
+ return /* @__PURE__ */ jsxDEV8(Box7, {
1827
1507
  flexDirection: "row",
1828
1508
  gap: 1,
1829
1509
  children: [
1830
- /* @__PURE__ */ jsxDEV10(Text9, {
1510
+ /* @__PURE__ */ jsxDEV8(Text8, {
1831
1511
  children: isSelected ? "\u276F" : " "
1832
1512
  }, undefined, false, undefined, this),
1833
- /* @__PURE__ */ jsxDEV10(Text9, {
1513
+ /* @__PURE__ */ jsxDEV8(Text8, {
1834
1514
  bold: isSelected,
1835
1515
  dim: conv.discarded,
1836
1516
  children: conv.id.slice(0, 8)
1837
1517
  }, undefined, false, undefined, this),
1838
- /* @__PURE__ */ jsxDEV10(Text9, {
1518
+ /* @__PURE__ */ jsxDEV8(Text8, {
1839
1519
  dim: true,
1840
1520
  children: ago
1841
1521
  }, undefined, false, undefined, this),
1842
- /* @__PURE__ */ jsxDEV10(Text9, {
1522
+ /* @__PURE__ */ jsxDEV8(Text8, {
1843
1523
  dim: true,
1844
1524
  children: "\xB7"
1845
1525
  }, undefined, false, undefined, this),
1846
- /* @__PURE__ */ jsxDEV10(Text9, {
1526
+ /* @__PURE__ */ jsxDEV8(Text8, {
1847
1527
  dim: true,
1848
1528
  children: [
1849
1529
  conv.eventCount,
1850
1530
  " events"
1851
1531
  ]
1852
1532
  }, undefined, true, undefined, this),
1853
- /* @__PURE__ */ jsxDEV10(Text9, {
1533
+ /* @__PURE__ */ jsxDEV8(Text8, {
1854
1534
  dim: true,
1855
1535
  children: "\xB7"
1856
1536
  }, undefined, false, undefined, this),
1857
- /* @__PURE__ */ jsxDEV10(Text9, {
1537
+ /* @__PURE__ */ jsxDEV8(Text8, {
1858
1538
  dim: true,
1859
1539
  children: duration
1860
1540
  }, undefined, false, undefined, this),
1861
- conv.discarded && /* @__PURE__ */ jsxDEV10(Text9, {
1541
+ conv.discarded && /* @__PURE__ */ jsxDEV8(Text8, {
1862
1542
  color: "red",
1863
1543
  dim: true,
1864
1544
  children: "(discarded)"
@@ -1868,8 +1548,8 @@ function Resume({ sessionId, onSelect }) {
1868
1548
  })
1869
1549
  ]
1870
1550
  }, undefined, true, undefined, this),
1871
- /* @__PURE__ */ jsxDEV10(Box8, {
1872
- children: /* @__PURE__ */ jsxDEV10(Text9, {
1551
+ /* @__PURE__ */ jsxDEV8(Box7, {
1552
+ children: /* @__PURE__ */ jsxDEV8(Text8, {
1873
1553
  dim: true,
1874
1554
  children: "\u2191\u2193 navigate \xB7 enter select \xB7 q back"
1875
1555
  }, undefined, false, undefined, this)
@@ -1878,70 +1558,319 @@ function Resume({ sessionId, onSelect }) {
1878
1558
  }, undefined, true, undefined, this);
1879
1559
  }
1880
1560
 
1561
+ // src/tui/screens/project-picker/index.tsx
1562
+ import { useMemo as useMemo3 } from "react";
1563
+ import { Box as Box8, Text as Text9, useTui as useTui4 } from "@orchetron/storm";
1564
+ import { existsSync as existsSync2 } from "fs";
1565
+ import { jsxDEV as jsxDEV9, Fragment as Fragment2 } from "react/jsx-dev-runtime";
1566
+ var SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
1567
+ function statsFor(slug) {
1568
+ const dbFile = projectPaths(slug).db;
1569
+ if (!existsSync2(dbFile))
1570
+ return { total: 0, active: 0, unreadable: false };
1571
+ try {
1572
+ const db = getDbForProject(slug);
1573
+ const all = db.select({ status: sessions.status }).from(sessions).all();
1574
+ return {
1575
+ total: all.length,
1576
+ active: all.filter((s) => s.status === "active" || s.status === "waiting").length,
1577
+ unreadable: false
1578
+ };
1579
+ } catch {
1580
+ return { total: 0, active: 0, unreadable: true };
1581
+ }
1582
+ }
1583
+ function projectRow(entry, isActive) {
1584
+ const stats = statsFor(entry.slug);
1585
+ const sessionsLabel = stats.unreadable ? "?" : `${stats.active}/${stats.total}`;
1586
+ const lastUsed = formatAgo(entry.lastUsedAt);
1587
+ return {
1588
+ value: entry.slug,
1589
+ label: `${entry.slug} ${entry.name} ${sessionsLabel}`,
1590
+ meta: lastUsed,
1591
+ display: (isCursor) => {
1592
+ const cursorColor = "green";
1593
+ const slugColor = isCursor ? cursorColor : undefined;
1594
+ const nameColor = isCursor ? cursorColor : undefined;
1595
+ return /* @__PURE__ */ jsxDEV9(Fragment2, {
1596
+ children: [
1597
+ /* @__PURE__ */ jsxDEV9(Text9, {
1598
+ color: cursorColor,
1599
+ bold: true,
1600
+ children: isCursor ? "\u276F " : " "
1601
+ }, undefined, false, undefined, this),
1602
+ /* @__PURE__ */ jsxDEV9(Text9, {
1603
+ bold: isActive,
1604
+ color: isActive ? "gold" : slugColor,
1605
+ children: isActive ? "\u25CF " : " "
1606
+ }, undefined, false, undefined, this),
1607
+ /* @__PURE__ */ jsxDEV9(Text9, {
1608
+ color: slugColor,
1609
+ bold: isCursor || isActive,
1610
+ children: entry.slug.padEnd(16)
1611
+ }, undefined, false, undefined, this),
1612
+ /* @__PURE__ */ jsxDEV9(Text9, {
1613
+ color: nameColor,
1614
+ dim: !isCursor,
1615
+ children: entry.name.padEnd(20)
1616
+ }, undefined, false, undefined, this),
1617
+ /* @__PURE__ */ jsxDEV9(Text9, {
1618
+ dim: true,
1619
+ children: stats.unreadable ? " (unreadable)" : ` ${sessionsLabel} active/total`
1620
+ }, undefined, false, undefined, this)
1621
+ ]
1622
+ }, undefined, true, undefined, this);
1623
+ }
1624
+ };
1625
+ }
1626
+ function ProjectPicker({ onSelect }) {
1627
+ const { exit } = useTui4();
1628
+ const allProjects = useMemo3(() => listProjects(), []);
1629
+ const activeSlug = useMemo3(() => getActiveProjectSlug(), []);
1630
+ const items = useMemo3(() => {
1631
+ return allProjects.slice().sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt)).map((p) => projectRow(p, p.slug === activeSlug));
1632
+ }, [allProjects, activeSlug]);
1633
+ const suggestions = useMemo3(() => allProjects.map((p) => p.slug), [allProjects]);
1634
+ const knownSlugs = useMemo3(() => new Set(allProjects.map((p) => p.slug)), [allProjects]);
1635
+ const select = (selection) => {
1636
+ onSelect(selection);
1637
+ exit();
1638
+ };
1639
+ const handleSubmit = (value) => {
1640
+ const trimmed = value.trim();
1641
+ if (!trimmed)
1642
+ return;
1643
+ if (knownSlugs.has(trimmed)) {
1644
+ select({ type: "select", slug: trimmed });
1645
+ return;
1646
+ }
1647
+ if (!SLUG_PATTERN.test(trimmed)) {
1648
+ return;
1649
+ }
1650
+ select({ type: "create", slug: trimmed });
1651
+ };
1652
+ return /* @__PURE__ */ jsxDEV9(Box8, {
1653
+ flexDirection: "column",
1654
+ paddingY: 1,
1655
+ gap: 1,
1656
+ children: [
1657
+ /* @__PURE__ */ jsxDEV9(Box8, {
1658
+ marginX: 1,
1659
+ children: /* @__PURE__ */ jsxDEV9(Logo, {}, undefined, false, undefined, this)
1660
+ }, undefined, false, undefined, this),
1661
+ /* @__PURE__ */ jsxDEV9(Box8, {
1662
+ flexDirection: "column",
1663
+ marginX: 2,
1664
+ gap: 1,
1665
+ children: [
1666
+ /* @__PURE__ */ jsxDEV9(AppDetails, {}, undefined, false, undefined, this),
1667
+ /* @__PURE__ */ jsxDEV9(Box8, {
1668
+ flexDirection: "column",
1669
+ gap: 1,
1670
+ children: [
1671
+ /* @__PURE__ */ jsxDEV9(Box8, {
1672
+ flexDirection: "row",
1673
+ gap: 1,
1674
+ children: [
1675
+ /* @__PURE__ */ jsxDEV9(Text9, {
1676
+ bold: true,
1677
+ children: "Projects"
1678
+ }, undefined, false, undefined, this),
1679
+ allProjects.length > 0 ? /* @__PURE__ */ jsxDEV9(Fragment2, {
1680
+ children: [
1681
+ /* @__PURE__ */ jsxDEV9(Text9, {
1682
+ dim: true,
1683
+ children: "\xB7"
1684
+ }, undefined, false, undefined, this),
1685
+ /* @__PURE__ */ jsxDEV9(Text9, {
1686
+ dim: true,
1687
+ children: [
1688
+ allProjects.length,
1689
+ " total",
1690
+ activeSlug ? ` \xB7 active: ${activeSlug}` : ""
1691
+ ]
1692
+ }, undefined, true, undefined, this)
1693
+ ]
1694
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV9(Text9, {
1695
+ dim: true,
1696
+ children: "\xB7 none \u2014 type a slug to create your first project"
1697
+ }, undefined, false, undefined, this)
1698
+ ]
1699
+ }, undefined, true, undefined, this),
1700
+ /* @__PURE__ */ jsxDEV9(Picker, {
1701
+ mode: "single",
1702
+ items,
1703
+ isFocused: true,
1704
+ maxVisible: 16,
1705
+ suggest: suggestions,
1706
+ placeholder: "Filter or type a new slug to create\u2026",
1707
+ emptyHint: "No projects. Type a slug to create one.",
1708
+ onSubmit: handleSubmit,
1709
+ onKey: (e) => {
1710
+ if (e.key === "c" && e.ctrl) {
1711
+ select({ type: "quit" });
1712
+ }
1713
+ }
1714
+ }, undefined, false, undefined, this),
1715
+ /* @__PURE__ */ jsxDEV9(Text9, {
1716
+ dim: true,
1717
+ children: "\u2191\u2193 navigate \xB7 enter select \xB7 type to filter or create new \xB7 ctrl+c quit"
1718
+ }, undefined, false, undefined, this)
1719
+ ]
1720
+ }, undefined, true, undefined, this)
1721
+ ]
1722
+ }, undefined, true, undefined, this)
1723
+ ]
1724
+ }, undefined, true, undefined, this);
1725
+ }
1726
+
1881
1727
  // src/tui/run-screen.tsx
1882
- import { jsxDEV as jsxDEV11 } from "react/jsx-dev-runtime";
1728
+ import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
1883
1729
  var [, , screen, outputPath, ...args] = process.argv;
1884
1730
  if (!screen || !outputPath) {
1885
1731
  console.error("Usage: run-screen <screen> <outputPath> [args...]");
1886
1732
  process.exit(1);
1887
1733
  }
1734
+ var RESULT_PATH = outputPath;
1735
+ var DEBUG_PATH = process.env.BERTRAND_DEBUG_TUI || null;
1736
+ var START_HR = process.hrtime.bigint();
1737
+ function phase(name, detail) {
1738
+ if (!DEBUG_PATH)
1739
+ return;
1740
+ const elapsedMs = Number(process.hrtime.bigint() - START_HR) / 1e6;
1741
+ const line = `${elapsedMs.toFixed(2).padStart(8)}ms ${name}${detail ? " " + detail : ""}
1742
+ `;
1743
+ try {
1744
+ appendFileSync(DEBUG_PATH, line);
1745
+ } catch {}
1746
+ }
1747
+ phase("spawn", `screen=${screen} pid=${process.pid} isTTY=${process.stdout.isTTY ?? "?"} term=${process.env.TERM_PROGRAM || process.env.TERM || "?"}`);
1888
1748
  var result;
1889
- switch (screen) {
1890
- case "startup": {
1891
- const skipProjectPicker = args[0] === "true";
1892
- const initialProjectSlug = args[1] ?? "";
1893
- let selection = { type: "quit" };
1894
- const app = render(/* @__PURE__ */ jsxDEV11(Startup, {
1895
- skipProjectPicker,
1896
- initialProjectSlug,
1897
- onSelect: (s) => {
1898
- selection = s;
1899
- }
1900
- }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true });
1901
- await app.waitUntilExit();
1902
- app.unmount();
1903
- result = selection;
1904
- break;
1905
- }
1906
- case "exit": {
1907
- const sessionId = args[0];
1908
- if (!sessionId) {
1909
- console.error("exit requires sessionId");
1910
- process.exit(1);
1749
+ var resultWritten = false;
1750
+ function writeResult() {
1751
+ if (resultWritten)
1752
+ return;
1753
+ resultWritten = true;
1754
+ try {
1755
+ writeFileSync2(RESULT_PATH, JSON.stringify(result));
1756
+ } catch {}
1757
+ }
1758
+ process.on("SIGINT", writeResult);
1759
+ process.on("SIGTERM", writeResult);
1760
+ process.on("SIGHUP", writeResult);
1761
+ process.on("uncaughtException", writeResult);
1762
+ process.on("unhandledRejection", writeResult);
1763
+ var renderCount = 0;
1764
+ var onRender = (m) => {
1765
+ renderCount++;
1766
+ phase(`paint #${renderCount}`, `cells=${m.cellsChanged} took=${m.renderTime.toFixed(2)}ms`);
1767
+ };
1768
+ try {
1769
+ switch (screen) {
1770
+ case "launch": {
1771
+ let selection = { type: "quit" };
1772
+ result = selection;
1773
+ phase("render() pre");
1774
+ const app = render(/* @__PURE__ */ jsxDEV10(Launch, {
1775
+ onSelect: (s) => {
1776
+ selection = s;
1777
+ result = selection;
1778
+ }
1779
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1780
+ phase("render() post");
1781
+ app.screen.invalidate();
1782
+ app.requestRepaint();
1783
+ phase("post-mount invalidate+requestRepaint");
1784
+ await app.waitUntilExit();
1785
+ phase("waitUntilExit returned");
1786
+ app.unmount();
1787
+ phase("unmount done");
1788
+ result = selection;
1789
+ break;
1911
1790
  }
1912
- let action = "save";
1913
- const app = render(/* @__PURE__ */ jsxDEV11(Exit, {
1914
- sessionId,
1915
- onAction: (a) => {
1916
- action = a;
1791
+ case "project-picker": {
1792
+ let selection = { type: "quit" };
1793
+ result = selection;
1794
+ phase("render() pre");
1795
+ const app = render(/* @__PURE__ */ jsxDEV10(ProjectPicker, {
1796
+ onSelect: (s) => {
1797
+ selection = s;
1798
+ result = selection;
1799
+ }
1800
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1801
+ phase("render() post");
1802
+ app.screen.invalidate();
1803
+ app.requestRepaint();
1804
+ phase("post-mount invalidate+requestRepaint");
1805
+ await app.waitUntilExit();
1806
+ phase("waitUntilExit returned");
1807
+ app.unmount();
1808
+ phase("unmount done");
1809
+ result = selection;
1810
+ break;
1811
+ }
1812
+ case "exit": {
1813
+ const sessionId = args[0];
1814
+ if (!sessionId) {
1815
+ console.error("exit requires sessionId");
1816
+ process.exit(1);
1917
1817
  }
1918
- }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true });
1919
- await app.waitUntilExit();
1920
- app.unmount();
1921
- result = action;
1922
- break;
1923
- }
1924
- case "resume": {
1925
- const sessionId = args[0];
1926
- if (!sessionId) {
1927
- console.error("resume requires sessionId");
1928
- process.exit(1);
1818
+ let action = "save";
1819
+ result = action;
1820
+ phase("render() pre");
1821
+ const app = render(/* @__PURE__ */ jsxDEV10(Exit, {
1822
+ sessionId,
1823
+ onAction: (a) => {
1824
+ action = a;
1825
+ result = action;
1826
+ }
1827
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1828
+ phase("render() post");
1829
+ app.screen.invalidate();
1830
+ app.requestRepaint();
1831
+ phase("post-mount invalidate+requestRepaint");
1832
+ await app.waitUntilExit();
1833
+ phase("waitUntilExit returned");
1834
+ app.unmount();
1835
+ phase("unmount done");
1836
+ result = action;
1837
+ break;
1929
1838
  }
1930
- let selection = { type: "back" };
1931
- const app = render(/* @__PURE__ */ jsxDEV11(Resume, {
1932
- sessionId,
1933
- onSelect: (s) => {
1934
- selection = s;
1839
+ case "resume": {
1840
+ const sessionId = args[0];
1841
+ if (!sessionId) {
1842
+ console.error("resume requires sessionId");
1843
+ process.exit(1);
1935
1844
  }
1936
- }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true });
1937
- await app.waitUntilExit();
1938
- app.unmount();
1939
- result = selection;
1940
- break;
1845
+ let selection = { type: "back" };
1846
+ result = selection;
1847
+ phase("render() pre");
1848
+ const app = render(/* @__PURE__ */ jsxDEV10(Resume, {
1849
+ sessionId,
1850
+ onSelect: (s) => {
1851
+ selection = s;
1852
+ result = selection;
1853
+ }
1854
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1855
+ phase("render() post");
1856
+ app.screen.invalidate();
1857
+ app.requestRepaint();
1858
+ phase("post-mount invalidate+requestRepaint");
1859
+ await app.waitUntilExit();
1860
+ phase("waitUntilExit returned");
1861
+ app.unmount();
1862
+ phase("unmount done");
1863
+ result = selection;
1864
+ break;
1865
+ }
1866
+ default:
1867
+ console.error(`Unknown screen: ${screen}`);
1868
+ process.exit(1);
1941
1869
  }
1942
- default:
1943
- console.error(`Unknown screen: ${screen}`);
1944
- process.exit(1);
1870
+ writeResult();
1871
+ phase("result written");
1872
+ } finally {
1873
+ writeResult();
1874
+ phase("finally complete");
1945
1875
  }
1946
- writeFileSync2(outputPath, JSON.stringify(result));
1947
1876
  process.exit(0);