bertrand 0.14.0 → 0.15.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.
package/dist/bertrand.js CHANGED
@@ -1151,8 +1151,54 @@ var init_engagement_stats = __esm(() => {
1151
1151
  init_events();
1152
1152
  });
1153
1153
 
1154
+ // src/lib/session-archive.ts
1155
+ function archiveSession(id) {
1156
+ const session = getSession(id);
1157
+ if (!session)
1158
+ return { ok: false, reason: "not-found" };
1159
+ if (ACTIVE_STATUSES.includes(session.status)) {
1160
+ return { ok: false, reason: "active" };
1161
+ }
1162
+ if (session.status === "archived") {
1163
+ return { ok: false, reason: "already-archived" };
1164
+ }
1165
+ const updated = updateSessionStatus(id, "archived");
1166
+ return { ok: true, session: updated };
1167
+ }
1168
+ function unarchiveSession(id) {
1169
+ const session = getSession(id);
1170
+ if (!session)
1171
+ return { ok: false, reason: "not-found" };
1172
+ if (session.status !== "archived") {
1173
+ return { ok: false, reason: "not-archived" };
1174
+ }
1175
+ const updated = updateSessionStatus(id, "paused");
1176
+ return { ok: true, session: updated };
1177
+ }
1178
+ function archiveAllPaused() {
1179
+ const rows = getAllSessions({ excludeArchived: true });
1180
+ const paused = rows.filter((r) => r.session.status === "paused");
1181
+ const archived = [];
1182
+ for (const row of paused) {
1183
+ const updated = updateSessionStatus(row.session.id, "archived");
1184
+ archived.push({ session: updated, groupPath: row.groupPath });
1185
+ }
1186
+ return { archived };
1187
+ }
1188
+ var ACTIVE_STATUSES;
1189
+ var init_session_archive = __esm(() => {
1190
+ init_sessions();
1191
+ ACTIVE_STATUSES = ["active", "waiting"];
1192
+ });
1193
+
1154
1194
  // src/server/index.ts
1155
1195
  import { execFile } from "child_process";
1196
+ function archiveResponse(result) {
1197
+ if (result.ok)
1198
+ return Response.json(result.session);
1199
+ const meta = ARCHIVE_ERROR[result.reason] ?? { status: 400, message: "Operation failed" };
1200
+ return Response.json({ error: meta.message, reason: result.reason }, { status: meta.status });
1201
+ }
1156
1202
  async function handleOpen(req) {
1157
1203
  let body;
1158
1204
  try {
@@ -1210,6 +1256,20 @@ function startServer(port = PORT) {
1210
1256
  return r;
1211
1257
  });
1212
1258
  }
1259
+ if (req.method === "POST") {
1260
+ const archiveMatch = /^\/api\/sessions\/([^/]+)\/archive$/.exec(url.pathname);
1261
+ if (archiveMatch) {
1262
+ const response2 = archiveResponse(archiveSession(archiveMatch[1]));
1263
+ response2.headers.set("Access-Control-Allow-Origin", "*");
1264
+ return response2;
1265
+ }
1266
+ const unarchiveMatch = /^\/api\/sessions\/([^/]+)\/unarchive$/.exec(url.pathname);
1267
+ if (unarchiveMatch) {
1268
+ const response2 = archiveResponse(unarchiveSession(unarchiveMatch[1]));
1269
+ response2.headers.set("Access-Control-Allow-Origin", "*");
1270
+ return response2;
1271
+ }
1272
+ }
1213
1273
  const response = match(url.pathname, url);
1214
1274
  response.headers.set("Access-Control-Allow-Origin", "*");
1215
1275
  return response;
@@ -1218,13 +1278,14 @@ function startServer(port = PORT) {
1218
1278
  console.log(`bertrand API server listening on http://localhost:${server.port}`);
1219
1279
  return server;
1220
1280
  }
