bertrand 0.18.0 → 0.20.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,12 +15,17 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // src/tui/run-screen.tsx
18
- import { writeFileSync } from "fs";
18
+ import { writeFileSync as writeFileSync2 } from "fs";
19
19
  import { render } from "@orchetron/storm";
20
20
 
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";
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";
24
29
 
25
30
  // src/tui/components/app-details.tsx
26
31
  import { Box, Text } from "@orchetron/storm";
@@ -560,14 +565,18 @@ function StatusDot({ status }) {
560
565
  children: "\u25CF"
561
566
  }, undefined, false, undefined, this);
562
567
  }
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 { mkdirSync } from "fs";
570
- import { dirname } from "path";
568
+ // src/lib/projects/registry.ts
569
+ import {
570
+ existsSync,
571
+ mkdirSync,
572
+ readFileSync,
573
+ readdirSync,
574
+ renameSync,
575
+ statSync,
576
+ writeFileSync
577
+ } from "fs";
578
+ import { randomBytes } from "crypto";
579
+ import { join as join2 } from "path";
571
580
 
572
581
  // src/lib/paths.ts
573
582
  import { homedir } from "os";
@@ -575,12 +584,197 @@ import { join } from "path";
575
584
  var BERTRAND_DIR = ".bertrand";
576
585
  var paths = {
577
586
  root: join(homedir(), BERTRAND_DIR),
578
- db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
579
587
  hooks: join(homedir(), BERTRAND_DIR, "hooks"),
580
588
  sessions: join(homedir(), BERTRAND_DIR, "sessions"),
589
+ db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
581
590
  syncEnv: join(homedir(), BERTRAND_DIR, "sync.env")
582
591
  };
583
592
 
593
+ // src/lib/projects/registry.ts
594
+ var DEFAULT_PROJECT_SLUG = "default";
595
+ var _registryDir = paths.root;
596
+ function _getRegistryDir() {
597
+ return _registryDir;
598
+ }
599
+ function registryPath() {
600
+ return join2(_registryDir, "projects.json");
601
+ }
602
+ function projectsDir() {
603
+ return join2(_registryDir, "projects");
604
+ }
605
+ function readRegistry() {
606
+ const path = registryPath();
607
+ if (!existsSync(path))
608
+ return null;
609
+ try {
610
+ const raw = readFileSync(path, "utf8");
611
+ const parsed = JSON.parse(raw);
612
+ if (!isRegistry(parsed))
613
+ return null;
614
+ return parsed;
615
+ } catch {
616
+ return null;
617
+ }
618
+ }
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
+ function recoverFromDisk() {
628
+ const dir = projectsDir();
629
+ if (!existsSync(dir))
630
+ return null;
631
+ let entries;
632
+ try {
633
+ entries = readdirSync(dir);
634
+ } catch {
635
+ return null;
636
+ }
637
+ const projects = [];
638
+ for (const slug of entries) {
639
+ const subdir = join2(dir, slug);
640
+ let mtime;
641
+ try {
642
+ const s = statSync(subdir);
643
+ if (!s.isDirectory())
644
+ continue;
645
+ mtime = new Date(s.mtimeMs);
646
+ } catch {
647
+ continue;
648
+ }
649
+ if (!existsSync(join2(subdir, "bertrand.db")))
650
+ continue;
651
+ projects.push({
652
+ slug,
653
+ name: slug,
654
+ createdAt: mtime.toISOString(),
655
+ lastUsedAt: mtime.toISOString()
656
+ });
657
+ }
658
+ if (projects.length === 0)
659
+ return null;
660
+ projects.sort((a, b) => a.slug.localeCompare(b.slug));
661
+ const envSlug = process.env.BERTRAND_PROJECT;
662
+ const hasEnv = !!envSlug && projects.some((p) => p.slug === envSlug);
663
+ const hasDefault = projects.some((p) => p.slug === DEFAULT_PROJECT_SLUG);
664
+ const activeProjectSlug = hasEnv ? envSlug : hasDefault ? DEFAULT_PROJECT_SLUG : projects[0].slug;
665
+ return { activeProjectSlug, projects };
666
+ }
667
+ function loadRegistry() {
668
+ return readRegistry() ?? recoverFromDisk();
669
+ }
670
+ function listProjects() {
671
+ return loadRegistry()?.projects ?? [];
672
+ }
673
+ function getActiveProjectSlug() {
674
+ return loadRegistry()?.activeProjectSlug ?? DEFAULT_PROJECT_SLUG;
675
+ }
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
+ function isRegistry(value) {
723
+ if (typeof value !== "object" || value === null)
724
+ return false;
725
+ const r = value;
726
+ if (typeof r.activeProjectSlug !== "string")
727
+ return false;
728
+ if (!Array.isArray(r.projects))
729
+ return false;
730
+ return r.projects.every((p) => {
731
+ if (typeof p !== "object" || p === null)
732
+ return false;
733
+ const e = p;
734
+ return typeof e.slug === "string" && typeof e.name === "string" && typeof e.createdAt === "string" && typeof e.lastUsedAt === "string";
735
+ });
736
+ }
737
+
738
+ // src/lib/projects/paths.ts
739
+ import { join as join3 } from "path";
740
+ function projectPaths(slug) {
741
+ const root = join3(_getRegistryDir(), "projects", slug);
742
+ return {
743
+ root,
744
+ db: join3(root, "bertrand.db"),
745
+ syncEnv: join3(root, "sync.env"),
746
+ snapshots: join3(root, "snapshots")
747
+ };
748
+ }
749
+
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
+ // src/lib/projects/resolve.ts
758
+ var _cached = null;
759
+ function resolveActiveProject() {
760
+ if (_cached)
761
+ return _cached;
762
+ const envSlug = process.env.BERTRAND_PROJECT;
763
+ let slug;
764
+ if (envSlug && envSlug.trim()) {
765
+ slug = envSlug.trim();
766
+ } else {
767
+ slug = getActiveProjectSlug();
768
+ }
769
+ const entry = listProjects().find((p) => p.slug === slug);
770
+ const name = entry?.name ?? slug;
771
+ _cached = { slug, name, ...projectPaths(slug) };
772
+ return _cached;
773
+ }
774
+ function _resetActiveProjectCache() {
775
+ _cached = null;
776
+ }
777
+
584
778
  // src/db/schema.ts
