bertrand 0.14.1 → 0.16.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.
@@ -19,8 +19,8 @@ import { writeFileSync } from "fs";
19
19
  import { render } from "@orchetron/storm";
20
20
 
21
21
  // src/tui/screens/launch/index.tsx
22
- import { useCallback as useCallback2 } from "react";
23
- import { Box as Box5, useTui as useTui2 } from "@orchetron/storm";
22
+ import { useMemo as useMemo2, useState as useState2 } from "react";
23
+ import { Box as Box4, Text as Text5, useTui } from "@orchetron/storm";
24
24
 
25
25
  // src/tui/components/app-details.tsx
26
26
  import { Box, Text } from "@orchetron/storm";
@@ -74,6 +74,24 @@ import { useMemo, useState } from "react";
74
74
  import { Box as Box3, Text as Text3, TextInput, useInput } from "@orchetron/storm";
75
75
  import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
76
76
  var NEW_KEY = "__new__";
77
+ function isSelectable(row) {
78
+ if (row.value === NEW_KEY)
79
+ return true;
80
+ const item = row;
81
+ return item.kind !== "header" && !item.disabled;
82
+ }
83
+ function findNextSelectable(rows, from, direction) {
84
+ let i = from;
85
+ while (i >= 0 && i < rows.length) {
86
+ if (isSelectable(rows[i]))
87
+ return i;
88
+ i += direction;
89
+ }
90
+ return -1;
91
+ }
92
+ function findFirstSelectable(rows) {
93
+ return findNextSelectable(rows, 0, 1);
94
+ }
77
95
  function Picker(props) {
78
96
  const {
79
97
  items,
@@ -81,7 +99,7 @@ function Picker(props) {
81
99
  placeholder,
82
100
  allowCreate = true,
83
101
  emptyHint,
84
- maxVisible = 8
102
+ maxVisible = 12
85
103
  } = props;
86
104
  const [filter, setFilter] = useState("");
87
105
  const [cursor, setCursor] = useState(0);
@@ -89,13 +107,19 @@ function Picker(props) {
89
107
  const q = filter.trim().toLowerCase();
90
108
  if (!q)
91
109
  return items;
92
- return items.filter((it) => it.label.toLowerCase().includes(q));
110
+ return items.filter((it) => it.kind !== "header" && it.label.toLowerCase().includes(q));
93
111
  }, [filter, items]);
94
- const exactMatch = useMemo(() => filtered.some((it) => it.label.toLowerCase() === filter.trim().toLowerCase()), [filter, filtered]);
112
+ const exactMatch = useMemo(() => filtered.some((it) => it.kind !== "header" && it.label.toLowerCase() === filter.trim().toLowerCase()), [filter, filtered]);
95
113
  const showCreate = allowCreate && filter.trim().length > 0 && !exactMatch;
96
114
  const visibleRows = useMemo(() => showCreate ? [...filtered, { value: NEW_KEY }] : filtered, [filtered, showCreate]);
97
- if (cursor >= visibleRows.length) {
98
- setTimeout(() => setCursor(Math.max(0, visibleRows.length - 1)), 0);
115
+ if (visibleRows.length === 0) {
116
+ if (cursor !== 0)
117
+ setTimeout(() => setCursor(0), 0);
118
+ } else if (cursor >= visibleRows.length || !isSelectable(visibleRows[cursor])) {
119
+ const next = findFirstSelectable(visibleRows);
120
+ if (next !== -1 && next !== cursor) {
121
+ setTimeout(() => setCursor(next), 0);
122
+ }
99
123
  }
100
124
  useInput((e) => {
101
125
  if (!isFocused)
@@ -108,16 +132,24 @@ function Picker(props) {
108
132
  if (visibleRows.length === 0)
109
133
  return;
110
134
  if (e.key === "up") {
111
- setCursor((c) => Math.max(0, c - 1));
135
+ setCursor((c) => {
136
+ const next = findNextSelectable(visibleRows, c - 1, -1);
137
+ return next === -1 ? c : next;
138
+ });
112
139
  } else if (e.key === "down") {
113
- setCursor((c) => Math.min(visibleRows.length - 1, c + 1));
140
+ setCursor((c) => {
141
+ const next = findNextSelectable(visibleRows, c + 1, 1);
142
+ return next === -1 ? c : next;
143
+ });
114
144
  } else if (e.key === "tab" && props.mode === "multi") {
115
145
  props.onDone();
116
146
  }
117
147
  }, { isActive: isFocused });
118
148
  const submitCursor = () => {
119
149
  const row = visibleRows[cursor];
120
- const value = row && row.value === NEW_KEY ? filter.trim() : row?.value ?? "";
150
+ if (!row || !isSelectable(row))
151
+ return;
152
+ const value = row.value === NEW_KEY ? filter.trim() : row.value;
121
153
  if (!value)
122
154
  return;
123
155
  if (props.mode === "single") {
@@ -230,28 +262,46 @@ function Picker(props) {
230
262
  }, "__new__", true, undefined, this);
231
263
  }
232
264
  const item = row;
265
+ if (item.kind === "header") {
266
+ return /* @__PURE__ */ jsxDEV3(Box3, {
267
+ flexDirection: "row",
268
+ children: /* @__PURE__ */ jsxDEV3(Text3, {
269
+ dim: true,
270
+ bold: true,
271
+ color: item.color ?? undefined,
272
+ children: item.label
273
+ }, undefined, false, undefined, this)
274
+ }, `h:${item.value}`, false, undefined, this);
275
+ }
233
276
  const isSelected = selectedSet.has(item.value);
234
277
  const marker = props.mode === "multi" ? isSelected ? "\u2713 " : " " : "";
278
+ const dim = item.dim || item.disabled;
235
279
  return /* @__PURE__ */ jsxDEV3(Box3, {
236
280
  flexDirection: "row",
237
281
  justifyContent: "space-between",
238
282
  gap: 1,
239
283
  backgroundColor: isCursor ? "green" : undefined,
240
284
  children: [
241
- /* @__PURE__ */ jsxDEV3(Text3, {
242
- color: isCursor ? "black" : item.color ?? undefined,
243
- bold: isCursor,
244
- children: [
245
- marker,
246
- item.label
247
- ]
248
- }, undefined, true, undefined, this),
285
+ /* @__PURE__ */ jsxDEV3(Box3, {
286
+ flexDirection: "row",
287
+ gap: 0,
288
+ children: item.display ? item.display : /* @__PURE__ */ jsxDEV3(Text3, {
289
+ color: isCursor ? "black" : item.color ?? undefined,
290
+ bold: isCursor,
291
+ dim: !isCursor && dim,
292
+ children: [
293
+ marker,
294
+ item.label
295
+ ]
296
+ }, undefined, true, undefined, this)
297
+ }, undefined, false, undefined, this),
249
298
  item.meta && /* @__PURE__ */ jsxDEV3(Box3, {
250
299
  paddingX: 1,
251
300
  backgroundColor: isCursor ? "black" : "#3a3a3a",
252
301
  children: /* @__PURE__ */ jsxDEV3(Text3, {
253
302
  color: isCursor ? "green" : "white",
254
303
  bold: true,
304
+ dim: !isCursor && dim,
255
305
  children: item.meta
256
306
  }, undefined, false, undefined, this)
257
307
  }, undefined, false, undefined, this)
@@ -287,12 +337,8 @@ function StatusDot({ status }) {
287
337
  children: "\u25CF"
288
338
  }, undefined, false, undefined, this);
289
339
  }
290
- // src/tui/screens/launch/create-screen.tsx
291
- import { useState as useState2, useMemo as useMemo2, useCallback, useEffect, useRef } from "react";
292
- import { Box as Box4, Text as Text5, TextInput as TextInput2, useInput as useInput2, useTui } from "@orchetron/storm";
293
-
294
- // src/db/queries/groups.ts
295
- import { eq, like, or, isNull } from "drizzle-orm";
340
+ // src/db/queries/sessions.ts
341
+ import { eq, and, inArray, sql as sql2 } from "drizzle-orm";
296
342
 
297
343
  // src/db/client.ts
298
344
  import { Database } from "bun:sqlite";
@@ -442,19 +488,13 @@ function getDb() {
442
488
  // src/lib/id.ts
443
489
  import { nanoid } from "nanoid";
444
490
 
445
- // src/db/queries/groups.ts
446
- function getAllGroups() {
447
- return getDb().select().from(groups).all();
448
- }
449
-
450
491
  // src/db/queries/sessions.ts
451
- import { eq as eq2, and, inArray, sql as sql2 } from "drizzle-orm";
452
492
  function getSession(id) {
453
- return getDb().select().from(sessions).where(eq2(sessions.id, id)).get();
493
+ return getDb().select().from(sessions).where(eq(sessions.id, id)).get();
454
494
  }
455
495
  function getAllSessions(opts) {
456
496
  const db = getDb();
457
- const query = db.select({ session: sessions, groupPath: groups.path }).from(sessions).innerJoin(groups, eq2(sessions.groupId, groups.id));
497
+ const query = db.select({ session: sessions, groupPath: groups.path }).from(sessions).innerJoin(groups, eq(sessions.groupId, groups.id));
458
498
  if (opts?.excludeArchived) {
459
499
  return query.where(inArray(sessions.status, [
460
500
  "active",
@@ -465,214 +505,6 @@ function getAllSessions(opts) {
465
505
  return query.all();
466
506
  }
467
507
 
468
- // src/tui/screens/launch/create-screen.tsx
469
- import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
470
- function CreateScreen({
471
- isFocused,
472
- onSubmit,
473
- onQuit
474
- }) {
475
- const { clear } = useTui();
476
- const [step, setStep] = useState2("group");
477
- const [groupPath, setGroupPath] = useState2(null);
478
- const [slug, setSlug] = useState2("");
479
- const [error, setError] = useState2(null);
480
- const stepRef = useRef(step);
481
- stepRef.current = step;
482
- useEffect(() => {
483
- clear();
484
- }, [step, clear]);
485
- const groupItems = useMemo2(() => {
486
- const activeSessions = getAllSessions({ excludeArchived: true });
487
- const counts = new Map;
488
- for (const s of activeSessions) {
489
- counts.set(s.groupPath, (counts.get(s.groupPath) ?? 0) + 1);
490
- }
491
- return getAllGroups().filter((g) => counts.has(g.path)).sort((a, b) => a.path.localeCompare(b.path)).map((g) => ({
492
- value: g.path,
493
- label: g.path,
494
- color: g.color,
495
- meta: `${counts.get(g.path)}`
496
- }));
497
- }, []);
498
- const handleGroupPicked = useCallback((value) => {
499
- setGroupPath(value);
500
- setError(null);
501
- setStep("slug");
502
- }, []);
503
- const handleSlugSubmit = useCallback((value) => {
504
- if (stepRef.current !== "slug")
505
- return;
506
- const trimmed = value.trim();
507
- if (!trimmed) {
508
- setError("Name cannot be empty.");
509
- return;
510
- }
511
- if (!/^[a-z0-9][a-z0-9._-]*$/i.test(trimmed)) {
512
- setError("Name must start alphanumeric; letters, digits, dots, underscores, dashes only.");
513
- return;
514
- }
515
- if (!groupPath)
516
- return;
517
- onSubmit({ groupPath, slug: trimmed });
518
- }, [groupPath, onSubmit]);
519
- useInput2((e) => {
520
- if (!isFocused)
521
- return;
522
- if (e.key === "c" && e.ctrl) {
523
- onQuit();
524
- return;
525
- }
526
- if (e.key === "escape" && step === "slug") {
527
- setStep("group");
528
- setSlug("");
529
- setError(null);
530
- }
531
- }, { isActive: isFocused });
532
- return /* @__PURE__ */ jsxDEV5(Box4, {
533
- flexDirection: "column",
534
- gap: 1,
535
- children: [
536
- /* @__PURE__ */ jsxDEV5(Box4, {
537
- flexDirection: "row",
538
- gap: 1,
539
- children: [
540
- /* @__PURE__ */ jsxDEV5(Text5, {
541
- bold: true,
542
- children: "New session"
543
- }, undefined, false, undefined, this),
544
- /* @__PURE__ */ jsxDEV5(Text5, {
545
- dim: true,
546
- children: "\xB7"
547
- }, undefined, false, undefined, this),
548
- /* @__PURE__ */ jsxDEV5(Text5, {
549
- dim: true,
550
- children: stepLabel(step)
551
- }, undefined, false, undefined, this)
552
- ]
553
- }, undefined, true, undefined, this),
554
- groupPath && step === "slug" && /* @__PURE__ */ jsxDEV5(Box4, {
555
- flexDirection: "row",
556
- gap: 1,
557
- children: [
558
- /* @__PURE__ */ jsxDEV5(Text5, {
559
- dim: true,
560
- children: "Group:"
561
- }, undefined, false, undefined, this),
562
- /* @__PURE__ */ jsxDEV5(Text5, {
563
- color: "green",
564
- children: groupPath
565
- }, undefined, false, undefined, this)
566
- ]
567
- }, undefined, true, undefined, this),
568
- /* @__PURE__ */ jsxDEV5(Box4, {
569
- flexDirection: "column",
570
- height: step === "group" ? undefined : 0,
571
- overflow: "hidden",
572
- children: /* @__PURE__ */ jsxDEV5(Picker, {
573
- mode: "single",
574
- items: groupItems,
575
- isFocused: isFocused && step === "group",
576
- placeholder: "Pick or type a new group path\u2026",
577
- onSubmit: handleGroupPicked,
578
- emptyHint: "No active groups. Type a name to create one."
579
- }, undefined, false, undefined, this)
580
- }, undefined, false, undefined, this),
581
- /* @__PURE__ */ jsxDEV5(Box4, {
582
- flexDirection: "column",
583
- gap: 0,
584
- height: step === "slug" ? undefined : 0,
585
- overflow: "hidden",
586
- children: [
587
- /* @__PURE__ */ jsxDEV5(Text5, {
588
- dim: true,
589
- children: "Name"
590
- }, undefined, false, undefined, this),
591
- /* @__PURE__ */ jsxDEV5(Box4, {
592
- borderStyle: "round",
593
- borderColor: isFocused ? "green" : undefined,
594
- borderDimColor: !isFocused,
595
- paddingX: 1,
596
- children: /* @__PURE__ */ jsxDEV5(TextInput2, {
597
- value: slug,
598
- onChange: (v) => {
599
- setSlug(v);
600
- setError(null);
601
- },
602
- onSubmit: handleSlugSubmit,
603
- placeholder: "fix-auth-bug",
604
- color: "green",
605
- placeholderColor: "gray",
606
- isFocused: isFocused && step === "slug"
607
- }, undefined, false, undefined, this)
608
- }, undefined, false, undefined, this)
609
- ]
610
- }, undefined, true, undefined, this),
611
- error && /* @__PURE__ */ jsxDEV5(Text5, {
612
- color: "red",
613
- children: error
614
- }, undefined, false, undefined, this),
615
- /* @__PURE__ */ jsxDEV5(Text5, {
616
- dim: true,
617
- children: footer(step)
618
- }, undefined, false, undefined, this)
619
- ]
620
- }, undefined, true, undefined, this);
621
- }
622
- function stepLabel(step) {
623
- return step === "group" ? "1/2 Group" : "2/2 Name";
624
- }
625
- function footer(step) {
626
- if (step === "group") {
627
- return "\u2191\u2193 navigate \xB7 enter pick \xB7 ctrl+c quit";
628
- }
629
- return "enter create \xB7 esc back \xB7 ctrl+c quit";
630
- }
631
-
632
- // src/tui/screens/launch/index.tsx
633
- import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
634
- function Launch({ onSelect }) {
635
- const { exit } = useTui2();
636
- const select = useCallback2((selection) => {
637
- onSelect(selection);
638
- exit();
639
- }, [onSelect, exit]);
640
- return /* @__PURE__ */ jsxDEV6(Box5, {
641
- flexDirection: "column",
642
- paddingY: 1,
643
- gap: 1,
644
- children: [
645
- /* @__PURE__ */ jsxDEV6(Box5, {
646
- marginX: 1,
647
- children: /* @__PURE__ */ jsxDEV6(Logo, {}, undefined, false, undefined, this)
648
- }, undefined, false, undefined, this),
649
- /* @__PURE__ */ jsxDEV6(Box5, {
650
- flexDirection: "column",
651
- marginX: 2,
652
- gap: 1,
653
- children: [
654
- /* @__PURE__ */ jsxDEV6(AppDetails, {}, undefined, false, undefined, this),
655
- /* @__PURE__ */ jsxDEV6(CreateScreen, {
656
- isFocused: true,
657
- onSubmit: (payload) => select({ type: "create", ...payload }),
658
- onQuit: () => select({ type: "quit" })
659
- }, undefined, false, undefined, this)
660
- ]
661
- }, undefined, true, undefined, this)
662
- ]
663
- }, undefined, true, undefined, this);
664
- }
665
-
666
- // src/tui/screens/Exit.tsx
667
- import { useState as useState3 } from "react";
668
- import { Box as Box6, Text as Text6, useInput as useInput3, useTui as useTui3 } from "@orchetron/storm";
669
-
670
- // src/db/queries/conversations.ts
671
- import { eq as eq3, and as and2, desc, sql as sql3 } from "drizzle-orm";
672
- function getConversationsBySession(sessionId) {
673
- return getDb().select().from(conversations).where(and2(eq3(conversations.sessionId, sessionId), eq3(conversations.discarded, false))).orderBy(desc(conversations.startedAt)).all();
674
- }
675
-
676
508
  // src/lib/format.ts
677
509
  var SECOND = 1000;
678
510
  var MINUTE = 60 * SECOND;
@@ -706,8 +538,248 @@ function formatAgo(isoOrDate) {
706
538
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
707
539
  }
708
540
 
541
+ // src/lib/parse-session-name.ts
542
+ function parseSessionName(input) {
543
+ const trimmed = input.trim().replace(/^\/+|\/+$/g, "");
544
+ if (!trimmed) {
545
+ throw new Error("Session name cannot be empty");
546
+ }
547
+ const segments = trimmed.split("/").filter(Boolean);
548
+ if (segments.length < 2) {
549
+ throw new Error(`Session name must include at least one group: "group/session" (got "${trimmed}")`);
550
+ }
551
+ for (const segment of segments) {
552
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(segment)) {
553
+ throw new Error(`Invalid segment "${segment}": must start with alphanumeric and contain only letters, digits, dots, underscores, or dashes`);
554
+ }
555
+ }
556
+ const slug = segments[segments.length - 1];
557
+ const groupPath = segments.slice(0, -1).join("/");
558
+ return { groupPath, slug };
559
+ }
560
+
561
+ // src/tui/screens/launch/index.tsx
562
+ import { jsxDEV as jsxDEV5, Fragment } from "react/jsx-dev-runtime";
563
+ var STATUS_COLOR = {
564
+ paused: "gold",
565
+ waiting: "red"
566
+ };
567
+ var STATUS_RANK = {
568
+ paused: 0,
569
+ waiting: 1
570
+ };
571
+ function statusRank(status) {
572
+ return STATUS_RANK[status] ?? 99;
573
+ }
574
+ function recencyKey(s) {
575
+ return s.session.endedAt ?? s.session.startedAt;
576
+ }
577
+ function sessionRow(s) {
578
+ const status = s.session.status;
579
+ const color = STATUS_COLOR[status] ?? "gray";
580
+ const disabled = status === "waiting";
581
+ return {
582
+ value: `${s.groupPath}/${s.session.slug}`,
583
+ label: `${s.groupPath}/${s.session.slug} ${status}`,
584
+ meta: formatAgo(recencyKey(s)),
585
+ disabled,
586
+ display: /* @__PURE__ */ jsxDEV5(Fragment, {
587
+ children: [
588
+ /* @__PURE__ */ jsxDEV5(Text5, {
589
+ children: " "
590
+ }, undefined, false, undefined, this),
591
+ /* @__PURE__ */ jsxDEV5(Text5, {
592
+ color,
593
+ children: "\u25CF "
594
+ }, undefined, false, undefined, this),
595
+ /* @__PURE__ */ jsxDEV5(Text5, {
596
+ color,
597
+ dim: disabled,
598
+ children: status.padEnd(8)
599
+ }, undefined, false, undefined, this),
600
+ /* @__PURE__ */ jsxDEV5(Text5, {
601
+ children: " "
602
+ }, undefined, false, undefined, this),
603
+ /* @__PURE__ */ jsxDEV5(Text5, {
604
+ dim: disabled,
605
+ children: s.session.slug
606
+ }, undefined, false, undefined, this)
607
+ ]
608
+ }, undefined, true, undefined, this)
609
+ };
610
+ }
611
+ function groupHeader(groupPath) {
612
+ return {
613
+ value: `__group:${groupPath}`,
614
+ label: groupPath,
615
+ kind: "header"
616
+ };
617
+ }
618
+ function Launch({ onSelect }) {
619
+ const { exit } = useTui();
620
+ const [error, setError] = useState2(null);
621
+ const allSessions = useMemo2(() => getAllSessions({ excludeArchived: true }), []);
622
+ const visibleSessions = useMemo2(() => {
623
+ return allSessions.filter((s) => s.session.status === "paused" || s.session.status === "waiting").sort((a, b) => {
624
+ const g = a.groupPath.localeCompare(b.groupPath);
625
+ if (g !== 0)
626
+ return g;
627
+ const r = statusRank(a.session.status) - statusRank(b.session.status);
628
+ if (r !== 0)
629
+ return r;
630
+ return recencyKey(b).localeCompare(recencyKey(a));
631
+ });
632
+ }, [allSessions]);
633
+ const items = useMemo2(() => {
634
+ const rows = [];
635
+ let lastGroup = null;
636
+ for (const s of visibleSessions) {
637
+ if (s.groupPath !== lastGroup) {
638
+ rows.push(groupHeader(s.groupPath));
639
+ lastGroup = s.groupPath;
640
+ }
641
+ rows.push(sessionRow(s));
642
+ }
643
+ return rows;
644
+ }, [visibleSessions]);
645
+ const sessionByValue = useMemo2(() => {
646
+ const map = new Map;
647
+ for (const s of allSessions) {
648
+ map.set(`${s.groupPath}/${s.session.slug}`, s);
649
+ }
650
+ return map;
651
+ }, [allSessions]);
652
+ const select = (selection) => {
653
+ onSelect(selection);
654
+ exit();
655
+ };
656
+ const handleSubmit = (value) => {
657
+ const existing = sessionByValue.get(value);
658
+ if (existing) {
659
+ if (existing.session.status === "paused") {
660
+ select({ type: "pick", sessionId: existing.session.id });
661
+ return;
662
+ }
663
+ setError(`${value} is ${existing.session.status} \u2014 can't resume from here.`);
664
+ return;
665
+ }
666
+ try {
667
+ const { groupPath, slug } = parseSessionName(value);
668
+ select({ type: "create", groupPath, slug });
669
+ } catch (e) {
670
+ setError(e instanceof Error ? e.message : "Invalid name");
671
+ }
672
+ };
673
+ const counts = useMemo2(() => {
674
+ let paused = 0;
675
+ let waiting = 0;
676
+ for (const s of visibleSessions) {
677
+ if (s.session.status === "paused")
678
+ paused++;
679
+ else if (s.session.status === "waiting")
680
+ waiting++;
681
+ }
682
+ return { paused, waiting };
683
+ }, [visibleSessions]);
684
+ return /* @__PURE__ */ jsxDEV5(Box4, {
685
+ flexDirection: "column",
686
+ paddingY: 1,
687
+ gap: 1,
688
+ children: [
689
+ /* @__PURE__ */ jsxDEV5(Box4, {
690
+ marginX: 1,
691
+ children: /* @__PURE__ */ jsxDEV5(Logo, {}, undefined, false, undefined, this)
692
+ }, undefined, false, undefined, this),
693
+ /* @__PURE__ */ jsxDEV5(Box4, {
694
+ flexDirection: "column",
695
+ marginX: 2,
696
+ gap: 1,
697
+ children: [
698
+ /* @__PURE__ */ jsxDEV5(AppDetails, {}, undefined, false, undefined, this),
699
+ /* @__PURE__ */ jsxDEV5(Box4, {
700
+ flexDirection: "column",
701
+ gap: 1,
702
+ children: [
703
+ /* @__PURE__ */ jsxDEV5(Box4, {
704
+ flexDirection: "row",
705
+ gap: 1,
706
+ children: [
707
+ /* @__PURE__ */ jsxDEV5(Text5, {
708
+ bold: true,
709
+ children: "Sessions"
710
+ }, undefined, false, undefined, this),
711
+ visibleSessions.length === 0 ? /* @__PURE__ */ jsxDEV5(Text5, {
712
+ dim: true,
713
+ children: "\xB7 none \u2014 type group/slug to create"
714
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV5(Fragment, {
715
+ children: [
716
+ /* @__PURE__ */ jsxDEV5(Text5, {
717
+ dim: true,
718
+ children: "\xB7"
719
+ }, undefined, false, undefined, this),
720
+ /* @__PURE__ */ jsxDEV5(Text5, {
721
+ color: "gold",
722
+ children: [
723
+ counts.paused,
724
+ " paused"
725
+ ]
726
+ }, undefined, true, undefined, this),
727
+ counts.waiting > 0 && /* @__PURE__ */ jsxDEV5(Fragment, {
728
+ children: [
729
+ /* @__PURE__ */ jsxDEV5(Text5, {
730
+ dim: true,
731
+ children: "\xB7"
732
+ }, undefined, false, undefined, this),
733
+ /* @__PURE__ */ jsxDEV5(Text5, {
734
+ color: "red",
735
+ dim: true,
736
+ children: [
737
+ counts.waiting,
738
+ " waiting"
739
+ ]
740
+ }, undefined, true, undefined, this)
741
+ ]
742
+ }, undefined, true, undefined, this)
743
+ ]
744
+ }, undefined, true, undefined, this)
745
+ ]
746
+ }, undefined, true, undefined, this),
747
+ /* @__PURE__ */ jsxDEV5(Picker, {
748
+ mode: "single",
749
+ items,
750
+ isFocused: true,
751
+ placeholder: "Filter or type group/slug to create\u2026",
752
+ emptyHint: "No paused sessions. Type group/slug to create one.",
753
+ onSubmit: handleSubmit
754
+ }, undefined, false, undefined, this),
755
+ error && /* @__PURE__ */ jsxDEV5(Text5, {
756
+ color: "red",
757
+ children: error
758
+ }, undefined, false, undefined, this),
759
+ /* @__PURE__ */ jsxDEV5(Text5, {
760
+ dim: true,
761
+ children: "\u2191\u2193 navigate \xB7 enter continue/create \xB7 esc clear \xB7 ctrl+c quit"
762
+ }, undefined, false, undefined, this)
763
+ ]
764
+ }, undefined, true, undefined, this)
765
+ ]
766
+ }, undefined, true, undefined, this)
767
+ ]
768
+ }, undefined, true, undefined, this);
769
+ }
770
+
709
771
  // src/tui/screens/Exit.tsx
710
- import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
772
+ import { useState as useState3 } from "react";
773
+ import { Box as Box5, Text as Text6, useInput as useInput2, useTui as useTui2 } from "@orchetron/storm";
774
+
775
+ // src/db/queries/conversations.ts
776
+ import { eq as eq2, and as and2, desc, sql as sql3 } from "drizzle-orm";
777
+ function getConversationsBySession(sessionId) {
778
+ return getDb().select().from(conversations).where(and2(eq2(conversations.sessionId, sessionId), eq2(conversations.discarded, false))).orderBy(desc(conversations.startedAt)).all();
779
+ }
780
+
781
+ // src/tui/screens/Exit.tsx
782
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
711
783
  var OPTIONS = [
712
784
  { action: "save", label: "Save", hint: "Keep session paused for later" },
713
785
  {
@@ -727,11 +799,11 @@ var OPTIONS = [
727
799
  }
728
800
  ];
729
801
  function Exit({ sessionId, onAction }) {
730
- const { exit } = useTui3();
802
+ const { exit } = useTui2();
731
803
  const [cursor, setCursor] = useState3(0);
732
804
  const session = getSession(sessionId);
733
805
  const conversations2 = session ? getConversationsBySession(session.id) : [];
734
- useInput3((e) => {
806
+ useInput2((e) => {
735
807
  if (e.key === "c" && e.ctrl)
736
808
  exit();
737
809
  if (e.key === "up" || e.key === "k") {
@@ -748,37 +820,37 @@ function Exit({ sessionId, onAction }) {
748
820
  }
749
821
  });
750
822
  if (!session) {
751
- return /* @__PURE__ */ jsxDEV7(Text6, {
823
+ return /* @__PURE__ */ jsxDEV6(Text6, {
752
824
  color: "red",
753
825
  children: "Session not found"
754
826
  }, undefined, false, undefined, this);
755
827
  }
756
828
  const duration = session.endedAt && session.startedAt ? formatDuration(new Date(session.endedAt).getTime() - new Date(session.startedAt).getTime()) : null;
757
- return /* @__PURE__ */ jsxDEV7(Box6, {
829
+ return /* @__PURE__ */ jsxDEV6(Box5, {
758
830
  flexDirection: "column",
759
831
  padding: 1,
760
832
  gap: 1,
761
833
  children: [
762
- /* @__PURE__ */ jsxDEV7(Text6, {
834
+ /* @__PURE__ */ jsxDEV6(Text6, {
763
835
  bold: true,
764
836
  color: "#82AAFF",
765
837
  children: "Session ended"
766
838
  }, undefined, false, undefined, this),
767
- /* @__PURE__ */ jsxDEV7(Box6, {
839
+ /* @__PURE__ */ jsxDEV6(Box5, {
768
840
  flexDirection: "column",
769
841
  children: [
770
- /* @__PURE__ */ jsxDEV7(Box6, {
842
+ /* @__PURE__ */ jsxDEV6(Box5, {
771
843
  flexDirection: "row",
772
844
  gap: 1,
773
845
  children: [
774
- /* @__PURE__ */ jsxDEV7(StatusDot, {
846
+ /* @__PURE__ */ jsxDEV6(StatusDot, {
775
847
  status: session.status
776
848
  }, undefined, false, undefined, this),
777
- /* @__PURE__ */ jsxDEV7(Text6, {
849
+ /* @__PURE__ */ jsxDEV6(Text6, {
778
850
  bold: true,
779
851
  children: session.name
780
852
  }, undefined, false, undefined, this),
781
- duration && /* @__PURE__ */ jsxDEV7(Text6, {
853
+ duration && /* @__PURE__ */ jsxDEV6(Text6, {
782
854
  dim: true,
783
855
  children: [
784
856
  "(",
@@ -788,7 +860,7 @@ function Exit({ sessionId, onAction }) {
788
860
  }, undefined, true, undefined, this)
789
861
  ]
790
862
  }, undefined, true, undefined, this),
791
- /* @__PURE__ */ jsxDEV7(Text6, {
863
+ /* @__PURE__ */ jsxDEV6(Text6, {
792
864
  dim: true,
793
865
  children: [
794
866
  conversations2.length,
@@ -798,28 +870,28 @@ function Exit({ sessionId, onAction }) {
798
870
  }, undefined, true, undefined, this)
799
871
  ]
800
872
  }, undefined, true, undefined, this),
801
- /* @__PURE__ */ jsxDEV7(Box6, {
873
+ /* @__PURE__ */ jsxDEV6(Box5, {
802
874
  flexDirection: "column",
803
- children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV7(Box6, {
875
+ children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV6(Box5, {
804
876
  flexDirection: "row",
805
877
  gap: 1,
806
878
  children: [
807
- /* @__PURE__ */ jsxDEV7(Text6, {
879
+ /* @__PURE__ */ jsxDEV6(Text6, {
808
880
  children: i === cursor ? "\u276F" : " "
809
881
  }, undefined, false, undefined, this),
810
- /* @__PURE__ */ jsxDEV7(Text6, {
882
+ /* @__PURE__ */ jsxDEV6(Text6, {
811
883
  bold: i === cursor,
812
884
  children: opt.label
813
885
  }, undefined, false, undefined, this),
814
- /* @__PURE__ */ jsxDEV7(Text6, {
886
+ /* @__PURE__ */ jsxDEV6(Text6, {
815
887
  dim: true,
816
888
  children: opt.hint
817
889
  }, undefined, false, undefined, this)
818
890
  ]
819
891
  }, opt.action, true, undefined, this))
820
892
  }, undefined, false, undefined, this),
821
- /* @__PURE__ */ jsxDEV7(Box6, {
822
- children: /* @__PURE__ */ jsxDEV7(Text6, {
893
+ /* @__PURE__ */ jsxDEV6(Box5, {
894
+ children: /* @__PURE__ */ jsxDEV6(Text6, {
823
895
  dim: true,
824
896
  children: "\u2191\u2193 navigate \xB7 enter select \xB7 q save & quit"
825
897
  }, undefined, false, undefined, this)
@@ -830,10 +902,10 @@ function Exit({ sessionId, onAction }) {
830
902
 
831
903
  // src/tui/screens/Resume.tsx
832
904
  import { useState as useState4 } from "react";
833
- import { Box as Box7, Text as Text7, useInput as useInput4, useTui as useTui4 } from "@orchetron/storm";
834
- import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
905
+ import { Box as Box6, Text as Text7, useInput as useInput3, useTui as useTui3 } from "@orchetron/storm";
906
+ import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
835
907
  function Resume({ sessionId, onSelect }) {
836
- const { exit } = useTui4();
908
+ const { exit } = useTui3();
837
909
  const [cursor, setCursor] = useState4(0);
838
910
  const session = getSession(sessionId);
839
911
  const conversations2 = getConversationsBySession(sessionId);
@@ -842,7 +914,7 @@ function Resume({ sessionId, onSelect }) {
842
914
  onSelect(selection);
843
915
  exit();
844
916
  };
845
- useInput4((e) => {
917
+ useInput3((e) => {
846
918
  if (e.key === "c" && e.ctrl)
847
919
  select({ type: "back" });
848
920
  if (e.key === "up" || e.key === "k") {
@@ -861,17 +933,17 @@ function Resume({ sessionId, onSelect }) {
861
933
  }
862
934
  });
863
935
  if (!session) {
864
- return /* @__PURE__ */ jsxDEV8(Text7, {
936
+ return /* @__PURE__ */ jsxDEV7(Text7, {
865
937
  color: "red",
866
938
  children: "Session not found"
867
939
  }, undefined, false, undefined, this);
868
940
  }
869
- return /* @__PURE__ */ jsxDEV8(Box7, {
941
+ return /* @__PURE__ */ jsxDEV7(Box6, {
870
942
  flexDirection: "column",
871
943
  padding: 1,
872
944
  gap: 1,
873
945
  children: [
874
- /* @__PURE__ */ jsxDEV8(Text7, {
946
+ /* @__PURE__ */ jsxDEV7(Text7, {
875
947
  bold: true,
876
948
  color: "#82AAFF",
877
949
  children: [
@@ -879,17 +951,17 @@ function Resume({ sessionId, onSelect }) {
879
951
  session.name
880
952
  ]
881
953
  }, undefined, true, undefined, this),
882
- /* @__PURE__ */ jsxDEV8(Box7, {
954
+ /* @__PURE__ */ jsxDEV7(Box6, {
883
955
  flexDirection: "column",
884
956
  children: [
885
- /* @__PURE__ */ jsxDEV8(Box7, {
957
+ /* @__PURE__ */ jsxDEV7(Box6, {
886
958
  flexDirection: "row",
887
959
  gap: 1,
888
960
  children: [
889
- /* @__PURE__ */ jsxDEV8(Text7, {
961
+ /* @__PURE__ */ jsxDEV7(Text7, {
890
962
  children: cursor === 0 ? "\u276F" : " "
891
963
  }, undefined, false, undefined, this),
892
- /* @__PURE__ */ jsxDEV8(Text7, {
964
+ /* @__PURE__ */ jsxDEV7(Text7, {
893
965
  bold: cursor === 0,
894
966
  color: "#34D399",
895
967
  children: "+ New conversation"
@@ -901,42 +973,42 @@ function Resume({ sessionId, onSelect }) {
901
973
  const isSelected = cursor === idx;
902
974
  const duration = conv.endedAt ? formatDuration(new Date(conv.endedAt).getTime() - new Date(conv.startedAt).getTime()) : "active";
903
975
  const ago = formatAgo(conv.startedAt);
904
- return /* @__PURE__ */ jsxDEV8(Box7, {
976
+ return /* @__PURE__ */ jsxDEV7(Box6, {
905
977
  flexDirection: "row",
906
978
  gap: 1,
907
979
  children: [
908
- /* @__PURE__ */ jsxDEV8(Text7, {
980
+ /* @__PURE__ */ jsxDEV7(Text7, {
909
981
  children: isSelected ? "\u276F" : " "
910
982
  }, undefined, false, undefined, this),
911
- /* @__PURE__ */ jsxDEV8(Text7, {
983
+ /* @__PURE__ */ jsxDEV7(Text7, {
912
984
  bold: isSelected,
913
985
  dim: conv.discarded,
914
986
  children: conv.id.slice(0, 8)
915
987
  }, undefined, false, undefined, this),
916
- /* @__PURE__ */ jsxDEV8(Text7, {
988
+ /* @__PURE__ */ jsxDEV7(Text7, {
917
989
  dim: true,
918
990
  children: ago
919
991
  }, undefined, false, undefined, this),
920
- /* @__PURE__ */ jsxDEV8(Text7, {
992
+ /* @__PURE__ */ jsxDEV7(Text7, {
921
993
  dim: true,
922
994
  children: "\xB7"
923
995
  }, undefined, false, undefined, this),
924
- /* @__PURE__ */ jsxDEV8(Text7, {
996
+ /* @__PURE__ */ jsxDEV7(Text7, {
925
997
  dim: true,
926
998
  children: [
927
999
  conv.eventCount,
928
1000
  " events"
929
1001
  ]
930
1002
  }, undefined, true, undefined, this),
931
- /* @__PURE__ */ jsxDEV8(Text7, {
1003
+ /* @__PURE__ */ jsxDEV7(Text7, {
932
1004
  dim: true,
933
1005
  children: "\xB7"
934
1006
  }, undefined, false, undefined, this),
935
- /* @__PURE__ */ jsxDEV8(Text7, {
1007
+ /* @__PURE__ */ jsxDEV7(Text7, {
936
1008
  dim: true,
937
1009
  children: duration
938
1010
  }, undefined, false, undefined, this),
939
- conv.discarded && /* @__PURE__ */ jsxDEV8(Text7, {
1011
+ conv.discarded && /* @__PURE__ */ jsxDEV7(Text7, {
940
1012
  color: "red",
941
1013
  dim: true,
942
1014
  children: "(discarded)"
@@ -946,8 +1018,8 @@ function Resume({ sessionId, onSelect }) {
946
1018
  })
947
1019
  ]
948
1020
  }, undefined, true, undefined, this),
949
- /* @__PURE__ */ jsxDEV8(Box7, {
950
- children: /* @__PURE__ */ jsxDEV8(Text7, {
1021
+ /* @__PURE__ */ jsxDEV7(Box6, {
1022
+ children: /* @__PURE__ */ jsxDEV7(Text7, {
951
1023
  dim: true,
952
1024
  children: "\u2191\u2193 navigate \xB7 enter select \xB7 q back"
953
1025
  }, undefined, false, undefined, this)
@@ -957,7 +1029,7 @@ function Resume({ sessionId, onSelect }) {
957
1029
  }
958
1030
 
959
1031
  // src/tui/run-screen.tsx
960
- import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
1032
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
961
1033
  var [, , screen, outputPath, ...args] = process.argv;
962
1034
  if (!screen || !outputPath) {
963
1035
  console.error("Usage: run-screen <screen> <outputPath> [args...]");
@@ -967,7 +1039,7 @@ var result;
967
1039
  switch (screen) {
968
1040
  case "launch": {
969
1041
  let selection = { type: "quit" };
970
- const app = render(/* @__PURE__ */ jsxDEV9(Launch, {
1042
+ const app = render(/* @__PURE__ */ jsxDEV8(Launch, {
971
1043
  onSelect: (s) => {
972
1044
  selection = s;
973
1045
  }
@@ -984,7 +1056,7 @@ switch (screen) {
984
1056
  process.exit(1);
985
1057
  }
986
1058
  let action = "save";
987
- const app = render(/* @__PURE__ */ jsxDEV9(Exit, {
1059
+ const app = render(/* @__PURE__ */ jsxDEV8(Exit, {
988
1060
  sessionId,
989
1061
  onAction: (a) => {
990
1062
  action = a;
@@ -1002,7 +1074,7 @@ switch (screen) {
1002
1074
  process.exit(1);
1003
1075
  }
1004
1076
  let selection = { type: "back" };
1005
- const app = render(/* @__PURE__ */ jsxDEV9(Resume, {
1077
+ const app = render(/* @__PURE__ */ jsxDEV8(Resume, {
1006
1078
  sessionId,
1007
1079
  onSelect: (s) => {
1008
1080
  selection = s;