@uiid/bertrand 0.10.1 → 0.11.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 { useState as useState4, useCallback as useCallback4 } from "react";
23
- import { Box as Box9, Text as Text10, useTui } from "@orchetron/storm";
22
+ import { useCallback as useCallback2 } from "react";
23
+ import { Box as Box5, useTui as useTui2 } from "@orchetron/storm";
24
24
 
25
25
  // src/tui/components/app-details.tsx
26
26
  import { Box, Text } from "@orchetron/storm";
@@ -69,14 +69,230 @@ function Logo() {
69
69
  }, i, false, undefined, this))
70
70
  }, undefined, false, undefined, this);
71
71
  }
72
- // src/tui/components/no-sessions.tsx
73
- import { Box as Box3, Text as Text3 } from "@orchetron/storm";
74
-
75
- // src/tui/hooks/use-launch-sessions.ts
76
- import { useState, useCallback } from "react";
72
+ // src/tui/components/picker.tsx
73
+ import { useMemo, useState } from "react";
74
+ import { Box as Box3, Text as Text3, TextInput, useInput } from "@orchetron/storm";
75
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
76
+ var NEW_KEY = "__new__";
77
+ function Picker(props) {
78
+ const {
79
+ items,
80
+ isFocused,
81
+ placeholder,
82
+ allowCreate = true,
83
+ emptyHint,
84
+ maxVisible = 8
85
+ } = props;
86
+ const [filter, setFilter] = useState("");
87
+ const [cursor, setCursor] = useState(0);
88
+ const filtered = useMemo(() => {
89
+ const q = filter.trim().toLowerCase();
90
+ if (!q)
91
+ return items;
92
+ return items.filter((it) => it.label.toLowerCase().includes(q));
93
+ }, [filter, items]);
94
+ const exactMatch = useMemo(() => filtered.some((it) => it.label.toLowerCase() === filter.trim().toLowerCase()), [filter, filtered]);
95
+ const showCreate = allowCreate && filter.trim().length > 0 && !exactMatch;
96
+ 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);
99
+ }
100
+ useInput((e) => {
101
+ if (!isFocused)
102
+ return;
103
+ if (e.key === "escape" && filter.length > 0) {
104
+ setFilter("");
105
+ setCursor(0);
106
+ return;
107
+ }
108
+ if (visibleRows.length === 0)
109
+ return;
110
+ if (e.key === "up") {
111
+ setCursor((c) => Math.max(0, c - 1));
112
+ } else if (e.key === "down") {
113
+ setCursor((c) => Math.min(visibleRows.length - 1, c + 1));
114
+ } else if (e.key === "tab" && props.mode === "multi") {
115
+ props.onDone();
116
+ }
117
+ }, { isActive: isFocused });
118
+ const submitCursor = () => {
119
+ const row = visibleRows[cursor];
120
+ const value = row && row.value === NEW_KEY ? filter.trim() : row?.value ?? "";
121
+ if (!value)
122
+ return;
123
+ if (props.mode === "single") {
124
+ props.onSubmit(value);
125
+ setFilter("");
126
+ setCursor(0);
127
+ } else {
128
+ props.onToggle(value);
129
+ setFilter("");
130
+ setCursor(0);
131
+ }
132
+ };
133
+ const handleSubmit = (text) => {
134
+ if (visibleRows.length === 0) {
135
+ if (allowCreate && text.trim() && props.mode === "single") {
136
+ props.onSubmit(text.trim());
137
+ setFilter("");
138
+ }
139
+ return;
140
+ }
141
+ submitCursor();
142
+ };
143
+ const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), visibleRows.length - maxVisible));
144
+ const visibleEnd = Math.min(visibleRows.length, visibleStart + maxVisible);
145
+ const slice = visibleRows.slice(visibleStart, visibleEnd);
146
+ const selectedSet = props.mode === "multi" ? new Set(props.selected) : new Set;
147
+ return /* @__PURE__ */ jsxDEV3(Box3, {
148
+ flexDirection: "column",
149
+ gap: 0,
150
+ width: "100%",
151
+ children: [
152
+ props.mode === "multi" && props.selected.length > 0 && /* @__PURE__ */ jsxDEV3(Box3, {
153
+ flexDirection: "row",
154
+ gap: 1,
155
+ children: [
156
+ /* @__PURE__ */ jsxDEV3(Text3, {
157
+ dim: true,
158
+ children: "Selected:"
159
+ }, undefined, false, undefined, this),
160
+ props.selected.map((v) => {
161
+ const item = items.find((i) => i.value === v);
162
+ return /* @__PURE__ */ jsxDEV3(Text3, {
163
+ color: item?.color ?? "cyan",
164
+ children: item?.label ?? v
165
+ }, v, false, undefined, this);
166
+ })
167
+ ]
168
+ }, undefined, true, undefined, this),
169
+ /* @__PURE__ */ jsxDEV3(Box3, {
170
+ borderStyle: "round",
171
+ borderColor: isFocused ? "green" : undefined,
172
+ borderDimColor: !isFocused,
173
+ paddingX: 1,
174
+ children: /* @__PURE__ */ jsxDEV3(TextInput, {
175
+ value: filter,
176
+ onChange: setFilter,
177
+ onSubmit: handleSubmit,
178
+ placeholder,
179
+ color: "green",
180
+ placeholderColor: "gray",
181
+ isFocused
182
+ }, undefined, false, undefined, this)
183
+ }, undefined, false, undefined, this),
184
+ /* @__PURE__ */ jsxDEV3(Box3, {
185
+ flexDirection: "column",
186
+ borderStyle: "round",
187
+ borderDimColor: true,
188
+ paddingX: 1,
189
+ children: [
190
+ visibleStart > 0 && /* @__PURE__ */ jsxDEV3(Text3, {
191
+ dim: true,
192
+ children: [
193
+ "\u25B2 ",
194
+ visibleStart,
195
+ " more above"
196
+ ]
197
+ }, undefined, true, undefined, this),
198
+ slice.length === 0 && emptyHint && /* @__PURE__ */ jsxDEV3(Text3, {
199
+ dim: true,
200
+ children: emptyHint
201
+ }, undefined, false, undefined, this),
202
+ slice.map((row, i) => {
203
+ const idx = visibleStart + i;
204
+ const isCursor = idx === cursor && isFocused;
205
+ const isNew = row.value === NEW_KEY;
206
+ const showDivider = isNew && filtered.length > 0 && i > 0;
207
+ if (isNew) {
208
+ return /* @__PURE__ */ jsxDEV3(Box3, {
209
+ flexDirection: "column",
210
+ children: [
211
+ showDivider && /* @__PURE__ */ jsxDEV3(Text3, {
212
+ dim: true,
213
+ children: "\u2500".repeat(Math.min(20, filter.length + 12))
214
+ }, undefined, false, undefined, this),
215
+ /* @__PURE__ */ jsxDEV3(Box3, {
216
+ flexDirection: "row",
217
+ gap: 1,
218
+ backgroundColor: isCursor ? "green" : undefined,
219
+ children: /* @__PURE__ */ jsxDEV3(Text3, {
220
+ color: isCursor ? "black" : "cyan",
221
+ bold: true,
222
+ children: [
223
+ "\u271A create \u201C",
224
+ filter.trim(),
225
+ "\u201D"
226
+ ]
227
+ }, undefined, true, undefined, this)
228
+ }, undefined, false, undefined, this)
229
+ ]
230
+ }, "__new__", true, undefined, this);
231
+ }
232
+ const item = row;
233
+ const isSelected = selectedSet.has(item.value);
234
+ const marker = props.mode === "multi" ? isSelected ? "\u2713 " : " " : "";
235
+ return /* @__PURE__ */ jsxDEV3(Box3, {
236
+ flexDirection: "row",
237
+ justifyContent: "space-between",
238
+ gap: 1,
239
+ backgroundColor: isCursor ? "green" : undefined,
240
+ 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),
249
+ item.meta && /* @__PURE__ */ jsxDEV3(Box3, {
250
+ paddingX: 1,
251
+ backgroundColor: isCursor ? "black" : "#3a3a3a",
252
+ children: /* @__PURE__ */ jsxDEV3(Text3, {
253
+ color: isCursor ? "green" : "white",
254
+ bold: true,
255
+ children: item.meta
256
+ }, undefined, false, undefined, this)
257
+ }, undefined, false, undefined, this)
258
+ ]
259
+ }, item.value, true, undefined, this);
260
+ }),
261
+ visibleEnd < visibleRows.length && /* @__PURE__ */ jsxDEV3(Text3, {
262
+ dim: true,
263
+ children: [
264
+ "\u25BC ",
265
+ visibleRows.length - visibleEnd,
266
+ " more below"
267
+ ]
268
+ }, undefined, true, undefined, this)
269
+ ]
270
+ }, undefined, true, undefined, this)
271
+ ]
272
+ }, undefined, true, undefined, this);
273
+ }
274
+ // src/tui/components/status-dot.tsx
275
+ import { Text as Text4 } from "@orchetron/storm";
276
+ import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
277
+ var STATUS_COLORS = {
278
+ active: "orange",
279
+ waiting: "red",
280
+ paused: "gold",
281
+ archived: "purple"
282
+ };
283
+ function StatusDot({ status }) {
284
+ const color = STATUS_COLORS[status] ?? "#6B7280";
285
+ return /* @__PURE__ */ jsxDEV4(Text4, {
286
+ color,
287
+ children: "\u25CF"
288
+ }, undefined, false, undefined, this);
289
+ }
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";
77
293
 