585
779
  var exports_schema = {};
586
780
  __export(exports_schema, {
@@ -589,15 +783,15 @@ __export(exports_schema, {
589
783
  sessionStats: () => sessionStats,
590
784
  sessionLabels: () => sessionLabels,
591
785
  labels: () => labels,
592
- groups: () => groups,
593
786
  events: () => events,
594
- conversations: () => conversations
787
+ conversations: () => conversations,
788
+ categories: () => categories
595
789
  });
596
790
  import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core";
597
791
  import { sql } from "drizzle-orm";
598
- var groups = sqliteTable("groups", {
792
+ var categories = sqliteTable("categories", {
599
793
  id: text("id").primaryKey(),
600
- parentId: text("parent_id").references(() => groups.id, {
794
+ parentId: text("parent_id").references(() => categories.id, {
601
795
  onDelete: "cascade"
602
796
  }),
603
797
  slug: text("slug").notNull(),
@@ -607,8 +801,8 @@ var groups = sqliteTable("groups", {
607
801
  color: text("color"),
608
802
  createdAt: text("created_at").notNull().default(sql`(datetime('now'))`)
609
803
  }, (t) => [
610
- uniqueIndex("groups_parent_slug").on(t.parentId, t.slug),
611
- index("groups_path").on(t.path)
804
+ uniqueIndex("categories_parent_slug").on(t.parentId, t.slug),
805
+ index("categories_path").on(t.path)
612
806
  ]);
613
807
  var labels = sqliteTable("labels", {
614
808
  id: text("id").primaryKey(),
@@ -618,7 +812,7 @@ var labels = sqliteTable("labels", {
618
812
  });
619
813
  var sessions = sqliteTable("sessions", {
620
814
  id: text("id").primaryKey(),
621
- groupId: text("group_id").notNull().references(() => groups.id, { onDelete: "cascade" }),
815
+ categoryId: text("category_id").notNull().references(() => categories.id, { onDelete: "cascade" }),
622
816
  slug: text("slug").notNull(),
623
817
  name: text("name").notNull(),
624
818
  status: text("status", {
@@ -631,7 +825,7 @@ var sessions = sqliteTable("sessions", {
631
825
  createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
632
826
  updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`)
633
827
  }, (t) => [
634
- uniqueIndex("sessions_group_slug").on(t.groupId, t.slug),
828
+ uniqueIndex("sessions_category_slug").on(t.categoryId, t.slug),
635
829
  index("sessions_status").on(t.status),
636
830
  index("sessions_started").on(t.startedAt)
637
831
  ]);
@@ -694,21 +888,255 @@ var sessionStats = sqliteTable("session_stats", {
694
888
  });
695
889
 
696
890
  // src/db/client.ts
697
- var _db = null;
891
+ var MIGRATIONS_FOLDER = import.meta.dir + "/migrations";
892
+ var _cache = new Map;
893
+ var _migrated = new Set;
894
+ var _testDb = null;
698
895
  function getDb() {
699
- if (_db)
700
- return _db;
701
- mkdirSync(dirname(paths.db), { recursive: true });
702
- const sqlite = new Database(paths.db);
896
+ if (_testDb)
897
+ return _testDb;
898
+ return openDb(resolveActiveProject().db);
899
+ }
900
+ function getDbForProject(slug) {
901
+ if (_testDb)
902
+ return _testDb;
903
+ return openDb(projectPaths(slug).db);
904
+ }
905
+ function openDb(dbPath) {
906
+ const cached = _cache.get(dbPath);
907
+ if (cached)
908
+ return cached;
909
+ mkdirSync2(dirname(dbPath), { recursive: true });
910
+ const sqlite = new Database(dbPath);
911
+ sqlite.exec("PRAGMA busy_timeout = 5000");
703
912
  sqlite.exec("PRAGMA journal_mode = WAL");
704
913
  sqlite.exec("PRAGMA foreign_keys = ON");
705
914
  sqlite.exec("PRAGMA synchronous = NORMAL");
706
915
  sqlite.exec("PRAGMA cache_size = -8000");
707
916
  sqlite.exec("PRAGMA temp_store = MEMORY");
708
- _db = drizzle(sqlite, { schema: exports_schema });
709
- return _db;
917
+ const db = drizzle(sqlite, { schema: exports_schema });
918
+ if (!_migrated.has(dbPath)) {
919
+ try {
920
+ migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
921
+ if (!hasSessionsTable(sqlite)) {
922
+ sqlite.exec("DROP TABLE IF EXISTS __drizzle_migrations");
923
+ migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
924
+ }
925
+ } catch (err) {
926
+ sqlite.close();
927
+ throw err;
928
+ }
929
+ _migrated.add(dbPath);
930
+ }
931
+ _cache.set(dbPath, db);
932
+ return db;
933
+ }
934
+ function hasSessionsTable(sqlite) {
935
+ const row = sqlite.query("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'").get();
936
+ return row !== null;
937
+ }
938
+
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);
710
1131
  }
711
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
+
712
1140
  // src/lib/id.ts
713
1141
  import { nanoid } from "nanoid";
714
1142
 
@@ -718,7 +1146,7 @@ function getSession(id) {
718
1146
  }
719
1147
  function getAllSessions(opts) {
720
1148
  const db = getDb();
721
- const query = db.select({ session: sessions, groupPath: groups.path }).from(sessions).innerJoin(groups, eq(sessions.groupId, groups.id));
1149
+ const query = db.select({ session: sessions, categoryPath: categories.path }).from(sessions).innerJoin(categories, eq(sessions.categoryId, categories.id));
722
1150
  if (opts?.excludeArchived) {
723
1151
  return query.where(inArray(sessions.status, [
724
1152
  "active",
@@ -758,39 +1186,6 @@ function unarchiveSession(id) {
758
1186
  return { ok: true, session: updated };
759
1187
  }
760
1188
 
761
- // src/lib/format.ts
762
- var SECOND = 1000;
763
- var MINUTE = 60 * SECOND;
764
- var HOUR = 60 * MINUTE;
765
- var DAY = 24 * HOUR;
766
- function formatDuration(ms) {
767
- if (ms < MINUTE)
768
- return `${Math.round(ms / SECOND)}s`;
769
- const days = Math.floor(ms / DAY);
770
- const hours = Math.floor(ms % DAY / HOUR);
771
- const minutes = Math.floor(ms % HOUR / MINUTE);
772
- if (days > 0)
773
- return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
774
- if (hours > 0)
775
- return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
776
- return `${minutes}m`;
777
- }
778
- function formatAgo(isoOrDate) {
779
- const date = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
780
- const ms = Date.now() - date.getTime();
781
- if (ms < MINUTE)
782
- return "just now";
783
- if (ms < HOUR)
784
- return `${Math.floor(ms / MINUTE)}m ago`;
785
- if (ms < DAY)
786
- return `${Math.floor(ms / HOUR)}h ago`;
787
- if (ms < 2 * DAY)
788
- return "yesterday";
789
- if (ms < 7 * DAY)
790
- return `${Math.floor(ms / DAY)}d ago`;
791
- return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
792
- }
793
-
794
1189
  // src/lib/parse-session-name.ts
795
1190
  function parseSessionName(input) {
796
1191
  const trimmed = input.trim().replace(/^\/+|\/+$/g, "");
@@ -799,7 +1194,7 @@ function parseSessionName(input) {
799
1194
  }
800
1195
  const segments = trimmed.split("/").filter(Boolean);
801
1196
  if (segments.length < 2) {
802
- throw new Error(`Session name must include at least one group: "group/session" (got "${trimmed}")`);
1197
+ throw new Error(`Session name must include at least one category: "category/session" (got "${trimmed}")`);
803
1198
  }
804
1199
  for (const segment of segments) {
805
1200
  if (!/^[a-z0-9][a-z0-9._-]*$/i.test(segment)) {
@@ -807,12 +1202,12 @@ function parseSessionName(input) {
807
1202
  }
808
1203
  }
809
1204
  const slug = segments[segments.length - 1];
810
- const groupPath = segments.slice(0, -1).join("/");
811
- return { groupPath, slug };
1205
+ const categoryPath = segments.slice(0, -1).join("/");
1206
+ return { categoryPath, slug };
812
1207
  }
813
1208
 
814
1209
  // src/tui/screens/launch/index.tsx
815
- import { jsxDEV as jsxDEV6, Fragment } from "react/jsx-dev-runtime";
1210
+ import { jsxDEV as jsxDEV7, Fragment as Fragment2 } from "react/jsx-dev-runtime";
816
1211
  var STATUS_COLOR = {
817
1212
  paused: "gold",
818
1213
  waiting: "red",
@@ -835,8 +1230,8 @@ function sessionRow(s) {
835
1230
  const disabled = status === "waiting";
836
1231
  const isArchived = status === "archived";
837
1232
  return {
838
- value: `${s.groupPath}/${s.session.slug}`,
839
- label: `${s.groupPath}/${s.session.slug} ${status}`,
1233
+ value: `${s.categoryPath}/${s.session.slug}`,
1234
+ label: `${s.categoryPath}/${s.session.slug} ${status}`,
840
1235
  meta: formatAgo(recencyKey(s)),
841
1236
  disabled,
842
1237
  dim: isArchived,
@@ -846,14 +1241,14 @@ function sessionRow(s) {
846
1241
  const textColor = isCursor ? cursorColor : color;
847
1242
  const slugColor = isCursor ? cursorColor : undefined;
848
1243
  const dimText = !isCursor && (disabled || isArchived);
849
- return /* @__PURE__ */ jsxDEV6(Fragment, {
1244
+ return /* @__PURE__ */ jsxDEV7(Fragment2, {
850
1245
  children: [
851
- /* @__PURE__ */ jsxDEV6(Text6, {
1246
+ /* @__PURE__ */ jsxDEV7(Text7, {
852
1247
  color: cursorColor,
853
1248
  bold: true,
854
1249
  children: isCursor ? "\u276F " : " "
855
1250
  }, undefined, false, undefined, this),
856
- /* @__PURE__ */ jsxDEV6(Text6, {
1251
+ /* @__PURE__ */ jsxDEV7(Text7, {
857
1252
  color: dotColor,
858
1253
  bold: isCursor,
859
1254
  children: [
@@ -861,17 +1256,17 @@ function sessionRow(s) {
861
1256
  " "
862
1257
  ]
863
1258
  }, undefined, true, undefined, this),
864
- /* @__PURE__ */ jsxDEV6(Text6, {
1259
+ /* @__PURE__ */ jsxDEV7(Text7, {
865
1260
  color: textColor,
866
1261
  bold: isCursor,
867
1262
  dim: dimText,
868
1263
  children: status.padEnd(8)
869
1264
  }, undefined, false, undefined, this),
870
- /* @__PURE__ */ jsxDEV6(Text6, {
1265
+ /* @__PURE__ */ jsxDEV7(Text7, {
871
1266
  color: slugColor,
872
1267
  children: " "
873
1268
  }, undefined, false, undefined, this),
874
- /* @__PURE__ */ jsxDEV6(Text6, {
1269
+ /* @__PURE__ */ jsxDEV7(Text7, {
875
1270
  color: slugColor,
876
1271
  bold: isCursor,
877
1272
  dim: dimText,
@@ -882,21 +1277,20 @@ function sessionRow(s) {
882
1277
  }
883
1278
  };
884
1279
  }
885
- function groupHeader(groupPath) {
1280
+ function categoryHeader(categoryPath) {
886
1281
  return {
887
- value: `__group:${groupPath}`,
888
- label: groupPath,
1282
+ value: `__category:${categoryPath}`,
1283
+ label: categoryPath,
889
1284
  kind: "header"
890
1285
  };
891
1286
  }
892
1287
  function Launch({ onSelect }) {
893
- const { exit } = useTui();
894
1288
  const [error, setError] = useState3(null);
895
1289
  const [notice, setNotice] = useState3(null);
896
1290
  const [showArchived, setShowArchived] = useState3(false);
897
1291
  const [refreshKey, setRefreshKey] = useState3(0);
898
- const allSessions = useMemo2(() => getAllSessions({ excludeArchived: !showArchived }), [showArchived, refreshKey]);
899
- const visibleSessions = useMemo2(() => {
1292
+ const allSessions = useMemo3(() => getAllSessions({ excludeArchived: !showArchived }), [showArchived, refreshKey]);
1293
+ const visibleSessions = useMemo3(() => {
900
1294
  return allSessions.filter((s) => {
901
1295
  const st = s.session.status;
902
1296
  if (st === "paused" || st === "waiting")
@@ -905,7 +1299,7 @@ function Launch({ onSelect }) {
905
1299
  return showArchived;
906
1300
  return false;
907
1301
  }).sort((a, b) => {
908
- const g = a.groupPath.localeCompare(b.groupPath);
1302
+ const g = a.categoryPath.localeCompare(b.categoryPath);
909
1303
  if (g !== 0)
910
1304
  return g;
911
1305
  const r = statusRank(a.session.status) - statusRank(b.session.status);
@@ -914,31 +1308,31 @@ function Launch({ onSelect }) {
914
1308
  return recencyKey(b).localeCompare(recencyKey(a));
915
1309
  });
916
1310
  }, [allSessions, showArchived]);
917
- const items = useMemo2(() => {
1311
+ const items = useMemo3(() => {
918
1312
  const rows = [];
919
- let lastGroup = null;
1313
+ let lastCategory = null;
920
1314
  for (const s of visibleSessions) {
921
- if (s.groupPath !== lastGroup) {
922
- rows.push(groupHeader(s.groupPath));
923
- lastGroup = s.groupPath;
1315
+ if (s.categoryPath !== lastCategory) {
1316
+ rows.push(categoryHeader(s.categoryPath));
1317
+ lastCategory = s.categoryPath;
924
1318
  }
925
1319
  rows.push(sessionRow(s));
926
1320
  }
927
1321
  return rows;
928
1322
  }, [visibleSessions]);
929
- const suggestions = useMemo2(() => {
930
- const groups2 = new Set;
1323
+ const suggestions = useMemo3(() => {
1324
+ const categories2 = new Set;
931
1325
  const names = [];
932
1326
  for (const s of allSessions) {
933
- groups2.add(`${s.groupPath}/`);
934
- names.push(`${s.groupPath}/${s.session.slug}`);
1327
+ categories2.add(`${s.categoryPath}/`);
1328
+ names.push(`${s.categoryPath}/${s.session.slug}`);
935
1329
  }
936
- return [...groups2, ...names];
1330
+ return [...categories2, ...names];
937
1331
  }, [allSessions]);
938
- const sessionByValue = useMemo2(() => {
1332
+ const sessionByValue = useMemo3(() => {
939
1333
  const map = new Map;
940
1334
  for (const s of allSessions) {
941
- map.set(`${s.groupPath}/${s.session.slug}`, s);
1335
+ map.set(`${s.categoryPath}/${s.session.slug}`, s);
942
1336
  }
943
1337
  return map;
944
1338
  }, [allSessions]);
@@ -969,7 +1363,6 @@ function Launch({ onSelect }) {
969
1363
  };
970
1364
  const select = (selection) => {
971
1365
  onSelect(selection);
972
- exit();
973
1366
  };
974
1367
  const handleSubmit = (value) => {
975
1368
  const existing = sessionByValue.get(value);
@@ -982,13 +1375,13 @@ function Launch({ onSelect }) {
982
1375
  return;
983
1376
  }
984
1377
  try {
985
- const { groupPath, slug } = parseSessionName(value);
986
- select({ type: "create", groupPath, slug });
1378
+ const { categoryPath, slug } = parseSessionName(value);
1379
+ select({ type: "create", categoryPath, slug });
987
1380
  } catch (e) {
988
1381
  setError(e instanceof Error ? e.message : "Invalid name");
989
1382
  }
990
1383
  };
991
- const counts = useMemo2(() => {
1384
+ const counts = useMemo3(() => {
992
1385
  let paused = 0;
993
1386
  let waiting = 0;
994
1387
  let archived = 0;
@@ -1002,56 +1395,56 @@ function Launch({ onSelect }) {
1002
1395
  }
1003
1396
  return { paused, waiting, archived };
1004
1397
  }, [visibleSessions]);
1005
- return /* @__PURE__ */ jsxDEV6(Box5, {
1398
+ return /* @__PURE__ */ jsxDEV7(Box6, {
1006
1399
  flexDirection: "column",
1007
1400
  paddingY: 1,
1008
1401
  gap: 1,
1009
1402
  children: [
1010
- /* @__PURE__ */ jsxDEV6(Box5, {
1403
+ /* @__PURE__ */ jsxDEV7(Box6, {
1011
1404
  marginX: 1,
1012
- children: /* @__PURE__ */ jsxDEV6(Logo, {}, undefined, false, undefined, this)
1405
+ children: /* @__PURE__ */ jsxDEV7(Logo, {}, undefined, false, undefined, this)
1013
1406
  }, undefined, false, undefined, this),
1014
- /* @__PURE__ */ jsxDEV6(Box5, {
1407
+ /* @__PURE__ */ jsxDEV7(Box6, {
1015
1408
  flexDirection: "column",
1016
1409
  marginX: 2,
1017
1410
  gap: 1,
1018
1411
  children: [
1019
- /* @__PURE__ */ jsxDEV6(AppDetails, {}, undefined, false, undefined, this),
1020
- /* @__PURE__ */ jsxDEV6(Box5, {
1412
+ /* @__PURE__ */ jsxDEV7(AppDetails, {}, undefined, false, undefined, this),
1413
+ /* @__PURE__ */ jsxDEV7(Box6, {
1021
1414
  flexDirection: "column",
1022
1415
  gap: 1,
1023
1416
  children: [
1024
- /* @__PURE__ */ jsxDEV6(Box5, {
1417
+ /* @__PURE__ */ jsxDEV7(Box6, {
1025
1418
  flexDirection: "row",
1026
1419
  gap: 1,
1027
1420
  children: [
1028
- /* @__PURE__ */ jsxDEV6(Text6, {
1421
+ /* @__PURE__ */ jsxDEV7(Text7, {
1029
1422
  bold: true,
1030
1423
  children: "Sessions"
1031
1424
  }, undefined, false, undefined, this),
1032
- visibleSessions.length === 0 ? /* @__PURE__ */ jsxDEV6(Text6, {
1425
+ visibleSessions.length === 0 ? /* @__PURE__ */ jsxDEV7(Text7, {
1033
1426
  dim: true,
1034
- children: "\xB7 none \u2014 type group/slug to create"
1035
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV6(Fragment, {
1427
+ children: "\xB7 none \u2014 type category/slug to create"
1428
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV7(Fragment2, {
1036
1429
  children: [
1037
- /* @__PURE__ */ jsxDEV6(Text6, {
1430
+ /* @__PURE__ */ jsxDEV7(Text7, {
1038
1431
  dim: true,
1039
1432
  children: "\xB7"
1040
1433
  }, undefined, false, undefined, this),
1041
- /* @__PURE__ */ jsxDEV6(Text6, {
1434
+ /* @__PURE__ */ jsxDEV7(Text7, {
1042
1435
  color: "gold",
1043
1436
  children: [
1044
1437
  counts.paused,
1045
1438
  " paused"
1046
1439
  ]
1047
1440
  }, undefined, true, undefined, this),
1048
- counts.waiting > 0 && /* @__PURE__ */ jsxDEV6(Fragment, {
1441
+ counts.waiting > 0 && /* @__PURE__ */ jsxDEV7(Fragment2, {
1049
1442
  children: [
1050
- /* @__PURE__ */ jsxDEV6(Text6, {
1443
+ /* @__PURE__ */ jsxDEV7(Text7, {
1051
1444
  dim: true,
1052
1445
  children: "\xB7"
1053
1446
  }, undefined, false, undefined, this),
1054
- /* @__PURE__ */ jsxDEV6(Text6, {
1447
+ /* @__PURE__ */ jsxDEV7(Text7, {
1055
1448
  color: "red",
1056
1449
  dim: true,
1057
1450
  children: [
@@ -1061,13 +1454,13 @@ function Launch({ onSelect }) {
1061
1454
  }, undefined, true, undefined, this)
1062
1455
  ]
1063
1456
  }, undefined, true, undefined, this),
1064
- showArchived && counts.archived > 0 && /* @__PURE__ */ jsxDEV6(Fragment, {
1457
+ showArchived && counts.archived > 0 && /* @__PURE__ */ jsxDEV7(Fragment2, {
1065
1458
  children: [
1066
- /* @__PURE__ */ jsxDEV6(Text6, {
1459
+ /* @__PURE__ */ jsxDEV7(Text7, {
1067
1460
  dim: true,
1068
1461
  children: "\xB7"
1069
1462
  }, undefined, false, undefined, this),
1070
- /* @__PURE__ */ jsxDEV6(Text6, {
1463
+ /* @__PURE__ */ jsxDEV7(Text7, {
1071
1464
  color: "purple",
1072
1465
  dim: true,
1073
1466
  children: [
@@ -1081,14 +1474,14 @@ function Launch({ onSelect }) {
1081
1474
  }, undefined, true, undefined, this)
1082
1475
  ]
1083
1476
  }, undefined, true, undefined, this),
1084
- /* @__PURE__ */ jsxDEV6(Picker, {
1477
+ /* @__PURE__ */ jsxDEV7(Picker, {
1085
1478
  mode: "single",
1086
1479
  items,
1087
1480
  isFocused: true,
1088
1481
  maxVisible: 24,
1089
1482
  suggest: suggestions,
1090
- placeholder: "Filter or type group/slug to create\u2026",
1091
- emptyHint: showArchived ? "No sessions. Type group/slug to create one." : "No paused sessions. Type group/slug to create one.",
1483
+ placeholder: "Filter or type category/slug to create\u2026",
1484
+ emptyHint: showArchived ? "No sessions. Type category/slug to create one." : "No paused sessions. Type category/slug to create one.",
1092
1485
  onSubmit: handleSubmit,
1093
1486
  onKey: (e, cursorItem) => {
1094
1487
  if (e.key === "c" && e.ctrl) {
@@ -1102,18 +1495,18 @@ function Launch({ onSelect }) {
1102
1495
  }
1103
1496
  }
1104
1497
  }, undefined, false, undefined, this),
1105
- notice && /* @__PURE__ */ jsxDEV6(Text6, {
1498
+ notice && /* @__PURE__ */ jsxDEV7(Text7, {
1106
1499
  color: "green",
1107
1500
  children: notice
1108
1501
  }, undefined, false, undefined, this),
1109
- error && /* @__PURE__ */ jsxDEV6(Text6, {
1502
+ error && /* @__PURE__ */ jsxDEV7(Text7, {
1110
1503
  color: "red",
1111
1504
  children: error
1112
1505
  }, undefined, false, undefined, this),
1113
- /* @__PURE__ */ jsxDEV6(Text6, {
1506
+ /* @__PURE__ */ jsxDEV7(Text7, {
1114
1507
  dim: true,
1115
1508
  children: [
1116
- "\u2191\u2193 navigate \xB7 \u2190\u2192 skip group \xB7 enter continue/create \xB7 ctrl+a",
1509
+ "\u2191\u2193 navigate \xB7 \u2190\u2192 skip category \xB7 enter continue/create \xB7 ctrl+a",
1117
1510
  " ",
1118
1511
  showArchived ? "(un)archive" : "archive",
1119
1512
  " \xB7 tab",
@@ -1130,9 +1523,104 @@ function Launch({ onSelect }) {
1130
1523
  }, undefined, true, undefined, this);
1131
1524
  }
1132
1525
 
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
+
1133
1621
  // src/tui/screens/Exit.tsx
1134
- import { useState as useState4 } from "react";
1135
- import { Box as Box6, Text as Text7, useInput as useInput3, useTui as useTui2 } from "@orchetron/storm";
1622
+ import { useState as useState5 } from "react";
1623
+ import { Box as Box7, Text as Text8, useInput as useInput3, useTui as useTui2 } from "@orchetron/storm";
1136
1624
 
1137
1625
  // src/db/queries/conversations.ts
1138
1626
  import { eq as eq2, and as and2, desc, sql as sql3 } from "drizzle-orm";
@@ -1141,7 +1629,7 @@ function getConversationsBySession(sessionId) {
1141
1629
  }
1142
1630
 
1143
1631
  // src/tui/screens/Exit.tsx
1144
- import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
1632
+ import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
1145
1633
  var OPTIONS = [
1146
1634
  { action: "save", label: "Save", hint: "Keep session paused for later" },
1147
1635
  {
@@ -1162,7 +1650,7 @@ var OPTIONS = [
1162
1650
  ];
1163
1651
  function Exit({ sessionId, onAction }) {
1164
1652
  const { exit } = useTui2();
1165
- const [cursor, setCursor] = useState4(0);
1653
+ const [cursor, setCursor] = useState5(0);
1166
1654
  const session = getSession(sessionId);
1167
1655
  const conversations2 = session ? getConversationsBySession(session.id) : [];
1168
1656
  useInput3((e) => {
@@ -1182,37 +1670,37 @@ function Exit({ sessionId, onAction }) {
1182
1670
  }
1183
1671
  });
1184
1672
  if (!session) {
1185
- return /* @__PURE__ */ jsxDEV7(Text7, {
1673
+ return /* @__PURE__ */ jsxDEV9(Text8, {
1186
1674
  color: "red",
1187
1675
  children: "Session not found"
1188
1676
  }, undefined, false, undefined, this);
1189
1677
  }
1190
1678
  const duration = session.endedAt && session.startedAt ? formatDuration(new Date(session.endedAt).getTime() - new Date(session.startedAt).getTime()) : null;
1191
- return /* @__PURE__ */ jsxDEV7(Box6, {
1679
+ return /* @__PURE__ */ jsxDEV9(Box7, {
1192
1680
  flexDirection: "column",
1193
1681
  padding: 1,
1194
1682
  gap: 1,
1195
1683
  children: [
1196
- /* @__PURE__ */ jsxDEV7(Text7, {
1684
+ /* @__PURE__ */ jsxDEV9(Text8, {
1197
1685
  bold: true,
1198
1686
  color: "#82AAFF",
1199
1687
  children: "Session ended"
1200
1688
  }, undefined, false, undefined, this),
1201
- /* @__PURE__ */ jsxDEV7(Box6, {
1689
+ /* @__PURE__ */ jsxDEV9(Box7, {
1202
1690
  flexDirection: "column",
1203
1691
  children: [
1204
- /* @__PURE__ */ jsxDEV7(Box6, {
1692
+ /* @__PURE__ */ jsxDEV9(Box7, {
1205
1693
  flexDirection: "row",
1206
1694
  gap: 1,
1207
1695
  children: [
1208
- /* @__PURE__ */ jsxDEV7(StatusDot, {
1696
+ /* @__PURE__ */ jsxDEV9(StatusDot, {
1209
1697
  status: session.status
1210
1698
  }, undefined, false, undefined, this),
1211
- /* @__PURE__ */ jsxDEV7(Text7, {
1699
+ /* @__PURE__ */ jsxDEV9(Text8, {
1212
1700
  bold: true,
1213
1701
  children: session.name
1214
1702
  }, undefined, false, undefined, this),
1215
- duration && /* @__PURE__ */ jsxDEV7(Text7, {
1703
+ duration && /* @__PURE__ */ jsxDEV9(Text8, {
1216
1704
  dim: true,
1217
1705
  children: [
1218
1706
  "(",
@@ -1222,7 +1710,7 @@ function Exit({ sessionId, onAction }) {
1222
1710
  }, undefined, true, undefined, this)
1223
1711
  ]
1224
1712
  }, undefined, true, undefined, this),
1225
- /* @__PURE__ */ jsxDEV7(Text7, {
1713
+ /* @__PURE__ */ jsxDEV9(Text8, {
1226
1714
  dim: true,
1227
1715
  children: [
1228
1716
  conversations2.length,
@@ -1232,28 +1720,28 @@ function Exit({ sessionId, onAction }) {
1232
1720
  }, undefined, true, undefined, this)
1233
1721
  ]
1234
1722
  }, undefined, true, undefined, this),
1235
- /* @__PURE__ */ jsxDEV7(Box6, {
1723
+ /* @__PURE__ */ jsxDEV9(Box7, {
1236
1724
  flexDirection: "column",
1237
- children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV7(Box6, {
1725
+ children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV9(Box7, {
1238
1726
  flexDirection: "row",
1239
1727
  gap: 1,
1240
1728
  children: [
1241
- /* @__PURE__ */ jsxDEV7(Text7, {
1729
+ /* @__PURE__ */ jsxDEV9(Text8, {
1242
1730
  children: i === cursor ? "\u276F" : " "
1243
1731
  }, undefined, false, undefined, this),
1244
- /* @__PURE__ */ jsxDEV7(Text7, {
1732
+ /* @__PURE__ */ jsxDEV9(Text8, {
1245
1733
  bold: i === cursor,
1246
1734
  children: opt.label
1247
1735
  }, undefined, false, undefined, this),
1248
- /* @__PURE__ */ jsxDEV7(Text7, {
1736
+ /* @__PURE__ */ jsxDEV9(Text8, {
1249
1737
  dim: true,
1250
1738
  children: opt.hint
1251
1739
  }, undefined, false, undefined, this)
1252
1740
  ]
1253
1741
  }, opt.action, true, undefined, this))
1254
1742
  }, undefined, false, undefined, this),
1255
- /* @__PURE__ */ jsxDEV7(Box6, {
1256
- children: /* @__PURE__ */ jsxDEV7(Text7, {
1743
+ /* @__PURE__ */ jsxDEV9(Box7, {
1744
+ children: /* @__PURE__ */ jsxDEV9(Text8, {
1257
1745
  dim: true,
1258
1746
  children: "\u2191\u2193 navigate \xB7 enter select \xB7 q save & quit"
1259
1747
  }, undefined, false, undefined, this)
@@ -1263,12 +1751,12 @@ function Exit({ sessionId, onAction }) {
1263
1751
  }
1264
1752
 
1265
1753
  // src/tui/screens/Resume.tsx
1266
- import { useState as useState5 } from "react";
1267
- import { Box as Box7, Text as Text8, useInput as useInput4, useTui as useTui3 } from "@orchetron/storm";
1268
- import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
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";
1269
1757
  function Resume({ sessionId, onSelect }) {
1270
1758
  const { exit } = useTui3();
1271
- const [cursor, setCursor] = useState5(0);
1759
+ const [cursor, setCursor] = useState6(0);
1272
1760
  const session = getSession(sessionId);
1273
1761
  const conversations2 = getConversationsBySession(sessionId);
1274
1762
  const totalOptions = conversations2.length + 1;
@@ -1295,17 +1783,17 @@ function Resume({ sessionId, onSelect }) {
1295
1783
  }
1296
1784
  });
1297
1785
  if (!session) {
1298
- return /* @__PURE__ */ jsxDEV8(Text8, {
1786
+ return /* @__PURE__ */ jsxDEV10(Text9, {
1299
1787
  color: "red",
1300
1788
  children: "Session not found"
1301
1789
  }, undefined, false, undefined, this);
1302
1790
  }
1303
- return /* @__PURE__ */ jsxDEV8(Box7, {
1791
+ return /* @__PURE__ */ jsxDEV10(Box8, {
1304
1792
  flexDirection: "column",
1305
1793
  padding: 1,
1306
1794
  gap: 1,
1307
1795
  children: [
1308
- /* @__PURE__ */ jsxDEV8(Text8, {
1796
+ /* @__PURE__ */ jsxDEV10(Text9, {
1309
1797
  bold: true,
1310
1798
  color: "#82AAFF",
1311
1799
  children: [
@@ -1313,17 +1801,17 @@ function Resume({ sessionId, onSelect }) {
1313
1801
  session.name
1314
1802
  ]
1315
1803
  }, undefined, true, undefined, this),
1316
- /* @__PURE__ */ jsxDEV8(Box7, {
1804
+ /* @__PURE__ */ jsxDEV10(Box8, {
1317
1805
  flexDirection: "column",
1318
1806
  children: [
1319
- /* @__PURE__ */ jsxDEV8(Box7, {
1807
+ /* @__PURE__ */ jsxDEV10(Box8, {
1320
1808
  flexDirection: "row",
1321
1809
  gap: 1,
1322
1810
  children: [
1323
- /* @__PURE__ */ jsxDEV8(Text8, {
1811
+ /* @__PURE__ */ jsxDEV10(Text9, {
1324
1812
  children: cursor === 0 ? "\u276F" : " "
1325
1813
  }, undefined, false, undefined, this),
1326
- /* @__PURE__ */ jsxDEV8(Text8, {
1814
+ /* @__PURE__ */ jsxDEV10(Text9, {
1327
1815
  bold: cursor === 0,
1328
1816
  color: "#34D399",
1329
1817
  children: "+ New conversation"
@@ -1335,42 +1823,42 @@ function Resume({ sessionId, onSelect }) {
1335
1823
  const isSelected = cursor === idx;
1336
1824
  const duration = conv.endedAt ? formatDuration(new Date(conv.endedAt).getTime() - new Date(conv.startedAt).getTime()) : "active";
1337
1825
  const ago = formatAgo(conv.startedAt);
1338
- return /* @__PURE__ */ jsxDEV8(Box7, {
1826
+ return /* @__PURE__ */ jsxDEV10(Box8, {
1339
1827
  flexDirection: "row",
1340
1828
  gap: 1,
1341
1829
  children: [
1342
- /* @__PURE__ */ jsxDEV8(Text8, {
1830
+ /* @__PURE__ */ jsxDEV10(Text9, {
1343
1831
  children: isSelected ? "\u276F" : " "
1344
1832
  }, undefined, false, undefined, this),
1345
- /* @__PURE__ */ jsxDEV8(Text8, {
1833
+ /* @__PURE__ */ jsxDEV10(Text9, {
1346
1834
  bold: isSelected,
1347
1835
  dim: conv.discarded,
1348
1836
  children: conv.id.slice(0, 8)
1349
1837
  }, undefined, false, undefined, this),
1350
- /* @__PURE__ */ jsxDEV8(Text8, {
1838
+ /* @__PURE__ */ jsxDEV10(Text9, {
1351
1839
  dim: true,
1352
1840
  children: ago
1353
1841
  }, undefined, false, undefined, this),
1354
- /* @__PURE__ */ jsxDEV8(Text8, {
1842
+ /* @__PURE__ */ jsxDEV10(Text9, {
1355
1843
  dim: true,
1356
1844
  children: "\xB7"
1357
1845
  }, undefined, false, undefined, this),
1358
- /* @__PURE__ */ jsxDEV8(Text8, {
1846
+ /* @__PURE__ */ jsxDEV10(Text9, {
1359
1847
  dim: true,
1360
1848
  children: [
1361
1849
  conv.eventCount,
1362
1850
  " events"
1363
1851
  ]
1364
1852
  }, undefined, true, undefined, this),
1365
- /* @__PURE__ */ jsxDEV8(Text8, {
1853
+ /* @__PURE__ */ jsxDEV10(Text9, {
1366
1854
  dim: true,
1367
1855
  children: "\xB7"
1368
1856
  }, undefined, false, undefined, this),
1369
- /* @__PURE__ */ jsxDEV8(Text8, {
1857
+ /* @__PURE__ */ jsxDEV10(Text9, {
1370
1858
  dim: true,
1371
1859
  children: duration
1372
1860
  }, undefined, false, undefined, this),
1373
- conv.discarded && /* @__PURE__ */ jsxDEV8(Text8, {
1861
+ conv.discarded && /* @__PURE__ */ jsxDEV10(Text9, {
1374
1862
  color: "red",
1375
1863
  dim: true,
1376
1864
  children: "(discarded)"
@@ -1380,8 +1868,8 @@ function Resume({ sessionId, onSelect }) {
1380
1868
  })
1381
1869
  ]
1382
1870
  }, undefined, true, undefined, this),
1383
- /* @__PURE__ */ jsxDEV8(Box7, {
1384
- children: /* @__PURE__ */ jsxDEV8(Text8, {
1871
+ /* @__PURE__ */ jsxDEV10(Box8, {
1872
+ children: /* @__PURE__ */ jsxDEV10(Text9, {
1385
1873
  dim: true,
1386
1874
  children: "\u2191\u2193 navigate \xB7 enter select \xB7 q back"
1387
1875
  }, undefined, false, undefined, this)
@@ -1391,7 +1879,7 @@ function Resume({ sessionId, onSelect }) {
1391
1879
  }
1392
1880
 
1393
1881
  // src/tui/run-screen.tsx
1394
- import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
1882
+ import { jsxDEV as jsxDEV11 } from "react/jsx-dev-runtime";
1395
1883
  var [, , screen, outputPath, ...args] = process.argv;
1396
1884
  if (!screen || !outputPath) {
1397
1885
  console.error("Usage: run-screen <screen> <outputPath> [args...]");
@@ -1399,9 +1887,13 @@ if (!screen || !outputPath) {
1399
1887
  }
1400
1888
  var result;
1401
1889
  switch (screen) {
1402
- case "launch": {
1890
+ case "startup": {
1891
+ const skipProjectPicker = args[0] === "true";
1892
+ const initialProjectSlug = args[1] ?? "";
1403
1893
  let selection = { type: "quit" };
1404
- const app = render(/* @__PURE__ */ jsxDEV9(Launch, {
1894
+ const app = render(/* @__PURE__ */ jsxDEV11(Startup, {
1895
+ skipProjectPicker,
1896
+ initialProjectSlug,
1405
1897
  onSelect: (s) => {
1406
1898
  selection = s;
1407
1899
  }
@@ -1418,7 +1910,7 @@ switch (screen) {
1418
1910
  process.exit(1);
1419
1911
  }
1420
1912
  let action = "save";
1421
- const app = render(/* @__PURE__ */ jsxDEV9(Exit, {
1913
+ const app = render(/* @__PURE__ */ jsxDEV11(Exit, {
1422
1914
  sessionId,
1423
1915
  onAction: (a) => {
1424
1916
  action = a;
@@ -1436,7 +1928,7 @@ switch (screen) {
1436
1928
  process.exit(1);
1437
1929
  }
1438
1930
  let selection = { type: "back" };
1439
- const app = render(/* @__PURE__ */ jsxDEV9(Resume, {
1931
+ const app = render(/* @__PURE__ */ jsxDEV11(Resume, {
1440
1932
  sessionId,
1441
1933
  onSelect: (s) => {
1442
1934
  selection = s;
@@ -1451,5 +1943,5 @@ switch (screen) {
1451
1943
  console.error(`Unknown screen: ${screen}`);
1452
1944
  process.exit(1);
1453
1945
  }
1454
- writeFileSync(outputPath, JSON.stringify(result));
1946
+ writeFileSync2(outputPath, JSON.stringify(result));
1455
1947
  process.exit(0);