1221
- var PORT, routes;
1281
+ var PORT, routes, ARCHIVE_ERROR;
1222
1282
  var init_server = __esm(() => {
1223
1283
  init_sessions();
1224
1284
  init_events();
1225
1285
  init_stats();
1226
1286
  init_timing();
1227
1287
  init_engagement_stats();
1288
+ init_session_archive();
1228
1289
  PORT = Number(process.env.BERTRAND_PORT ?? 5200);
1229
1290
  routes = [
1230
1291
  [/^\/api\/sessions$/, (_params, url) => {
@@ -1275,6 +1336,12 @@ var init_server = __esm(() => {
1275
1336
  return getLatestRecaps();
1276
1337
  }]
1277
1338
  ];
1339
+ ARCHIVE_ERROR = {
1340
+ "not-found": { status: 404, message: "Session not found" },
1341
+ active: { status: 409, message: "Cannot archive an active session" },
1342
+ "already-archived": { status: 409, message: "Session is already archived" },
1343
+ "not-archived": { status: 409, message: "Session is not archived" }
1344
+ };
1278
1345
  });
1279
1346
 
1280
1347
  // src/cli/commands/serve.ts
@@ -1361,16 +1428,10 @@ var init_labels = __esm(() => {
1361
1428
  // src/contract/template.md
1362
1429
  var template_default = `You are running inside bertrand, session: {sessionName}. Follow these rules strictly:
1363
1430
 
1364
- At session start, run: ToolSearch with query "select:AskUserQuestion" to load the tool.
1365
-
1366
- After every response, you MUST call AskUserQuestion. This is a continuous loop \u2014 every turn ends with AskUserQuestion. Always include a "Done for now" option so the user can exit the loop when ready. The description for "Done for now" must be a 1-2 sentence summary of what was accomplished so far. Describe outcomes (what was built, fixed, decided), not process.
1431
+ After every response, you MUST call AskUserQuestion. This is a continuous loop \u2014 every turn ends with AskUserQuestion. Always include an option labeled exactly \`Done for now\` (this exact wording is required \u2014 the session-exit hook greps for it). The description for "Done for now" must be a 1-2 sentence summary of what was accomplished so far. Describe outcomes (what was built, fixed, decided), not process.
1367
1432
 
1368
1433
  If the user's most recent answer to AskUserQuestion was "Done for now" (or contains it), this turn is the FINAL turn. Respond briefly to acknowledge and do NOT call AskUserQuestion again \u2014 the loop is over.
1369
1434
 
1370
- Every option must be a concrete, actionable next step. No filler like "Have questions?" or "Want to learn more?" \u2014 if clarification is needed, phrase it as a specific action: "Discuss tradeoffs of X vs Y".
1371
-
1372
- Every AskUserQuestion call MUST use multiSelect: true. No exceptions. Single-select fires on Enter with no confirmation, which causes accidental selections when a block gains focus. multiSelect requires explicit confirmation before submitting.
1373
-
1374
1435
  Before each AskUserQuestion call, emit a \`<recap>...</recap>\` block in your text output. Use markdown \u2014 a short bullet list is usually the most scannable shape; a single short paragraph is fine when the turn was one cohesive thing. Keep it concise. The recap covers what happened since the previous AskUserQuestion (or session start) \u2014 what you found, decided, or did. Write the gist for someone reading the session timeline, not a process log. The dashboard renders these between AskUserQuestion events; do not use this tag for any other purpose.
1375
1436
  `;
1376
1437
  var init_template = () => {};
@@ -1774,7 +1835,7 @@ async function runSessionLoop(sessionId) {
1774
1835
  case "save":
1775
1836
  break;
1776
1837
  case "archive":
1777
- updateSessionStatus(sessionId, "archived");
1838
+ archiveSession(sessionId);
1778
1839
  break;
1779
1840
  case "discard":
1780
1841
  deleteSession(sessionId);
@@ -1799,11 +1860,6 @@ async function startTui() {
1799
1860
  await runSessionLoop(sessionId);
1800
1861
  break;
1801
1862
  }
1802
- case "resume": {
1803
- const sessionId = await resume(selection);
1804
- await runSessionLoop(sessionId);
1805
- break;
1806
- }
1807
1863
  case "pick": {
1808
1864
  const conversationId = await resolveConversationForResume(selection.sessionId);
1809
1865
  if (!conversationId)
@@ -1821,6 +1877,7 @@ var SCREEN_ENTRY;
1821
1877
  var init_app = __esm(() => {
1822
1878
  init_sessions();
1823
1879
  init_conversations();
1880
+ init_session_archive();
1824
1881
  init_session();
1825
1882
  SCREEN_ENTRY = (() => {
1826
1883
  const built = join3(import.meta.dir, "run-screen.js");
@@ -1928,11 +1985,21 @@ var init_migrate = __esm(() => {
1928
1985
  function waitingScript(bin) {
1929
1986
  const BIN = bin;
1930
1987
  return `#!/usr/bin/env bash
1931
- # Hook: PreToolUse AskUserQuestion \u2192 mark session as waiting
1988
+ # Hook: PreToolUse AskUserQuestion \u2192 enforce multiSelect, mark session as waiting
1932
1989
  sid="\${BERTRAND_SESSION:-}"
1933
1990
  [ -z "$sid" ] && exit 0
1934
1991
 
1935
1992
  input="$(cat)"
1993
+
1994
+ # Block AUQ calls that omit multiSelect:true on any question. multiSelect is a
1995
+ # UX-safety mechanism in bertrand (prevents submit-on-focus), not a cardinality
1996
+ # signal. Enforce mechanically so the rule sticks in subagent / job contexts
1997
+ # where the system-prompt contract never reaches the agent.
1998
+ if printf '%s' "$input" | jq -e '.tool_input.questions[]? | select(.multiSelect != true)' > /dev/null 2>&1; then
1999
+ printf 'All AskUserQuestion questions must set multiSelect:true. This is a UX-safety mechanism in bertrand (prevents submit-on-focus when the question block gains focus), not a cardinality signal. Retry with multiSelect:true on every question.\\n' >&2
2000
+ exit 2
2001
+ fi
2002
+
1936
2003
  cid="\${BERTRAND_CLAUDE_ID:-}"
1937
2004
 
1938
2005
  # Extract question \u2014 grep for simple field extraction (~1ms vs jq ~15ms)
@@ -2973,7 +3040,7 @@ function dur(seconds) {
2973
3040
  function renderGlobal(metrics, isJson) {
2974
3041
  const totals = metrics.reduce((acc, m) => ({
2975
3042
  sessions: acc.sessions + 1,
2976
- active: acc.active + (ACTIVE_STATUSES.includes(m.status) ? 1 : 0),
3043
+ active: acc.active + (ACTIVE_STATUSES2.includes(m.status) ? 1 : 0),
2977
3044
  durationS: acc.durationS + m.durationS,
2978
3045
  claudeWorkS: acc.claudeWorkS + m.claudeWorkS,
2979
3046
  userWaitS: acc.userWaitS + m.userWaitS,
@@ -3046,7 +3113,7 @@ function renderSession(m, isJson) {
3046
3113
  console.log(` Interactions: ${m.interactionCount}`);
3047
3114
  console.log(` PRs: ${m.prCount}`);
3048
3115
  }
3049
- var ACTIVE_STATUSES;
3116
+ var ACTIVE_STATUSES2;
3050
3117
  var init_stats2 = __esm(() => {
3051
3118
  init_router();
3052
3119
  init_sessions();
@@ -3055,7 +3122,7 @@ var init_stats2 = __esm(() => {
3055
3122
  init_events();
3056
3123
  init_timing();
3057
3124
  init_format();
3058
- ACTIVE_STATUSES = ["active", "waiting"];
3125
+ ACTIVE_STATUSES2 = ["active", "waiting"];
3059
3126
  register("stats", async (args) => {
3060
3127
  const isJson = args.includes("--json");
3061
3128
  const filteredArgs = args.filter((a) => !a.startsWith("--"));
@@ -3128,32 +3195,27 @@ function resolveSession(name) {
3128
3195
  }
3129
3196
  return { session, groupPath };
3130
3197
  }
3131
- var ACTIVE_STATUSES2;
3132
3198
  var init_archive = __esm(() => {
3133
3199
  init_router();
3134
3200
  init_sessions();
3135
3201
  init_groups();
3136
- ACTIVE_STATUSES2 = ["active", "waiting"];
3202
+ init_session_archive();
3137
3203
  register("archive", async (args) => {
3138
3204
  const isUndo = args.includes("--undo");
3139
3205
  const isAllPaused = args.includes("--all-paused");
3140
3206
  const filteredArgs = args.filter((a) => !a.startsWith("--"));
3141
3207
  const sessionName = filteredArgs[0];
3142
3208
  if (isAllPaused) {
3143
- const rows = getAllSessions({ excludeArchived: true });
3144
- const paused = rows.filter((r) => r.session.status === "paused");
3145
- if (paused.length === 0) {
3209
+ const { archived } = archiveAllPaused();
3210
+ if (archived.length === 0) {
3146
3211
  console.log("No paused sessions to archive.");
3147
3212
  return;
3148
3213
  }
3149
- let archived = 0;
3150
- for (const row of paused) {
3151
- updateSessionStatus(row.session.id, "archived");
3152
- console.log(` archived ${row.groupPath}/${row.session.slug}`);
3153
- archived++;
3214
+ for (const { session: session2, groupPath: groupPath2 } of archived) {
3215
+ console.log(` archived ${groupPath2}/${session2.slug}`);
3154
3216
  }
3155
3217
  console.log(`
3156
- Archived ${archived} session${archived === 1 ? "" : "s"}.`);
3218
+ Archived ${archived.length} session${archived.length === 1 ? "" : "s"}.`);
3157
3219
  return;
3158
3220
  }
3159
3221
  if (!sessionName) {
@@ -3165,23 +3227,33 @@ Archived ${archived} session${archived === 1 ? "" : "s"}.`);
3165
3227
  const { session, groupPath } = resolveSession(sessionName);
3166
3228
  const fullName = `${groupPath}/${session.slug}`;
3167
3229
  if (isUndo) {
3168
- if (session.status !== "archived") {
3169
- console.error(`${fullName} is not archived (status: ${session.status})`);
3230
+ const result2 = unarchiveSession(session.id);
3231
+ if (!result2.ok) {
3232
+ if (result2.reason === "not-archived") {
3233
+ console.error(`${fullName} is not archived (status: ${session.status})`);
3234
+ } else {
3235
+ console.error(`Session not found: ${fullName}`);
3236
+ }
3170
3237
  process.exit(1);
3171
3238
  }
3172
- updateSessionStatus(session.id, "paused");
3173
3239
  console.log(`Unarchived ${fullName}`);
3174
3240
  return;
3175
3241
  }
3176
- if (ACTIVE_STATUSES2.includes(session.status)) {
3177
- console.error(`Cannot archive active session ${fullName} (status: ${session.status})`);
3178
- process.exit(1);
3179
- }
3180
- if (session.status === "archived") {
3181
- console.error(`${fullName} is already archived`);
3242
+ const result = archiveSession(session.id);
3243
+ if (!result.ok) {
3244
+ switch (result.reason) {
3245
+ case "active":
3246
+ console.error(`Cannot archive active session ${fullName} (status: ${session.status})`);
3247
+ break;
3248
+ case "already-archived":
3249
+ console.error(`${fullName} is already archived`);
3250
+ break;
3251
+ case "not-found":
3252
+ console.error(`Session not found: ${fullName}`);
3253
+ break;
3254
+ }
3182
3255
  process.exit(1);
3183
3256
  }
3184
- updateSessionStatus(session.id, "archived");
3185
3257
  console.log(`Archived ${fullName}`);
3186
3258
  });
3187
3259
  });
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertrand",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },