bertrand 0.20.1 → 0.22.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.
@@ -29,6 +29,13 @@
29
29
  "when": 1781474706585,
30
30
  "tag": "0003_categories_rename",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "6",
36
+ "when": 1781546596070,
37
+ "tag": "0004_dapper_apocalypse",
38
+ "breakpoints": true
32
39
  }
33
40
  ]
34
41
  }
@@ -15,7 +15,7 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // src/tui/run-screen.tsx
18
- import { writeFileSync as writeFileSync2 } from "fs";
18
+ import { writeFileSync as writeFileSync2, appendFileSync } from "fs";
19
19
  import { render } from "@orchetron/storm";
20
20
 
21
21
  // src/tui/screens/launch/index.tsx
@@ -590,6 +590,7 @@ var paths = {
590
590
  root: join(homedir(), BERTRAND_DIR),
591
591
  hooks: join(homedir(), BERTRAND_DIR, "hooks"),
592
592
  sessions: join(homedir(), BERTRAND_DIR, "sessions"),
593
+ runtime: join(homedir(), BERTRAND_DIR, "run"),
593
594
  db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
594
595
  syncEnv: join(homedir(), BERTRAND_DIR, "sync.env")
595
596
  };
@@ -759,6 +760,7 @@ var sessions = sqliteTable("sessions", {
759
760
  enum: ["active", "waiting", "paused", "archived"]
760
761
  }).notNull().default("paused"),
761
762
  summary: text("summary"),
763
+ rating: integer("rating"),
762
764
  pid: integer("pid"),
763
765
  startedAt: text("started_at").notNull().default(sql`(datetime('now'))`),
764
766
  endedAt: text("ended_at"),
