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 +111 -39
- package/dist/run-screen.js +357 -285
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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 + (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3144
|
-
|
|
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
|
-
|
|
3150
|
-
|
|
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
|
-
|
|
3169
|
-
|
|
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
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
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
|
});
|
package/dist/run-screen.js
CHANGED
|
@@ -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 {
|
|
23
|
-
import { Box as
|
|
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 =
|
|
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 (
|
|
98
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
children:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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/
|
|
291
|
-
import {
|
|
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(
|
|
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,
|
|
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 {
|
|
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 } =
|
|
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
|
-
|
|
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__ */
|
|
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__ */
|
|
829
|
+
return /* @__PURE__ */ jsxDEV6(Box5, {
|
|
758
830
|
flexDirection: "column",
|
|
759
831
|
padding: 1,
|
|
760
832
|
gap: 1,
|
|
761
833
|
children: [
|
|
762
|
-
/* @__PURE__ */
|
|
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__ */
|
|
839
|
+
/* @__PURE__ */ jsxDEV6(Box5, {
|
|
768
840
|
flexDirection: "column",
|
|
769
841
|
children: [
|
|
770
|
-
/* @__PURE__ */
|
|
842
|
+
/* @__PURE__ */ jsxDEV6(Box5, {
|
|
771
843
|
flexDirection: "row",
|
|
772
844
|
gap: 1,
|
|
773
845
|
children: [
|
|
774
|
-
/* @__PURE__ */
|
|
846
|
+
/* @__PURE__ */ jsxDEV6(StatusDot, {
|
|
775
847
|
status: session.status
|
|
776
848
|
}, undefined, false, undefined, this),
|
|
777
|
-
/* @__PURE__ */
|
|
849
|
+
/* @__PURE__ */ jsxDEV6(Text6, {
|
|
778
850
|
bold: true,
|
|
779
851
|
children: session.name
|
|
780
852
|
}, undefined, false, undefined, this),
|
|
781
|
-
duration && /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
873
|
+
/* @__PURE__ */ jsxDEV6(Box5, {
|
|
802
874
|
flexDirection: "column",
|
|
803
|
-
children: OPTIONS.map((opt, i) => /* @__PURE__ */
|
|
875
|
+
children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV6(Box5, {
|
|
804
876
|
flexDirection: "row",
|
|
805
877
|
gap: 1,
|
|
806
878
|
children: [
|
|
807
|
-
/* @__PURE__ */
|
|
879
|
+
/* @__PURE__ */ jsxDEV6(Text6, {
|
|
808
880
|
children: i === cursor ? "\u276F" : " "
|
|
809
881
|
}, undefined, false, undefined, this),
|
|
810
|
-
/* @__PURE__ */
|
|
882
|
+
/* @__PURE__ */ jsxDEV6(Text6, {
|
|
811
883
|
bold: i === cursor,
|
|
812
884
|
children: opt.label
|
|
813
885
|
}, undefined, false, undefined, this),
|
|
814
|
-
/* @__PURE__ */
|
|
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__ */
|
|
822
|
-
children: /* @__PURE__ */
|
|
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
|
|
834
|
-
import { jsxDEV as
|
|
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 } =
|
|
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
|
-
|
|
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__ */
|
|
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__ */
|
|
941
|
+
return /* @__PURE__ */ jsxDEV7(Box6, {
|
|
870
942
|
flexDirection: "column",
|
|
871
943
|
padding: 1,
|
|
872
944
|
gap: 1,
|
|
873
945
|
children: [
|
|
874
|
-
/* @__PURE__ */
|
|
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__ */
|
|
954
|
+
/* @__PURE__ */ jsxDEV7(Box6, {
|
|
883
955
|
flexDirection: "column",
|
|
884
956
|
children: [
|
|
885
|
-
/* @__PURE__ */
|
|
957
|
+
/* @__PURE__ */ jsxDEV7(Box6, {
|
|
886
958
|
flexDirection: "row",
|
|
887
959
|
gap: 1,
|
|
888
960
|
children: [
|
|
889
|
-
/* @__PURE__ */
|
|
961
|
+
/* @__PURE__ */ jsxDEV7(Text7, {
|
|
890
962
|
children: cursor === 0 ? "\u276F" : " "
|
|
891
963
|
}, undefined, false, undefined, this),
|
|
892
|
-
/* @__PURE__ */
|
|
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__ */
|
|
976
|
+
return /* @__PURE__ */ jsxDEV7(Box6, {
|
|
905
977
|
flexDirection: "row",
|
|
906
978
|
gap: 1,
|
|
907
979
|
children: [
|
|
908
|
-
/* @__PURE__ */
|
|
980
|
+
/* @__PURE__ */ jsxDEV7(Text7, {
|
|
909
981
|
children: isSelected ? "\u276F" : " "
|
|
910
982
|
}, undefined, false, undefined, this),
|
|
911
|
-
/* @__PURE__ */
|
|
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__ */
|
|
988
|
+
/* @__PURE__ */ jsxDEV7(Text7, {
|
|
917
989
|
dim: true,
|
|
918
990
|
children: ago
|
|
919
991
|
}, undefined, false, undefined, this),
|
|
920
|
-
/* @__PURE__ */
|
|
992
|
+
/* @__PURE__ */ jsxDEV7(Text7, {
|
|
921
993
|
dim: true,
|
|
922
994
|
children: "\xB7"
|
|
923
995
|
}, undefined, false, undefined, this),
|
|
924
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1003
|
+
/* @__PURE__ */ jsxDEV7(Text7, {
|
|
932
1004
|
dim: true,
|
|
933
1005
|
children: "\xB7"
|
|
934
1006
|
}, undefined, false, undefined, this),
|
|
935
|
-
/* @__PURE__ */
|
|
1007
|
+
/* @__PURE__ */ jsxDEV7(Text7, {
|
|
936
1008
|
dim: true,
|
|
937
1009
|
children: duration
|
|
938
1010
|
}, undefined, false, undefined, this),
|
|
939
|
-
conv.discarded && /* @__PURE__ */
|
|
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__ */
|
|
950
|
-
children: /* @__PURE__ */
|
|
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
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
1077
|
+
const app = render(/* @__PURE__ */ jsxDEV8(Resume, {
|
|
1006
1078
|
sessionId,
|
|
1007
1079
|
onSelect: (s) => {
|
|
1008
1080
|
selection = s;
|