78
- // src/db/queries/sessions.ts
79
- import { eq, and, inArray, sql as sql2 } from "drizzle-orm";
294
+ // src/db/queries/groups.ts
295
+ import { eq, like, or, isNull } from "drizzle-orm";
80
296
 
81
297
  // src/db/client.ts
82
298
  import { Database } from "bun:sqlite";
@@ -225,17 +441,20 @@ function getDb() {
225
441
 
226
442
  // src/lib/id.ts
227
443
  import { nanoid } from "nanoid";
228
- function createId(size = 12) {
229
- return nanoid(size);
444
+
445
+ // src/db/queries/groups.ts
446
+ function getAllGroups() {
447
+ return getDb().select().from(groups).all();
230
448
  }
231
449
 
232
450
  // src/db/queries/sessions.ts
451
+ import { eq as eq2, and, inArray, sql as sql2 } from "drizzle-orm";
233
452
  function getSession(id) {
234
- return getDb().select().from(sessions).where(eq(sessions.id, id)).get();
453
+ return getDb().select().from(sessions).where(eq2(sessions.id, id)).get();
235
454
  }
236
455
  function getAllSessions(opts) {
237
456
  const db = getDb();
238
- const query = db.select({ session: sessions, groupPath: groups.path }).from(sessions).innerJoin(groups, eq(sessions.groupId, groups.id));
457
+ const query = db.select({ session: sessions, groupPath: groups.path }).from(sessions).innerJoin(groups, eq2(sessions.groupId, groups.id));
239
458
  if (opts?.excludeArchived) {
240
459
  return query.where(inArray(sessions.status, [
241
460
  "active",
@@ -245,739 +464,198 @@ function getAllSessions(opts) {
245
464
  }
246
465
  return query.all();
247
466
  }
248
- function updateSessionStatus(id, status) {
249
- return getDb().update(sessions).set({ status, updatedAt: sql2`(datetime('now'))` }).where(eq(sessions.id, id)).returning().get();
250
- }
251
- function renameSession(id, slug, name) {
252
- return getDb().update(sessions).set({ slug, name: name ?? slug, updatedAt: sql2`(datetime('now'))` }).where(eq(sessions.id, id)).returning().get();
253
- }
254
- function moveSession(id, groupId) {
255
- return getDb().update(sessions).set({ groupId, updatedAt: sql2`(datetime('now'))` }).where(eq(sessions.id, id)).returning().get();
256
- }
257
- function deleteSession(id) {
258
- return getDb().delete(sessions).where(eq(sessions.id, id)).run();
259
- }
260
467
 
261
- // src/tui/hooks/use-launch-sessions.ts
262
- function useLaunchSessions() {
263
- const [cursor, setCursor] = useState(0);
264
- const [showArchived, setShowArchived] = useState(false);
265
- const [refreshKey, setRefreshKey] = useState(0);
266
- const refresh = useCallback(() => setRefreshKey((k) => k + 1), []);
267
- const sessionRows = getAllSessions(showArchived ? undefined : { excludeArchived: true });
268
- const selected = sessionRows[cursor];
269
- const toggleArchived = useCallback(() => {
270
- setShowArchived((s) => !s);
271
- setCursor(0);
272
- }, []);
273
- return {
274
- sessionRows,
275
- selected,
276
- cursor,
277
- setCursor,
278
- showArchived,
279
- toggleArchived,
280
- refresh
281
- };
282
- }
283
-
284
- // src/tui/components/no-sessions.tsx
285
- import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
286
- // src/tui/components/session-row.tsx
287
- import { Box as Box4, Text as Text5 } from "@orchetron/storm";
288
-
289
- // src/lib/format.ts
290
- var SECOND = 1000;
291
- var MINUTE = 60 * SECOND;
292
- var HOUR = 60 * MINUTE;
293
- var DAY = 24 * HOUR;
294
- function formatDuration(ms) {
295
- if (ms < MINUTE)
296
- return `${Math.round(ms / SECOND)}s`;
297
- const days = Math.floor(ms / DAY);
298
- const hours = Math.floor(ms % DAY / HOUR);
299
- const minutes = Math.floor(ms % HOUR / MINUTE);
300
- if (days > 0)
301
- return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
302
- if (hours > 0)
303
- return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
304
- return `${minutes}m`;
305
- }
306
- function formatAgo(isoOrDate) {
307
- const date = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
308
- const ms = Date.now() - date.getTime();
309
- if (ms < MINUTE)
310
- return "just now";
311
- if (ms < HOUR)
312
- return `${Math.floor(ms / MINUTE)}m ago`;
313
- if (ms < DAY)
314
- return `${Math.floor(ms / HOUR)}h ago`;
315
- if (ms < 2 * DAY)
316
- return "yesterday";
317
- if (ms < 7 * DAY)
318
- return `${Math.floor(ms / DAY)}d ago`;
319
- return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
320
- }
321
-
322
- // src/tui/components/status-dot.tsx
323
- import { Text as Text4 } from "@orchetron/storm";
324
- import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
325
- var STATUS_COLORS = {
326
- active: "orange",
327
- waiting: "red",
328
- paused: "gold",
329
- archived: "purple"
330
- };
331
- function StatusDot({ status }) {
332
- const color = STATUS_COLORS[status] ?? "#6B7280";
333
- return /* @__PURE__ */ jsxDEV4(Text4, {
334
- color,
335
- children: "\u25CF"
336
- }, undefined, false, undefined, this);
337
- }
338
-
339
- // src/tui/components/session-row.tsx
468
+ // src/tui/screens/launch/create-screen.tsx
340
469
  import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
341
- function SessionRow({
342
- name,
343
- status,
344
- updatedAt,
345
- selected
346
- }) {
347
- return /* @__PURE__ */ jsxDEV5(Box4, {
348
- flexDirection: "row",
349
- gap: 1,
350
- children: [
351
- /* @__PURE__ */ jsxDEV5(Text5, {
352
- color: "green",
353
- children: selected ? "\u276F " : " "
354
- }, undefined, false, undefined, this),
355
- /* @__PURE__ */ jsxDEV5(StatusDot, {
356
- status
357
- }, undefined, false, undefined, this),
358
- /* @__PURE__ */ jsxDEV5(Text5, {
359
- bold: selected,
360
- color: selected ? "green" : undefined,
361
- children: name
362
- }, undefined, false, undefined, this),
363
- /* @__PURE__ */ jsxDEV5(Text5, {
364
- color: selected ? "green" : undefined,
365
- dim: true,
366
- children: formatAgo(updatedAt)
367
- }, undefined, false, undefined, this)
368
- ]
369
- }, undefined, true, undefined, this);
370
- }
371
- // src/tui/hooks/use-launch-actions.ts
372
- import { useCallback as useCallback2 } from "react";
373
-
374
- // src/db/queries/conversations.ts
375
- import { eq as eq2, and as and2, desc, sql as sql3 } from "drizzle-orm";
376
- function getConversationsBySession(sessionId) {
377
- return getDb().select().from(conversations).where(and2(eq2(conversations.sessionId, sessionId), eq2(conversations.discarded, false))).orderBy(desc(conversations.startedAt)).all();
378
- }
379
-
380
- // src/db/queries/groups.ts
381
- import { eq as eq3, like, or, isNull } from "drizzle-orm";
382
- function createGroup(opts) {
383
- const db = getDb();
384
- const id = createId();
385
- let path = opts.slug;
386
- let depth = 0;
387
- if (opts.parentId) {
388
- const parent = db.select().from(groups).where(eq3(groups.id, opts.parentId)).get();
389
- if (!parent)
390
- throw new Error(`Parent group ${opts.parentId} not found`);
391
- path = `${parent.path}/${opts.slug}`;
392
- depth = parent.depth + 1;
393
- }
394
- return db.insert(groups).values({ id, slug: opts.slug, name: opts.name, parentId: opts.parentId, path, depth }).returning().get();
395
- }
396
- function getGroupByPath(path) {
397
- return getDb().select().from(groups).where(eq3(groups.path, path)).get();
398
- }
399
- function getOrCreateGroupPath(path) {
400
- const existing = getGroupByPath(path);
401
- if (existing)
402
- return existing.id;
403
- const segments = path.split("/");
404
- let parentId;
405
- for (let i = 0;i < segments.length; i++) {
406
- const partialPath = segments.slice(0, i + 1).join("/");
407
- const group = getGroupByPath(partialPath);
408
- if (group) {
409
- parentId = group.id;
410
- } else {
411
- const slug = segments[i];
412
- const created = createGroup({ slug, name: slug, parentId });
413
- parentId = created.id;
414
- }
415
- }
416
- return parentId;
417
- }
418
-
419
- // src/lib/parse-session-name.ts
420
- function parseSessionName(input) {
421
- const trimmed = input.trim().replace(/^\/+|\/+$/g, "");
422
- if (!trimmed) {
423
- throw new Error("Session name cannot be empty");
424
- }
425
- const segments = trimmed.split("/").filter(Boolean);
426
- if (segments.length < 2) {
427
- throw new Error(`Session name must include at least one group: "group/session" (got "${trimmed}")`);
428
- }
429
- for (const segment of segments) {
430
- if (!/^[a-z0-9][a-z0-9._-]*$/i.test(segment)) {
431
- throw new Error(`Invalid segment "${segment}": must start with alphanumeric and contain only letters, digits, dots, underscores, or dashes`);
432
- }
433
- }
434
- const slug = segments[segments.length - 1];
435
- const groupPath = segments.slice(0, -1).join("/");
436
- return { groupPath, slug };
437
- }
438
-
439
- // src/tui/hooks/use-launch-actions.ts
440
- function useLaunchActions({
441
- selected,
442
- select,
443
- refresh,
444
- setMode,
445
- setCursor,
446
- setNewName,
447
- setEditValue,
448
- setError
470
+ function CreateScreen({
471
+ isFocused,
472
+ onSubmit,
473
+ onQuit
449
474
  }) {
450
- const handleResume = useCallback2(() => {
451
- if (!selected)
452
- return;
453
- const conversations2 = getConversationsBySession(selected.session.id);
454
- if (conversations2.length === 1) {
455
- select({
456
- type: "resume",
457
- sessionId: selected.session.id,
458
- conversationId: conversations2[0].id
459
- });
460
- } else {
461
- select({ type: "pick", sessionId: selected.session.id });
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);
462
490
  }
463
- }, [selected, select]);
464
- const handleArchiveToggle = useCallback2(() => {
465
- if (!selected)
466
- return;
467
- const newStatus = selected.session.status === "archived" ? "paused" : "archived";
468
- updateSessionStatus(selected.session.id, newStatus);
469
- refresh();
470
- }, [selected, refresh]);
471
- const handleConfirmDelete = useCallback2(() => {
472
- if (!selected)
473
- return;
474
- deleteSession(selected.session.id);
475
- setCursor((c) => Math.max(0, c - 1));
476
- setMode("browse");
477
- refresh();
478
- }, [selected, refresh, setCursor, setMode]);
479
- const handleStartRename = useCallback2(() => {
480
- if (!selected)
481
- return;
482
- setMode("rename");
483
- setEditValue(selected.session.slug);
484
- setError(null);
485
- }, [selected, setMode, setEditValue, setError]);
486
- const handleStartMove = useCallback2(() => {
487
- if (!selected)
488
- return;
489
- setMode("move");
490
- setEditValue(selected.groupPath);
491
- setError(null);
492
- }, [selected, setMode, setEditValue, setError]);
493
- const handleStartCreate = useCallback2(() => {
494
- setMode("create");
495
- setNewName("");
496
- setError(null);
497
- }, [setMode, setNewName, setError]);
498
- const cancelToBrowse = useCallback2(() => {
499
- setMode("browse");
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
500
  setError(null);
501
- }, [setMode, setError]);
502
- const handleCreateSubmit = useCallback2((value) => {
503
- if (!value.trim())
501
+ setStep("slug");
502
+ }, []);
503
+ const handleSlugSubmit = useCallback((value) => {
504
+ if (stepRef.current !== "slug")
504
505
  return;
505
- try {
506
- const { groupPath, slug } = parseSessionName(value);
507
- select({ type: "create", groupPath, slug });
508
- } catch (e) {
509
- setError(e instanceof Error ? e.message : String(e));
510
- }
511
- }, [select, setError]);
512
- const handleRenameSubmit = useCallback2((value) => {
513
506
  const trimmed = value.trim();
514
- if (!trimmed || !selected)
507
+ if (!trimmed) {
508
+ setError("Name cannot be empty.");
515
509
  return;
510
+ }
516
511
  if (!/^[a-z0-9][a-z0-9._-]*$/i.test(trimmed)) {
517
- setError("Invalid name: must start with alphanumeric, contain only letters, digits, dots, underscores, or dashes");
512
+ setError("Name must start alphanumeric; letters, digits, dots, underscores, dashes only.");
518
513
  return;
519
514
  }
520
- renameSession(selected.session.id, trimmed);
521
- setMode("browse");
522
- refresh();
523
- }, [selected, setError, setMode, refresh]);
524
- const handleMoveSubmit = useCallback2((value) => {
525
- const trimmed = value.trim().replace(/^\/+|\/+$/g, "");
526
- if (!trimmed || !selected)
515
+ if (!groupPath)
527
516
  return;
528
- try {
529
- const groupId = getOrCreateGroupPath(trimmed);
530
- moveSession(selected.session.id, groupId);
531
- setMode("browse");
532
- refresh();
533
- } catch (e) {
534
- setError(e instanceof Error ? e.message : String(e));
535
- }
536
- }, [selected, setMode, refresh, setError]);
537
- return {
538
- handleResume,
539
- handleArchiveToggle,
540
- handleConfirmDelete,
541
- handleStartRename,
542
- handleStartMove,
543
- handleStartCreate,
544
- cancelToBrowse,
545
- handleCreateSubmit,
546
- handleRenameSubmit,
547
- handleMoveSubmit
548
- };
549
- }
550
- // src/tui/hooks/use-launch-hotkeys.ts
551
- import { useCallback as useCallback3, useRef, useState as useState2 } from "react";
552
- import { useHotkey, useInput } from "@orchetron/storm";
553
- function useLaunchHotkeys({
554
- mode,
555
- selected,
556
- sessionCount,
557
- quit,
558
- setCursor,
559
- toggleArchived,
560
- handleResume,
561
- handleStartCreate,
562
- handleArchiveToggle,
563
- handleStartRename,
564
- handleStartMove,
565
- handleConfirmDelete,
566
- cancelToBrowse,
567
- setMode
568
- }) {
569
- useInput((e) => {
570
- if (e.key === "c" && e.ctrl) {
571
- e.consumed = true;
572
- quit();
517
+ onSubmit({ groupPath, slug: trimmed });
518
+ }, [groupPath, onSubmit]);
519
+ useInput2((e) => {
520
+ if (!isFocused)
573
521
  return;
574
- }
575
- if (e.key === "up" || e.key === "k") {
576
- setCursor((c) => Math.max(0, c - 1));
577
- } else if (e.key === "down" || e.key === "j") {
578
- setCursor((c) => Math.min(sessionCount - 1, c + 1));
579
- } else if (e.key === "return") {
580
- handleResume();
581
- } else if (e.key === "n") {
582
- handleStartCreate();
583
- } else if (e.key === "a") {
584
- handleArchiveToggle();
585
- } else if (e.key === "d" && selected) {
586
- setMode("confirm-delete");
587
- } else if (e.key === "r") {
588
- handleStartRename();
589
- } else if (e.key === "m") {
590
- handleStartMove();
591
- } else if (e.key === "tab") {
592
- toggleArchived();
593
- } else if (e.key === "q") {
594
- quit();
595
- } else {
522
+ if (e.key === "c" && e.ctrl) {
523
+ onQuit();
596
524
  return;
597
525
  }
598
- e.consumed = true;
599
- }, { isActive: mode === "browse", priority: 1 });
600
- const browseBindings = [
601
- { label: "navigate", description: "\u2191\u2193" },
602
- { label: "resume", description: "return" },
603
- { label: "new", description: "n" },
604
- { label: "archive", description: "a" },
605
- { label: "delete", description: "d" },
606
- { label: "rename", description: "r" },
607
- { label: "move", description: "m" },
608
- { label: "quit", description: "q" }
609
- ];
610
- const escPendingRef = useRef(false);
611
- const [escPending, setEscPending] = useState2(false);
612
- const clearEscPending = useCallback3(() => {
613
- if (escPendingRef.current) {
614
- escPendingRef.current = false;
615
- setEscPending(false);
526
+ if (e.key === "escape" && step === "slug") {
527
+ setStep("group");
528
+ setSlug("");
529
+ setError(null);
616
530
  }
617
- }, []);
618
- const { bindings: inputBindings } = useHotkey({
619
- hotkeys: [
620
- {
621
- key: "escape",
622
- label: "cancel",
623
- action: () => {
624
- if (escPendingRef.current) {
625
- escPendingRef.current = false;
626
- setEscPending(false);
627
- cancelToBrowse();
628
- } else {
629
- escPendingRef.current = true;
630
- setEscPending(true);
631
- }
632
- }
633
- }
634
- ],
635
- isActive: mode === "create" || mode === "rename" || mode === "move"
636
- });
637
- const { bindings: deleteBindings } = useHotkey({
638
- hotkeys: [
639
- { key: "y", label: "confirm", action: handleConfirmDelete },
640
- { key: "escape", label: "cancel", action: () => setMode("browse") }
641
- ],
642
- isActive: mode === "confirm-delete"
643
- });
644
- return {
645
- browseBindings,
646
- inputBindings,
647
- deleteBindings,
648
- escPending,
649
- clearEscPending
650
- };
651
- }
652
- // src/tui/screens/launch/launch.constants.ts
653
- var KEY_DISPLAY = {
654
- up: "\u2191",
655
- down: "\u2193",
656
- return: "enter",
657
- escape: "esc"
658
- };
659
-
660
- // src/tui/screens/launch/launch.utils.ts
661
- function formatBindings(bindings) {
662
- return bindings.filter((b) => b.label).map((b) => `${KEY_DISPLAY[b.description] ?? b.description} ${b.label}`).join(" \xB7 ");
663
- }
664
-
665
- // src/tui/screens/launch/create-session.tsx
666
- import { useState as useState3 } from "react";
667
- import { Box as Box5, Text as Text6, TextInput } from "@orchetron/storm";
668
- import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
669
- function Create({ isFocused, setError, handleCreateSubmit, error }) {
670
- const [newName, setNewName] = useState3("");
671
- const handleChange = (v) => {
672
- setNewName(v);
673
- setError(null);
674
- };
675
- return /* @__PURE__ */ jsxDEV6(Box5, {
676
- flexDirection: "column",
677
- children: [
678
- /* @__PURE__ */ jsxDEV6(Text6, {
679
- children: "Create a session:"
680
- }, undefined, false, undefined, this),
681
- /* @__PURE__ */ jsxDEV6(TextInput, {
682
- value: newName,
683
- onChange: handleChange,
684
- onSubmit: handleCreateSubmit,
685
- placeholder: "group/session-name",
686
- placeholderColor: "green",
687
- color: "green",
688
- isFocused
689
- }, undefined, false, undefined, this),
690
- error && /* @__PURE__ */ jsxDEV6(Text6, {
691
- color: "red",
692
- children: error
693
- }, undefined, false, undefined, this)
694
- ]
695
- }, undefined, true, undefined, this);
696
- }
697
-
698
- // src/tui/screens/launch/rename.tsx
699
- import { Box as Box6, Text as Text7, TextInput as TextInput2 } from "@orchetron/storm";
700
- import { jsxDEV as jsxDEV7, Fragment } from "react/jsx-dev-runtime";
701
- function Rename({
702
- editValue,
703
- setEditValue,
704
- setError,
705
- handleRenameSubmit,
706
- inputBindings,
707
- error,
708
- escPending,
709
- clearEscPending,
710
- placeholder
711
- }) {
712
- return /* @__PURE__ */ jsxDEV7(Box6, {
531
+ }, { isActive: isFocused });
532
+ return /* @__PURE__ */ jsxDEV5(Box4, {
713
533
  flexDirection: "column",
714
- padding: 1,
534
+ gap: 1,
715
535
  children: [
716
- /* @__PURE__ */ jsxDEV7(Text7, {
717
- bold: true,
718
- color: "#82AAFF",
719
- children: "Rename session"
720
- }, undefined, false, undefined, this),
721
- /* @__PURE__ */ jsxDEV7(Box6, {
722
- height: 1
723
- }, undefined, false, undefined, this),
724
- /* @__PURE__ */ jsxDEV7(Text7, {
725
- children: "New name: "
726
- }, undefined, false, undefined, this),
727
- /* @__PURE__ */ jsxDEV7(TextInput2, {
728
- value: editValue,
729
- onChange: (v) => {
730
- setEditValue(v);
731
- setError(null);
732
- clearEscPending();
733
- },
734
- onSubmit: handleRenameSubmit,
735
- placeholder
736
- }, undefined, false, undefined, this),
737
- error && /* @__PURE__ */ jsxDEV7(Fragment, {
536
+ /* @__PURE__ */ jsxDEV5(Box4, {
537
+ flexDirection: "row",
538
+ gap: 1,
738
539
  children: [
739
- /* @__PURE__ */ jsxDEV7(Box6, {
740
- height: 1
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"
741
547
  }, undefined, false, undefined, this),
742
- /* @__PURE__ */ jsxDEV7(Text7, {
743
- color: "red",
744
- children: error
548
+ /* @__PURE__ */ jsxDEV5(Text5, {
549
+ dim: true,
550
+ children: stepLabel(step)
745
551
  }, undefined, false, undefined, this)
746
552
  ]
747
553
  }, undefined, true, undefined, this),
748
- /* @__PURE__ */ jsxDEV7(Box6, {
749
- height: 1
750
- }, undefined, false, undefined, this),
751
- escPending ? /* @__PURE__ */ jsxDEV7(Text7, {
752
- color: "yellow",
753
- children: "Press esc again to cancel"
754
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV7(Text7, {
755
- dim: true,
756
- children: [
757
- "enter save \xB7 ",
758
- formatBindings(inputBindings)
759
- ]
760
- }, undefined, true, undefined, this)
761
- ]
762
- }, undefined, true, undefined, this);
763
- }
764
-
765
- // src/tui/screens/launch/move.tsx
766
- import { Box as Box7, Text as Text8, TextInput as TextInput3 } from "@orchetron/storm";
767
- import { jsxDEV as jsxDEV8, Fragment as Fragment2 } from "react/jsx-dev-runtime";
768
- function Move({
769
- editValue,
770
- setEditValue,
771
- setError,
772
- handleMoveSubmit,
773
- inputBindings,
774
- error,
775
- escPending,
776
- clearEscPending,
777
- placeholder = "category/session-name"
778
- }) {
779
- return /* @__PURE__ */ jsxDEV8(Box7, {
780
- flexDirection: "column",
781
- padding: 1,
782
- children: [
783
- /* @__PURE__ */ jsxDEV8(Text8, {
784
- bold: true,
785
- color: "#82AAFF",
786
- children: "Move session"
787
- }, undefined, false, undefined, this),
788
- /* @__PURE__ */ jsxDEV8(Box7, {
789
- height: 1
790
- }, undefined, false, undefined, this),
791
- /* @__PURE__ */ jsxDEV8(Text8, {
792
- children: "New group path: "
793
- }, undefined, false, undefined, this),
794
- /* @__PURE__ */ jsxDEV8(TextInput3, {
795
- value: editValue,
796
- onChange: (v) => {
797
- setEditValue(v);
798
- setError(null);
799
- clearEscPending();
800
- },
801
- onSubmit: handleMoveSubmit,
802
- placeholder
803
- }, undefined, false, undefined, this),
804
- error && /* @__PURE__ */ jsxDEV8(Fragment2, {
554
+ groupPath && step === "slug" && /* @__PURE__ */ jsxDEV5(Box4, {
555
+ flexDirection: "row",
556
+ gap: 1,
805
557
  children: [
806
- /* @__PURE__ */ jsxDEV8(Box7, {
807
- height: 1
558
+ /* @__PURE__ */ jsxDEV5(Text5, {
559
+ dim: true,
560
+ children: "Group:"
808
561
  }, undefined, false, undefined, this),
809
- /* @__PURE__ */ jsxDEV8(Text8, {
810
- color: "red",
811
- children: error
562
+ /* @__PURE__ */ jsxDEV5(Text5, {
563
+ color: "green",
564
+ children: groupPath
812
565
  }, undefined, false, undefined, this)
813
566
  ]
814
567
  }, undefined, true, undefined, this),
815
- /* @__PURE__ */ jsxDEV8(Box7, {
816
- height: 1
817
- }, undefined, false, undefined, this),
818
- escPending ? /* @__PURE__ */ jsxDEV8(Text8, {
819
- color: "yellow",
820
- children: "Press esc again to cancel"
821
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV8(Text8, {
822
- dim: true,
823
- children: [
824
- "enter move \xB7 ",
825
- formatBindings(inputBindings)
826
- ]
827
- }, undefined, true, undefined, this)
828
- ]
829
- }, undefined, true, undefined, this);
830
- }
831
-
832
- // src/tui/screens/launch/confirm-delete.tsx
833
- import { Box as Box8, Text as Text9 } from "@orchetron/storm";
834
- import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
835
- function ConfirmDelete({ sessionName, deleteBindings }) {
836
- return /* @__PURE__ */ jsxDEV9(Box8, {
837
- flexDirection: "column",
838
- padding: 1,
839
- children: [
840
- /* @__PURE__ */ jsxDEV9(Text9, {
841
- bold: true,
842
- color: "red",
843
- children: "Delete session?"
844
- }, undefined, false, undefined, this),
845
- /* @__PURE__ */ jsxDEV9(Box8, {
846
- height: 1
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)
847
580
  }, undefined, false, undefined, this),
848
- /* @__PURE__ */ jsxDEV9(Text9, {
581
+ /* @__PURE__ */ jsxDEV5(Box4, {
582
+ flexDirection: "column",
583
+ gap: 0,
584
+ height: step === "slug" ? undefined : 0,
585
+ overflow: "hidden",
849
586
  children: [
850
- "This will permanently delete",
851
- " ",
852
- /* @__PURE__ */ jsxDEV9(Text9, {
853
- bold: true,
854
- children: sessionName
587
+ /* @__PURE__ */ jsxDEV5(Text5, {
588
+ dim: true,
589
+ children: "Name"
855
590
  }, undefined, false, undefined, this),
856
- " ",
857
- "and all its conversations and events."
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)
858
609
  ]
859
610
  }, undefined, true, undefined, this),
860
- /* @__PURE__ */ jsxDEV9(Box8, {
861
- height: 1
611
+ error && /* @__PURE__ */ jsxDEV5(Text5, {
612
+ color: "red",
613
+ children: error
862
614
  }, undefined, false, undefined, this),
863
- /* @__PURE__ */ jsxDEV9(Text9, {
615
+ /* @__PURE__ */ jsxDEV5(Text5, {
864
616
  dim: true,
865
- children: formatBindings(deleteBindings)
617
+ children: footer(step)
866
618
  }, undefined, false, undefined, this)
867
619
  ]
868
620
  }, undefined, true, undefined, this);
869
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
+ }
870
631
 
871
632
  // src/tui/screens/launch/index.tsx
872
- import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
633
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
873
634
  function Launch({ onSelect }) {
874
- const { exit } = useTui();
875
- const [mode, setMode] = useState4("browse");
876
- const [newName, setNewName] = useState4("");
877
- const [editValue, setEditValue] = useState4("");
878
- const [error, setError] = useState4(null);
879
- const { sessionRows, selected, cursor, setCursor, toggleArchived, refresh } = useLaunchSessions();
880
- const select = useCallback4((selection) => {
635
+ const { exit } = useTui2();
636
+ const select = useCallback2((selection) => {
881
637
  onSelect(selection);
882
638
  exit();
883
639
  }, [onSelect, exit]);
884
- const actions = useLaunchActions({
885
- selected,
886
- select,
887
- refresh,
888
- setMode,
889
- setCursor,
890
- setNewName,
891
- setEditValue,
892
- setError
893
- });
894
- const {
895
- browseBindings,
896
- inputBindings,
897
- deleteBindings,
898
- escPending,
899
- clearEscPending
900
- } = useLaunchHotkeys({
901
- mode,
902
- selected,
903
- sessionCount: sessionRows.length,
904
- quit: () => select({ type: "quit" }),
905
- setCursor,
906
- toggleArchived,
907
- setMode,
908
- ...actions
909
- });
910
- if (mode === "confirm-delete" && selected) {
911
- return /* @__PURE__ */ jsxDEV10(ConfirmDelete, {
912
- sessionName: `${selected.groupPath}/${selected.session.slug}`,
913
- deleteBindings
914
- }, undefined, false, undefined, this);
915
- }
916
- if (mode === "rename" && selected) {
917
- return /* @__PURE__ */ jsxDEV10(Rename, {
918
- editValue,
919
- setEditValue,
920
- setError,
921
- handleRenameSubmit: actions.handleRenameSubmit,
922
- inputBindings,
923
- error,
924
- escPending,
925
- clearEscPending,
926
- placeholder: selected.session.slug
927
- }, undefined, false, undefined, this);
928
- }
929
- if (mode === "move" && selected) {
930
- return /* @__PURE__ */ jsxDEV10(Move, {
931
- editValue,
932
- setEditValue,
933
- setError,
934
- handleMoveSubmit: actions.handleMoveSubmit,
935
- inputBindings,
936
- error,
937
- escPending,
938
- clearEscPending,
939
- placeholder: selected.groupPath
940
- }, undefined, false, undefined, this);
941
- }
942
- const hasSessions = sessionRows.length > 0;
943
- return /* @__PURE__ */ jsxDEV10(Box9, {
640
+ return /* @__PURE__ */ jsxDEV6(Box5, {
944
641
  flexDirection: "column",
945
642
  paddingY: 1,
946
643
  gap: 1,
947
644
  children: [
948
- /* @__PURE__ */ jsxDEV10(Box9, {
949
- "data-slot": "logo",
645
+ /* @__PURE__ */ jsxDEV6(Box5, {
950
646
  marginX: 1,
951
- children: /* @__PURE__ */ jsxDEV10(Logo, {}, undefined, false, undefined, this)
647
+ children: /* @__PURE__ */ jsxDEV6(Logo, {}, undefined, false, undefined, this)
952
648
  }, undefined, false, undefined, this),
953
- /* @__PURE__ */ jsxDEV10(Box9, {
954
- "data-slot": "app-menu",
649
+ /* @__PURE__ */ jsxDEV6(Box5, {
955
650
  flexDirection: "column",
956
651
  marginX: 2,
957
652
  gap: 1,
958
653
  children: [
959
- /* @__PURE__ */ jsxDEV10(AppDetails, {}, undefined, false, undefined, this),
960
- /* @__PURE__ */ jsxDEV10(Box9, {
961
- flexDirection: "column",
962
- paddingY: 1,
963
- children: [
964
- /* @__PURE__ */ jsxDEV10(Create, {
965
- isFocused: mode === "create",
966
- setError,
967
- handleCreateSubmit: actions.handleCreateSubmit,
968
- error
969
- }, undefined, false, undefined, this),
970
- hasSessions && sessionRows.map((row, i) => /* @__PURE__ */ jsxDEV10(SessionRow, {
971
- name: `${row.groupPath}/${row.session.slug}`,
972
- status: row.session.status,
973
- updatedAt: row.session.updatedAt,
974
- selected: i === cursor
975
- }, row.session.id, false, undefined, this))
976
- ]
977
- }, undefined, true, undefined, this),
978
- /* @__PURE__ */ jsxDEV10(Text10, {
979
- dim: true,
980
- children: formatBindings(browseBindings)
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" })
981
659
  }, undefined, false, undefined, this)
982
660
  ]
983
661
  }, undefined, true, undefined, this)
@@ -986,9 +664,50 @@ function Launch({ onSelect }) {
986
664
  }
987
665
 
988
666
  // src/tui/screens/Exit.tsx
989
- import { useState as useState5 } from "react";
990
- import { Box as Box10, Text as Text11, useInput as useInput2, useTui as useTui2 } from "@orchetron/storm";
991
- import { jsxDEV as jsxDEV11 } from "react/jsx-dev-runtime";
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
+ // src/lib/format.ts
677
+ var SECOND = 1000;
678
+ var MINUTE = 60 * SECOND;
679
+ var HOUR = 60 * MINUTE;
680
+ var DAY = 24 * HOUR;
681
+ function formatDuration(ms) {
682
+ if (ms < MINUTE)
683
+ return `${Math.round(ms / SECOND)}s`;
684
+ const days = Math.floor(ms / DAY);
685
+ const hours = Math.floor(ms % DAY / HOUR);
686
+ const minutes = Math.floor(ms % HOUR / MINUTE);
687
+ if (days > 0)
688
+ return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
689
+ if (hours > 0)
690
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
691
+ return `${minutes}m`;
692
+ }
693
+ function formatAgo(isoOrDate) {
694
+ const date = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
695
+ const ms = Date.now() - date.getTime();
696
+ if (ms < MINUTE)
697
+ return "just now";
698
+ if (ms < HOUR)
699
+ return `${Math.floor(ms / MINUTE)}m ago`;
700
+ if (ms < DAY)
701
+ return `${Math.floor(ms / HOUR)}h ago`;
702
+ if (ms < 2 * DAY)
703
+ return "yesterday";
704
+ if (ms < 7 * DAY)
705
+ return `${Math.floor(ms / DAY)}d ago`;
706
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
707
+ }
708
+
709
+ // src/tui/screens/Exit.tsx
710
+ import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
992
711
  var OPTIONS = [
993
712
  { action: "save", label: "Save", hint: "Keep session paused for later" },
994
713
  {
@@ -1008,11 +727,11 @@ var OPTIONS = [
1008
727
  }
1009
728
  ];
1010
729
  function Exit({ sessionId, onAction }) {
1011
- const { exit } = useTui2();
1012
- const [cursor, setCursor] = useState5(0);
730
+ const { exit } = useTui3();
731
+ const [cursor, setCursor] = useState3(0);
1013
732
  const session = getSession(sessionId);
1014
733
  const conversations2 = session ? getConversationsBySession(session.id) : [];
1015
- useInput2((e) => {
734
+ useInput3((e) => {
1016
735
  if (e.key === "c" && e.ctrl)
1017
736
  exit();
1018
737
  if (e.key === "up" || e.key === "k") {
@@ -1029,37 +748,37 @@ function Exit({ sessionId, onAction }) {
1029
748
  }
1030
749
  });
1031
750
  if (!session) {
1032
- return /* @__PURE__ */ jsxDEV11(Text11, {
751
+ return /* @__PURE__ */ jsxDEV7(Text6, {
1033
752
  color: "red",
1034
753
  children: "Session not found"
1035
754
  }, undefined, false, undefined, this);
1036
755
  }
1037
756
  const duration = session.endedAt && session.startedAt ? formatDuration(new Date(session.endedAt).getTime() - new Date(session.startedAt).getTime()) : null;
1038
- return /* @__PURE__ */ jsxDEV11(Box10, {
757
+ return /* @__PURE__ */ jsxDEV7(Box6, {
1039
758
  flexDirection: "column",
1040
759
  padding: 1,
1041
760
  gap: 1,
1042
761
  children: [
1043
- /* @__PURE__ */ jsxDEV11(Text11, {
762
+ /* @__PURE__ */ jsxDEV7(Text6, {
1044
763
  bold: true,
1045
764
  color: "#82AAFF",
1046
765
  children: "Session ended"
1047
766
  }, undefined, false, undefined, this),
1048
- /* @__PURE__ */ jsxDEV11(Box10, {
767
+ /* @__PURE__ */ jsxDEV7(Box6, {
1049
768
  flexDirection: "column",
1050
769
  children: [
1051
- /* @__PURE__ */ jsxDEV11(Box10, {
770
+ /* @__PURE__ */ jsxDEV7(Box6, {
1052
771
  flexDirection: "row",
1053
772
  gap: 1,
1054
773
  children: [
1055
- /* @__PURE__ */ jsxDEV11(StatusDot, {
774
+ /* @__PURE__ */ jsxDEV7(StatusDot, {
1056
775
  status: session.status
1057
776
  }, undefined, false, undefined, this),
1058
- /* @__PURE__ */ jsxDEV11(Text11, {
777
+ /* @__PURE__ */ jsxDEV7(Text6, {
1059
778
  bold: true,
1060
779
  children: session.name
1061
780
  }, undefined, false, undefined, this),
1062
- duration && /* @__PURE__ */ jsxDEV11(Text11, {
781
+ duration && /* @__PURE__ */ jsxDEV7(Text6, {
1063
782
  dim: true,
1064
783
  children: [
1065
784
  "(",
@@ -1069,7 +788,7 @@ function Exit({ sessionId, onAction }) {
1069
788
  }, undefined, true, undefined, this)
1070
789
  ]
1071
790
  }, undefined, true, undefined, this),
1072
- /* @__PURE__ */ jsxDEV11(Text11, {
791
+ /* @__PURE__ */ jsxDEV7(Text6, {
1073
792
  dim: true,
1074
793
  children: [
1075
794
  conversations2.length,
@@ -1079,28 +798,28 @@ function Exit({ sessionId, onAction }) {
1079
798
  }, undefined, true, undefined, this)
1080
799
  ]
1081
800
  }, undefined, true, undefined, this),
1082
- /* @__PURE__ */ jsxDEV11(Box10, {
801
+ /* @__PURE__ */ jsxDEV7(Box6, {
1083
802
  flexDirection: "column",
1084
- children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV11(Box10, {
803
+ children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV7(Box6, {
1085
804
  flexDirection: "row",
1086
805
  gap: 1,
1087
806
  children: [
1088
- /* @__PURE__ */ jsxDEV11(Text11, {
807
+ /* @__PURE__ */ jsxDEV7(Text6, {
1089
808
  children: i === cursor ? "\u276F" : " "
1090
809
  }, undefined, false, undefined, this),
1091
- /* @__PURE__ */ jsxDEV11(Text11, {
810
+ /* @__PURE__ */ jsxDEV7(Text6, {
1092
811
  bold: i === cursor,
1093
812
  children: opt.label
1094
813
  }, undefined, false, undefined, this),
1095
- /* @__PURE__ */ jsxDEV11(Text11, {
814
+ /* @__PURE__ */ jsxDEV7(Text6, {
1096
815
  dim: true,
1097
816
  children: opt.hint
1098
817
  }, undefined, false, undefined, this)
1099
818
  ]
1100
819
  }, opt.action, true, undefined, this))
1101
820
  }, undefined, false, undefined, this),
1102
- /* @__PURE__ */ jsxDEV11(Box10, {
1103
- children: /* @__PURE__ */ jsxDEV11(Text11, {
821
+ /* @__PURE__ */ jsxDEV7(Box6, {
822
+ children: /* @__PURE__ */ jsxDEV7(Text6, {
1104
823
  dim: true,
1105
824
  children: "\u2191\u2193 navigate \xB7 enter select \xB7 q save & quit"
1106
825
  }, undefined, false, undefined, this)
@@ -1110,12 +829,12 @@ function Exit({ sessionId, onAction }) {
1110
829
  }
1111
830
 
1112
831
  // src/tui/screens/Resume.tsx
1113
- import { useState as useState6 } from "react";
1114
- import { Box as Box11, Text as Text12, useInput as useInput3, useTui as useTui3 } from "@orchetron/storm";
1115
- import { jsxDEV as jsxDEV12 } from "react/jsx-dev-runtime";
832
+ 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";
1116
835
  function Resume({ sessionId, onSelect }) {
1117
- const { exit } = useTui3();
1118
- const [cursor, setCursor] = useState6(0);
836
+ const { exit } = useTui4();
837
+ const [cursor, setCursor] = useState4(0);
1119
838
  const session = getSession(sessionId);
1120
839
  const conversations2 = getConversationsBySession(sessionId);
1121
840
  const totalOptions = conversations2.length + 1;
@@ -1123,7 +842,7 @@ function Resume({ sessionId, onSelect }) {
1123
842
  onSelect(selection);
1124
843
  exit();
1125
844
  };
1126
- useInput3((e) => {
845
+ useInput4((e) => {
1127
846
  if (e.key === "c" && e.ctrl)
1128
847
  select({ type: "back" });
1129
848
  if (e.key === "up" || e.key === "k") {
@@ -1142,17 +861,17 @@ function Resume({ sessionId, onSelect }) {
1142
861
  }
1143
862
  });
1144
863
  if (!session) {
1145
- return /* @__PURE__ */ jsxDEV12(Text12, {
864
+ return /* @__PURE__ */ jsxDEV8(Text7, {
1146
865
  color: "red",
1147
866
  children: "Session not found"
1148
867
  }, undefined, false, undefined, this);
1149
868
  }
1150
- return /* @__PURE__ */ jsxDEV12(Box11, {
869
+ return /* @__PURE__ */ jsxDEV8(Box7, {
1151
870
  flexDirection: "column",
1152
871
  padding: 1,
1153
872
  gap: 1,
1154
873
  children: [
1155
- /* @__PURE__ */ jsxDEV12(Text12, {
874
+ /* @__PURE__ */ jsxDEV8(Text7, {
1156
875
  bold: true,
1157
876
  color: "#82AAFF",
1158
877
  children: [
@@ -1160,17 +879,17 @@ function Resume({ sessionId, onSelect }) {
1160
879
  session.name
1161
880
  ]
1162
881
  }, undefined, true, undefined, this),
1163
- /* @__PURE__ */ jsxDEV12(Box11, {
882
+ /* @__PURE__ */ jsxDEV8(Box7, {
1164
883
  flexDirection: "column",
1165
884
  children: [
1166
- /* @__PURE__ */ jsxDEV12(Box11, {
885
+ /* @__PURE__ */ jsxDEV8(Box7, {
1167
886
  flexDirection: "row",
1168
887
  gap: 1,
1169
888
  children: [
1170
- /* @__PURE__ */ jsxDEV12(Text12, {
889
+ /* @__PURE__ */ jsxDEV8(Text7, {
1171
890
  children: cursor === 0 ? "\u276F" : " "
1172
891
  }, undefined, false, undefined, this),
1173
- /* @__PURE__ */ jsxDEV12(Text12, {
892
+ /* @__PURE__ */ jsxDEV8(Text7, {
1174
893
  bold: cursor === 0,
1175
894
  color: "#34D399",
1176
895
  children: "+ New conversation"
@@ -1182,42 +901,42 @@ function Resume({ sessionId, onSelect }) {
1182
901
  const isSelected = cursor === idx;
1183
902
  const duration = conv.endedAt ? formatDuration(new Date(conv.endedAt).getTime() - new Date(conv.startedAt).getTime()) : "active";
1184
903
  const ago = formatAgo(conv.startedAt);
1185
- return /* @__PURE__ */ jsxDEV12(Box11, {
904
+ return /* @__PURE__ */ jsxDEV8(Box7, {
1186
905
  flexDirection: "row",
1187
906
  gap: 1,
1188
907
  children: [
1189
- /* @__PURE__ */ jsxDEV12(Text12, {
908
+ /* @__PURE__ */ jsxDEV8(Text7, {
1190
909
  children: isSelected ? "\u276F" : " "
1191
910
  }, undefined, false, undefined, this),
1192
- /* @__PURE__ */ jsxDEV12(Text12, {
911
+ /* @__PURE__ */ jsxDEV8(Text7, {
1193
912
  bold: isSelected,
1194
913
  dim: conv.discarded,
1195
914
  children: conv.id.slice(0, 8)
1196
915
  }, undefined, false, undefined, this),
1197
- /* @__PURE__ */ jsxDEV12(Text12, {
916
+ /* @__PURE__ */ jsxDEV8(Text7, {
1198
917
  dim: true,
1199
918
  children: ago
1200
919
  }, undefined, false, undefined, this),
1201
- /* @__PURE__ */ jsxDEV12(Text12, {
920
+ /* @__PURE__ */ jsxDEV8(Text7, {
1202
921
  dim: true,
1203
922
  children: "\xB7"
1204
923
  }, undefined, false, undefined, this),
1205
- /* @__PURE__ */ jsxDEV12(Text12, {
924
+ /* @__PURE__ */ jsxDEV8(Text7, {
1206
925
  dim: true,
1207
926
  children: [
1208
927
  conv.eventCount,
1209
928
  " events"
1210
929
  ]
1211
930
  }, undefined, true, undefined, this),
1212
- /* @__PURE__ */ jsxDEV12(Text12, {
931
+ /* @__PURE__ */ jsxDEV8(Text7, {
1213
932
  dim: true,
1214
933
  children: "\xB7"
1215
934
  }, undefined, false, undefined, this),
1216
- /* @__PURE__ */ jsxDEV12(Text12, {
935
+ /* @__PURE__ */ jsxDEV8(Text7, {
1217
936
  dim: true,
1218
937
  children: duration
1219
938
  }, undefined, false, undefined, this),
1220
- conv.discarded && /* @__PURE__ */ jsxDEV12(Text12, {
939
+ conv.discarded && /* @__PURE__ */ jsxDEV8(Text7, {
1221
940
  color: "red",
1222
941
  dim: true,
1223
942
  children: "(discarded)"
@@ -1227,8 +946,8 @@ function Resume({ sessionId, onSelect }) {
1227
946
  })
1228
947
  ]
1229
948
  }, undefined, true, undefined, this),
1230
- /* @__PURE__ */ jsxDEV12(Box11, {
1231
- children: /* @__PURE__ */ jsxDEV12(Text12, {
949
+ /* @__PURE__ */ jsxDEV8(Box7, {
950
+ children: /* @__PURE__ */ jsxDEV8(Text7, {
1232
951
  dim: true,
1233
952
  children: "\u2191\u2193 navigate \xB7 enter select \xB7 q back"
1234
953
  }, undefined, false, undefined, this)
@@ -1238,7 +957,7 @@ function Resume({ sessionId, onSelect }) {
1238
957
  }
1239
958
 
1240
959
  // src/tui/run-screen.tsx
1241
- import { jsxDEV as jsxDEV13 } from "react/jsx-dev-runtime";
960
+ import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
1242
961
  var [, , screen, outputPath, ...args] = process.argv;
1243
962
  if (!screen || !outputPath) {
1244
963
  console.error("Usage: run-screen <screen> <outputPath> [args...]");
@@ -1248,7 +967,7 @@ var result;
1248
967
  switch (screen) {
1249
968
  case "launch": {
1250
969
  let selection = { type: "quit" };
1251
- const app = render(/* @__PURE__ */ jsxDEV13(Launch, {
970
+ const app = render(/* @__PURE__ */ jsxDEV9(Launch, {
1252
971
  onSelect: (s) => {
1253
972
  selection = s;
1254
973
  }
@@ -1265,7 +984,7 @@ switch (screen) {
1265
984
  process.exit(1);
1266
985
  }
1267
986
  let action = "save";
1268
- const app = render(/* @__PURE__ */ jsxDEV13(Exit, {
987
+ const app = render(/* @__PURE__ */ jsxDEV9(Exit, {
1269
988
  sessionId,
1270
989
  onAction: (a) => {
1271
990
  action = a;
@@ -1283,7 +1002,7 @@ switch (screen) {
1283
1002
  process.exit(1);
1284
1003
  }
1285
1004
  let selection = { type: "back" };
1286
- const app = render(/* @__PURE__ */ jsxDEV13(Resume, {
1005
+ const app = render(/* @__PURE__ */ jsxDEV9(Resume, {
1287
1006
  sessionId,
1288
1007
  onSelect: (s) => {
1289
1008
  selection = s;