@@ -898,6 +900,9 @@ function getAllSessions(opts) {
898
900
  function updateSessionStatus(id, status) {
899
901
  return getDb().update(sessions).set({ status, updatedAt: sql2`(datetime('now'))` }).where(eq(sessions.id, id)).returning().get();
900
902
  }
903
+ function setSessionRating(id, rating) {
904
+ return getDb().update(sessions).set({ rating, updatedAt: sql2`(datetime('now'))` }).where(eq(sessions.id, id)).returning().get();
905
+ }
901
906
 
902
907
  // src/lib/session-archive.ts
903
908
  var ACTIVE_STATUSES = ["active", "waiting"];
@@ -959,6 +964,7 @@ function formatAgo(isoOrDate) {
959
964
  }
960
965
 
961
966
  // src/lib/parse-session-name.ts
967
+ var SEGMENT_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
962
968
  function parseSessionName(input) {
963
969
  const trimmed = input.trim().replace(/^\/+|\/+$/g, "");
964
970
  if (!trimmed) {
@@ -969,12 +975,12 @@ function parseSessionName(input) {
969
975
  throw new Error(`Session name must include at least one category: "category/session" (got "${trimmed}")`);
970
976
  }
971
977
  for (const segment of segments) {
972
- if (!/^[a-z0-9][a-z0-9._-]*$/i.test(segment)) {
978
+ if (!SEGMENT_PATTERN.test(segment)) {
973
979
  throw new Error(`Invalid segment "${segment}": must start with alphanumeric and contain only letters, digits, dots, underscores, or dashes`);
974
980
  }
975
981
  }
976
- const slug = segments[segments.length - 1];
977
- const categoryPath = segments.slice(0, -1).join("/");
982
+ const categoryPath = segments[0];
983
+ const slug = segments.slice(1).join("/");
978
984
  return { categoryPath, slug };
979
985
  }
980
986
 
@@ -1324,7 +1330,7 @@ var OPTIONS = [
1324
1330
  {
1325
1331
  action: "resume",
1326
1332
  label: "Resume",
1327
- hint: "Start a new conversation in this session"
1333
+ hint: "Continue a conversation or start a new one"
1328
1334
  }
1329
1335
  ];
1330
1336
  function Exit({ sessionId, onAction }) {
@@ -1332,10 +1338,19 @@ function Exit({ sessionId, onAction }) {
1332
1338
  const [cursor, setCursor] = useState4(0);
1333
1339
  const session = getSession(sessionId);
1334
1340
  const conversations2 = session ? getConversationsBySession(session.id) : [];
1341
+ const [rating, setRating] = useState4(session?.rating ?? null);
1342
+ const persistRating = (next) => {
1343
+ setRating(next);
1344
+ setSessionRating(sessionId, next);
1345
+ };
1335
1346
  useInput3((e) => {
1336
1347
  if (e.key === "c" && e.ctrl)
1337
1348
  exit();
1338
- if (e.key === "up" || e.key === "k") {
1349
+ if (e.key >= "1" && e.key <= "5") {
1350
+ persistRating(Number(e.key));
1351
+ } else if (e.key === "0" || e.key === "backspace") {
1352
+ persistRating(null);
1353
+ } else if (e.key === "up" || e.key === "k") {
1339
1354
  setCursor((c) => Math.max(0, c - 1));
1340
1355
  } else if (e.key === "down" || e.key === "j") {
1341
1356
  setCursor((c) => Math.min(OPTIONS.length - 1, c + 1));
@@ -1399,6 +1414,29 @@ function Exit({ sessionId, onAction }) {
1399
1414
  }, undefined, true, undefined, this)
1400
1415
  ]
1401
1416
  }, undefined, true, undefined, this),
1417
+ /* @__PURE__ */ jsxDEV7(Box6, {
1418
+ flexDirection: "column",
1419
+ children: [
1420
+ /* @__PURE__ */ jsxDEV7(Text7, {
1421
+ dim: true,
1422
+ children: "How effective was this session?"
1423
+ }, undefined, false, undefined, this),
1424
+ /* @__PURE__ */ jsxDEV7(Box6, {
1425
+ flexDirection: "row",
1426
+ gap: 1,
1427
+ children: [
1428
+ /* @__PURE__ */ jsxDEV7(Text7, {
1429
+ color: "#FFD580",
1430
+ children: [1, 2, 3, 4, 5].map((n) => rating !== null && n <= rating ? "\u2605" : "\u2606").join(" ")
1431
+ }, undefined, false, undefined, this),
1432
+ /* @__PURE__ */ jsxDEV7(Text7, {
1433
+ dim: true,
1434
+ children: rating !== null ? `${rating} star${rating === 1 ? "" : "s"}` : "unrated"
1435
+ }, undefined, false, undefined, this)
1436
+ ]
1437
+ }, undefined, true, undefined, this)
1438
+ ]
1439
+ }, undefined, true, undefined, this),
1402
1440
  /* @__PURE__ */ jsxDEV7(Box6, {
1403
1441
  flexDirection: "column",
1404
1442
  children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsxDEV7(Box6, {
@@ -1422,7 +1460,7 @@ function Exit({ sessionId, onAction }) {
1422
1460
  /* @__PURE__ */ jsxDEV7(Box6, {
1423
1461
  children: /* @__PURE__ */ jsxDEV7(Text7, {
1424
1462
  dim: true,
1425
- children: "\u2191\u2193 navigate \xB7 enter select \xB7 q save & quit"
1463
+ children: "1-5 rate \xB7 0/backspace clear \xB7 \u2191\u2193 navigate \xB7 enter select \xB7 q save & quit"
1426
1464
  }, undefined, false, undefined, this)
1427
1465
  }, undefined, false, undefined, this)
1428
1466
  ]
@@ -1730,71 +1768,146 @@ if (!screen || !outputPath) {
1730
1768
  console.error("Usage: run-screen <screen> <outputPath> [args...]");
1731
1769
  process.exit(1);
1732
1770
  }
1771
+ var RESULT_PATH = outputPath;
1772
+ var DEBUG_PATH = process.env.BERTRAND_DEBUG_TUI || null;
1773
+ var START_HR = process.hrtime.bigint();
1774
+ function phase(name, detail) {
1775
+ if (!DEBUG_PATH)
1776
+ return;
1777
+ const elapsedMs = Number(process.hrtime.bigint() - START_HR) / 1e6;
1778
+ const line = `${elapsedMs.toFixed(2).padStart(8)}ms ${name}${detail ? " " + detail : ""}
1779
+ `;
1780
+ try {
1781
+ appendFileSync(DEBUG_PATH, line);
1782
+ } catch {}
1783
+ }
1784
+ phase("spawn", `screen=${screen} pid=${process.pid} isTTY=${process.stdout.isTTY ?? "?"} term=${process.env.TERM_PROGRAM || process.env.TERM || "?"}`);
1733
1785
  var result;
1734
- switch (screen) {
1735
- case "launch": {
1736
- let selection = { type: "quit" };
1737
- const app = render(/* @__PURE__ */ jsxDEV10(Launch, {
1738
- onSelect: (s) => {
1739
- selection = s;
1740
- }
1741
- }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true });
1742
- await app.waitUntilExit();
1743
- app.unmount();
1744
- result = selection;
1745
- break;
1746
- }
1747
- case "project-picker": {
1748
- let selection = { type: "quit" };
1749
- const app = render(/* @__PURE__ */ jsxDEV10(ProjectPicker, {
1750
- onSelect: (s) => {
1751
- selection = s;
1752
- }
1753
- }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true });
1754
- await app.waitUntilExit();
1755
- app.unmount();
1756
- result = selection;
1757
- break;
1758
- }
1759
- case "exit": {
1760
- const sessionId = args[0];
1761
- if (!sessionId) {
1762
- console.error("exit requires sessionId");
1763
- process.exit(1);
1786
+ var resultWritten = false;
1787
+ function writeResult() {
1788
+ if (resultWritten)
1789
+ return;
1790
+ resultWritten = true;
1791
+ try {
1792
+ writeFileSync2(RESULT_PATH, JSON.stringify(result));
1793
+ } catch {}
1794
+ }
1795
+ process.on("SIGINT", writeResult);
1796
+ process.on("SIGTERM", writeResult);
1797
+ process.on("SIGHUP", writeResult);
1798
+ process.on("uncaughtException", writeResult);
1799
+ process.on("unhandledRejection", writeResult);
1800
+ var renderCount = 0;
1801
+ var onRender = (m) => {
1802
+ renderCount++;
1803
+ phase(`paint #${renderCount}`, `cells=${m.cellsChanged} took=${m.renderTime.toFixed(2)}ms`);
1804
+ };
1805
+ try {
1806
+ switch (screen) {
1807
+ case "launch": {
1808
+ let selection = { type: "quit" };
1809
+ result = selection;
1810
+ phase("render() pre");
1811
+ const app = render(/* @__PURE__ */ jsxDEV10(Launch, {
1812
+ onSelect: (s) => {
1813
+ selection = s;
1814
+ result = selection;
1815
+ }
1816
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1817
+ phase("render() post");
1818
+ app.screen.invalidate();
1819
+ app.requestRepaint();
1820
+ phase("post-mount invalidate+requestRepaint");
1821
+ await app.waitUntilExit();
1822
+ phase("waitUntilExit returned");
1823
+ app.unmount();
1824
+ phase("unmount done");
1825
+ result = selection;
1826
+ break;
1764
1827
  }
1765
- let action = "save";
1766
- const app = render(/* @__PURE__ */ jsxDEV10(Exit, {
1767
- sessionId,
1768
- onAction: (a) => {
1769
- action = a;
1828
+ case "project-picker": {
1829
+ let selection = { type: "quit" };
1830
+ result = selection;
1831
+ phase("render() pre");
1832
+ const app = render(/* @__PURE__ */ jsxDEV10(ProjectPicker, {
1833
+ onSelect: (s) => {
1834
+ selection = s;
1835
+ result = selection;
1836
+ }
1837
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1838
+ phase("render() post");
1839
+ app.screen.invalidate();
1840
+ app.requestRepaint();
1841
+ phase("post-mount invalidate+requestRepaint");
1842
+ await app.waitUntilExit();
1843
+ phase("waitUntilExit returned");
1844
+ app.unmount();
1845
+ phase("unmount done");
1846
+ result = selection;
1847
+ break;
1848
+ }
1849
+ case "exit": {
1850
+ const sessionId = args[0];
1851
+ if (!sessionId) {
1852
+ console.error("exit requires sessionId");
1853
+ process.exit(1);
1770
1854
  }
1771
- }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true });
1772
- await app.waitUntilExit();
1773
- app.unmount();
1774
- result = action;
1775
- break;
1776
- }
1777
- case "resume": {
1778
- const sessionId = args[0];
1779
- if (!sessionId) {
1780
- console.error("resume requires sessionId");
1781
- process.exit(1);
1855
+ let action = "save";
1856
+ result = action;
1857
+ phase("render() pre");
1858
+ const app = render(/* @__PURE__ */ jsxDEV10(Exit, {
1859
+ sessionId,
1860
+ onAction: (a) => {
1861
+ action = a;
1862
+ result = action;
1863
+ }
1864
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1865
+ phase("render() post");
1866
+ app.screen.invalidate();
1867
+ app.requestRepaint();
1868
+ phase("post-mount invalidate+requestRepaint");
1869
+ await app.waitUntilExit();
1870
+ phase("waitUntilExit returned");
1871
+ app.unmount();
1872
+ phase("unmount done");
1873
+ result = action;
1874
+ break;
1782
1875
  }
1783
- let selection = { type: "back" };
1784
- const app = render(/* @__PURE__ */ jsxDEV10(Resume, {
1785
- sessionId,
1786
- onSelect: (s) => {
1787
- selection = s;
1876
+ case "resume": {
1877
+ const sessionId = args[0];
1878
+ if (!sessionId) {
1879
+ console.error("resume requires sessionId");
1880
+ process.exit(1);
1788
1881
  }
1789
- }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true });
1790
- await app.waitUntilExit();
1791
- app.unmount();
1792
- result = selection;
1793
- break;
1882
+ let selection = { type: "back" };
1883
+ result = selection;
1884
+ phase("render() pre");
1885
+ const app = render(/* @__PURE__ */ jsxDEV10(Resume, {
1886
+ sessionId,
1887
+ onSelect: (s) => {
1888
+ selection = s;
1889
+ result = selection;
1890
+ }
1891
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1892
+ phase("render() post");
1893
+ app.screen.invalidate();
1894
+ app.requestRepaint();
1895
+ phase("post-mount invalidate+requestRepaint");
1896
+ await app.waitUntilExit();
1897
+ phase("waitUntilExit returned");
1898
+ app.unmount();
1899
+ phase("unmount done");
1900
+ result = selection;
1901
+ break;
1902
+ }
1903
+ default:
1904
+ console.error(`Unknown screen: ${screen}`);
1905
+ process.exit(1);
1794
1906
  }
1795
- default:
1796
- console.error(`Unknown screen: ${screen}`);
1797
- process.exit(1);
1907
+ writeResult();
1908
+ phase("result written");
1909
+ } finally {
1910
+ writeResult();
1911
+ phase("finally complete");
1798
1912
  }
1799
- writeFileSync2(outputPath, JSON.stringify(result));
1800
1913
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertrand",
3
- "version": "0.20.1",
3
+ "version": "0.22.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },