bertrand 0.20.1 → 0.21.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/README.md CHANGED
@@ -79,13 +79,14 @@ Shows a picker: start a fresh Claude conversation, or resume one of the prior co
79
79
  bertrand list
80
80
  ```
81
81
 
82
- Interactive picker showing all sessions with status badges.
82
+ Interactive picker showing all sessions with status badges. Add `--project <slug>` to list sessions from another project without switching the active one.
83
83
 
84
84
  ### Other commands
85
85
 
86
86
  | Command | Purpose |
87
87
  |---|---|
88
- | `bertrand log <session>` | Print the timeline event log for a session. |
88
+ | `bertrand log` | List sessions in the active project. Add `--project <slug>` to scope to a different project. |
89
+ | `bertrand log <session>` | Print the timeline event log for a session. Supports `--json` (includes `project: { slug, name }` for agent consumption) and `--project <slug>` for cross-project reads. |
89
90
  | `bertrand stats <session>` | Print materialized stats (duration, work/wait split, lines changed). |
90
91
  | `bertrand archive <name>` | Archive or unarchive a session. |
91
92
  | `bertrand serve` | Start the dashboard HTTP API on `:5200`. |
package/dist/bertrand.js CHANGED
@@ -15,6 +15,7 @@ var __export = (target, all) => {
15
15
  });
16
16
  };
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = import.meta.require;
18
19
 
19
20
  // src/lib/paths.ts
20
21
  import { homedir } from "os";
@@ -25,6 +26,7 @@ var init_paths = __esm(() => {
25
26
  root: join(homedir(), BERTRAND_DIR),
26
27
  hooks: join(homedir(), BERTRAND_DIR, "hooks"),
27
28
  sessions: join(homedir(), BERTRAND_DIR, "sessions"),
29
+ runtime: join(homedir(), BERTRAND_DIR, "run"),
28
30
  db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
29
31
  syncEnv: join(homedir(), BERTRAND_DIR, "sync.env")
30
32
  };
@@ -3453,10 +3455,8 @@ function installExitHandlers() {
3453
3455
  const onSignal = (signal) => {
3454
3456
  if (isClaudeRunning())
3455
3457
  return;
3456
- process.exit(signal === "SIGINT" ? 130 : signal === "SIGHUP" ? 129 : 143);
3458
+ process.exit(signal === "SIGHUP" ? 129 : 143);
3457
3459
  };
3458
- process.on("SIGINT", onSignal);
3459
- process.on("SIGTERM", onSignal);
3460
3460
  process.on("SIGHUP", onSignal);
3461
3461
  }
3462
3462
  async function launch(opts) {
@@ -3568,6 +3568,7 @@ function finalizeSession(sessionId, conversationId, exitCode) {
3568
3568
  emitSessionEnded({ sessionId });
3569
3569
  computeAndPersist(sessionId);
3570
3570
  stopServerIfIdle();
3571
+ triggerBackgroundPush();
3571
3572
  }
3572
3573
  var liveSession = null, exitHandlersInstalled = false;
3573
3574
  var init_session = __esm(() => {
@@ -3582,27 +3583,56 @@ var init_session = __esm(() => {
3582
3583
  init_spawn_context();
3583
3584
  init_timing();
3584
3585
  init_server_lifecycle();
3586
+ init_trigger();
3585
3587
  });
3586
3588
 
3587
3589
  // src/tui/app.tsx
3588
3590
  import { spawn as spawn4 } from "child_process";
3589
3591
  import { existsSync as existsSync8, readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
3592
+ import { tmpdir } from "os";
3590
3593
  import { join as join9 } from "path";
3591
3594
  import { randomUUID as randomUUID2 } from "crypto";
3592
3595
  async function runScreen(screen, ...args) {
3593
- const tmpFile = `/tmp/bertrand-tui-${process.pid}-${Date.now()}.json`;
3596
+ const tmpFile = join9(tmpdir(), `bertrand-tui-${randomUUID2()}.json`);
3597
+ if (process.env.BERTRAND_DEBUG_TUI) {
3598
+ try {
3599
+ const { appendFileSync } = await import("fs");
3600
+ appendFileSync(process.env.BERTRAND_DEBUG_TUI, `--- parent runScreen("${screen}") at ${Date.now()} entry=${SCREEN_ENTRY}
3601
+ `);
3602
+ } catch {}
3603
+ }
3594
3604
  const child = spawn4("bun", ["run", SCREEN_ENTRY, screen, tmpFile, ...args], {
3595
- stdio: "inherit"
3596
- });
3597
- const exitCode = await new Promise((resolve) => {
3598
- child.on("exit", (code) => resolve(code ?? 1));
3605
+ stdio: "inherit",
3606
+ env: process.env
3599
3607
  });
3600
- if (exitCode !== 0) {
3601
- throw new Error(`TUI screen "${screen}" exited with code ${exitCode}`);
3608
+ const noopSignal = () => {};
3609
+ process.on("SIGINT", noopSignal);
3610
+ process.on("SIGTERM", noopSignal);
3611
+ let spawnError = null;
3612
+ try {
3613
+ const { code, signal } = await new Promise((resolve) => {
3614
+ child.on("error", (err) => {
3615
+ spawnError = err;
3616
+ resolve({ code: 1, signal: null });
3617
+ });
3618
+ child.on("exit", (c, s) => resolve({ code: c, signal: s }));
3619
+ });
3620
+ if (spawnError) {
3621
+ throw new Error(`Failed to launch TUI screen "${screen}": ${spawnError.message}`);
3622
+ }
3623
+ if (!existsSync8(tmpFile)) {
3624
+ const detail = signal ? `killed by ${signal}` : `exited with code ${code ?? "?"} without writing result`;
3625
+ throw new Error(`TUI screen "${screen}" ${detail}`);
3626
+ }
3627
+ return JSON.parse(readFileSync8(tmpFile, "utf-8"));
3628
+ } finally {
3629
+ process.removeListener("SIGINT", noopSignal);
3630
+ process.removeListener("SIGTERM", noopSignal);
3631
+ try {
3632
+ if (existsSync8(tmpFile))
3633
+ unlinkSync3(tmpFile);
3634
+ } catch {}
3602
3635
  }
3603
- const result = JSON.parse(readFileSync8(tmpFile, "utf-8"));
3604
- unlinkSync3(tmpFile);
3605
- return result;
3606
3636
  }
3607
3637
  async function startLaunchTui() {
3608
3638
  return runScreen("launch");
@@ -3615,9 +3645,6 @@ async function startExitTui(sessionId) {
3615
3645
  }
3616
3646
  async function startResumeTui(sessionId) {
3617
3647
  const conversations2 = getConversationsBySession(sessionId);
3618
- if (conversations2.length === 1) {
3619
- return { type: "conversation", conversationId: conversations2[0].id };
3620
- }
3621
3648
  if (conversations2.length === 0) {
3622
3649
  return { type: "new" };
3623
3650
  }
@@ -3782,21 +3809,33 @@ var init_recovery = __esm(() => {
3782
3809
 
3783
3810
  // src/cli/commands/launch.ts
3784
3811
  var exports_launch = {};
3812
+ function reportFatal(err) {
3813
+ const message = err instanceof Error ? err.message : String(err);
3814
+ console.error(`bertrand: ${message}`);
3815
+ if (process.env.BERTRAND_DEBUG && err instanceof Error && err.stack) {
3816
+ console.error(err.stack);
3817
+ }
3818
+ process.exit(1);
3819
+ }
3785
3820
  var init_launch = __esm(() => {
3786
3821
  init_router();
3787
3822
  init_app();
3788
3823
  init_session();
3789
3824
  init_recovery();
3790
3825
  register("launch", async (args) => {
3791
- recoverStaleSessions();
3792
- const sessionName = args[0];
3793
- if (sessionName) {
3794
- const { categoryPath, slug } = parseSessionName(sessionName);
3795
- const sessionId = await launch({ categoryPath, slug });
3796
- await runSessionLoop(sessionId);
3797
- return;
3826
+ try {
3827
+ recoverStaleSessions();
3828
+ const sessionName = args[0];
3829
+ if (sessionName) {
3830
+ const { categoryPath, slug } = parseSessionName(sessionName);
3831
+ const sessionId = await launch({ categoryPath, slug });
3832
+ await runSessionLoop(sessionId);
3833
+ return;
3834
+ }
3835
+ await startTui();
3836
+ } catch (err) {
3837
+ reportFatal(err);
3798
3838
  }
3799
- await startTui();
3800
3839
  });
3801
3840
  });
3802
3841
 
@@ -3804,7 +3843,7 @@ var init_launch = __esm(() => {
3804
3843
  function quietHelper(bin) {
3805
3844
  return `bq() { ${bin} "$@" 2>/dev/null || true; }`;
3806
3845
  }
3807
- function waitingScript(bin) {
3846
+ function waitingScript(bin, runtimeDir) {
3808
3847
  return `#!/usr/bin/env bash
3809
3848
  # Hook: PreToolUse AskUserQuestion \u2192 enforce multiSelect, mark session as waiting
3810
3849
  ${quietHelper(bin)}
@@ -3829,7 +3868,7 @@ question="$(printf '%s' "$input" | grep -o '"question":"[^"]*"' | head -1 | cut
3829
3868
  [ -z "$question" ] && question="Waiting for input"
3830
3869
 
3831
3870
  # Clear working debounce marker so next resume\u2192working transition fires
3832
- rm -f "/tmp/bertrand-working-$sid"
3871
+ rm -f "${runtimeDir}/working-$sid"
3833
3872
 
3834
3873
  bq update --session-id "$sid" --event session.waiting --meta "$(jq -n --arg q "$question" --arg cid "$cid" '{question:$q, claude_id:$cid}')"
3835
3874
 
@@ -3850,7 +3889,7 @@ bq notify bertrand "$question" &
3850
3889
  wait
3851
3890
  `;
3852
3891
  }
3853
- function answeredScript(bin) {
3892
+ function answeredScript(bin, _runtimeDir) {
3854
3893
  return `#!/usr/bin/env bash
3855
3894
  # Hook: PostToolUse AskUserQuestion \u2192 mark session as active
3856
3895
  #
@@ -3904,7 +3943,7 @@ fi
3904
3943
  wait
3905
3944
  `;
3906
3945
  }
3907
- function activeScript(bin) {
3946
+ function activeScript(bin, runtimeDir) {
3908
3947
  return `#!/usr/bin/env bash
3909
3948
  # Hook: PreToolUse (catch-all) \u2192 flip waiting to active
3910
3949
  ${quietHelper(bin)}
@@ -3913,7 +3952,7 @@ sid="\${BERTRAND_SESSION:-}"
3913
3952
 
3914
3953
  # Debounce: skip if we already sent session.active within the last 5 seconds.
3915
3954
  # This avoids spawning bertrand (~31ms) on every tool call during rapid sequences.
3916
- marker="/tmp/bertrand-working-$sid"
3955
+ marker="${runtimeDir}/working-$sid"
3917
3956
  if [ -f "$marker" ]; then
3918
3957
  age=$(( $(date +%s) - $(stat -f%m "$marker" 2>/dev/null || echo 0) ))
3919
3958
  [ "$age" -lt 5 ] && exit 0
@@ -3930,7 +3969,7 @@ cid="\${BERTRAND_CLAUDE_ID:-}"
3930
3969
  bq update --session-id "$sid" --event session.active --meta "$(jq -n --arg cid "$cid" '{claude_id:$cid}')"
3931
3970
  `;
3932
3971
  }
3933
- function permissionWaitScript(bin) {
3972
+ function permissionWaitScript(bin, runtimeDir) {
3934
3973
  return `#!/usr/bin/env bash
3935
3974
  # Hook: PermissionRequest \u2192 mark pending, emit permission.request
3936
3975
  ${quietHelper(bin)}
@@ -3942,7 +3981,7 @@ ${EXTRACT_TOOL}
3942
3981
  [ "$tool" = "AskUserQuestion" ] && exit 0
3943
3982
 
3944
3983
  # Write marker so PostToolUse knows this was a real permission prompt (not auto-approved)
3945
- touch "/tmp/bertrand-perm-pending-$sid"
3984
+ touch "${runtimeDir}/perm-pending-$sid"
3946
3985
 
3947
3986
  # Extract detail from tool_input via grep (avoid jq for simple fields)
3948
3987
  detail=""
@@ -3960,7 +3999,7 @@ bq notify bertrand "Needs permission: $tool" &
3960
3999
  wait
3961
4000
  `;
3962
4001
  }
3963
- function permissionDoneScript(bin) {
4002
+ function permissionDoneScript(bin, runtimeDir) {
3964
4003
  return `#!/usr/bin/env bash
3965
4004
  # Hook: PostToolUse (catch-all)
3966
4005
  #
@@ -3985,7 +4024,7 @@ ${EXTRACT_TOOL}
3985
4024
  # Don't double-log: AskUserQuestion has its own waiting/answered events
3986
4025
  [ "$tool" = "AskUserQuestion" ] && exit 0
3987
4026
 
3988
- marker="/tmp/bertrand-perm-pending-$sid"
4027
+ marker="${runtimeDir}/perm-pending-$sid"
3989
4028
  had_marker=0
3990
4029
  if [ -f "$marker" ]; then
3991
4030
  had_marker=1
@@ -4044,7 +4083,7 @@ fi
4044
4083
  wait
4045
4084
  `;
4046
4085
  }
4047
- function userPromptScript(bin) {
4086
+ function userPromptScript(bin, _runtimeDir) {
4048
4087
  return `#!/usr/bin/env bash
4049
4088
  # Hook: UserPromptSubmit \u2192 record user free-text prompt
4050
4089
  ${quietHelper(bin)}
@@ -4060,7 +4099,7 @@ meta="$(printf '%s' "$input" | jq --arg cid "$cid" '{prompt: (.prompt // ""), cl
4060
4099
  bq update --session-id "$sid" --event user.prompt --meta "$meta"
4061
4100
  `;
4062
4101
  }
4063
- function doneScript(bin) {
4102
+ function doneScript(bin, _runtimeDir) {
4064
4103
  return `#!/usr/bin/env bash
4065
4104
  # Hook: Stop \u2192 mark session as paused
4066
4105
  ${quietHelper(bin)}
@@ -4102,9 +4141,10 @@ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5, chmodSync as
4102
4141
  import { join as join10 } from "path";
4103
4142
  function installHookScripts(bin) {
4104
4143
  mkdirSync7(paths.hooks, { recursive: true });
4144
+ mkdirSync7(paths.runtime, { recursive: true });
4105
4145
  for (const [filename, scriptFn] of Object.entries(HOOK_SCRIPTS)) {
4106
4146
  const filePath = join10(paths.hooks, filename);
4107
- writeFileSync5(filePath, scriptFn(bin));
4147
+ writeFileSync5(filePath, scriptFn(bin, paths.runtime));
4108
4148
  chmodSync2(filePath, 493);
4109
4149
  }
4110
4150
  console.log(`Installed ${Object.keys(HOOK_SCRIPTS).length} hook scripts to ${paths.hooks}`);
@@ -4293,6 +4333,7 @@ var init_init = __esm(() => {
4293
4333
  `);
4294
4334
  mkdirSync10(paths.root, { recursive: true });
4295
4335
  mkdirSync10(paths.hooks, { recursive: true });
4336
+ mkdirSync10(paths.runtime, { recursive: true });
4296
4337
  const active = resolveActiveProject();
4297
4338
  runMigrations(active.db);
4298
4339
  console.log(` Database: ${active.db}`);
@@ -4323,6 +4364,40 @@ Error: couldn't locate the bertrand binary on PATH.
4323
4364
  });
4324
4365
  });
4325
4366
 
4367
+ // src/lib/projects/cli-flag.ts
4368
+ function extractProjectFlag2(args) {
4369
+ const rest = [];
4370
+ let project;
4371
+ for (let i = 0;i < args.length; i++) {
4372
+ const a = args[i];
4373
+ if (a === "--project") {
4374
+ project = args[i + 1];
4375
+ i++;
4376
+ continue;
4377
+ }
4378
+ if (a.startsWith("--project=")) {
4379
+ project = a.slice("--project=".length);
4380
+ continue;
4381
+ }
4382
+ rest.push(a);
4383
+ }
4384
+ return { project, rest };
4385
+ }
4386
+ function applyProjectFlag(slug) {
4387
+ if (!slug)
4388
+ return;
4389
+ if (!projectExists(slug)) {
4390
+ console.error(`Unknown project: ${slug}. Run \`bertrand project list\` to see registered projects.`);
4391
+ process.exit(1);
4392
+ }
4393
+ process.env.BERTRAND_PROJECT = slug;
4394
+ _resetActiveProjectCache();
4395
+ }
4396
+ var init_cli_flag = __esm(() => {
4397
+ init_resolve();
4398
+ init_registry();
4399
+ });
4400
+
4326
4401
  // src/cli/commands/list.ts
4327
4402
  var exports_list = {};
4328
4403
  function buildRows(sessions2) {
@@ -4338,13 +4413,15 @@ function buildRows(sessions2) {
4338
4413
  });
4339
4414
  }
4340
4415
  function renderTable(rows) {
4416
+ const dim = "\x1B[2m";
4417
+ const reset = "\x1B[0m";
4418
+ const project = resolveActiveProject();
4419
+ console.log(`${dim}Project: ${project.slug} (${project.name})${reset}`);
4341
4420
  if (rows.length === 0) {
4342
4421
  console.log("No sessions found.");
4343
4422
  return;
4344
4423
  }
4345
4424
  const maxName = Math.max(...rows.map((r) => r.name.length), 4);
4346
- const dim = "\x1B[2m";
4347
- const reset = "\x1B[0m";
4348
4425
  console.log(`${dim}${" "} ${"NAME".padEnd(maxName)} ${"STATUS".padEnd(10)} ${"DURATION".padEnd(8)} ${"CONVOS".padEnd(6)} LAST ACTIVE${reset}`);
4349
4426
  for (const row of rows) {
4350
4427
  const dot = STATUS_DOTS[row.status] ?? "?";
@@ -4356,12 +4433,14 @@ function renderTable(rows) {
4356
4433
  }
4357
4434
  }
4358
4435
  function renderJson(rows) {
4436
+ const project = resolveActiveProject();
4359
4437
  const data = rows.map((r) => ({
4360
4438
  name: r.name,
4361
4439
  status: r.status,
4362
4440
  duration: r.duration,
4363
4441
  conversations: r.conversations,
4364
- updatedAt: r.updatedAt
4442
+ updatedAt: r.updatedAt,
4443
+ project: { slug: project.slug, name: project.name }
4365
4444
  }));
4366
4445
  console.log(JSON.stringify(data, null, 2));
4367
4446
  }
@@ -4372,6 +4451,8 @@ var init_list = __esm(() => {
4372
4451
  init_categories();
4373
4452
  init_stats();
4374
4453
  init_format();
4454
+ init_resolve();
4455
+ init_cli_flag();
4375
4456
  STATUS_DOTS = {
4376
4457
  active: "\x1B[32m\u25CF\x1B[0m",
4377
4458
  waiting: "\x1B[33m\u25CF\x1B[0m",
@@ -4380,10 +4461,12 @@ var init_list = __esm(() => {
4380
4461
  };
4381
4462
  alias("ls", "list");
4382
4463
  register("list", async (args) => {
4383
- const isJson = args.includes("--json");
4384
- const showAll = args.includes("--all") || args.includes("-a");
4385
- const categoryFlag = args.indexOf("--category");
4386
- const categoryPath = categoryFlag !== -1 ? args[categoryFlag + 1] : undefined;
4464
+ const { project: projectSlug, rest: argsWithoutProject } = extractProjectFlag2(args);
4465
+ applyProjectFlag(projectSlug);
4466
+ const isJson = argsWithoutProject.includes("--json");
4467
+ const showAll = argsWithoutProject.includes("--all") || argsWithoutProject.includes("-a");
4468
+ const categoryFlag = argsWithoutProject.indexOf("--category");
4469
+ const categoryPath = categoryFlag !== -1 ? argsWithoutProject[categoryFlag + 1] : undefined;
4387
4470
  let sessionRows;
4388
4471
  if (categoryPath) {
4389
4472
  const category = getCategoryByPath(categoryPath);
@@ -4643,7 +4726,9 @@ function ansi256(code, text2) {
4643
4726
  return `\x1B[38;5;${code}m${text2}${RESET}`;
4644
4727
  }
4645
4728
  function showAllSessions() {
4729
+ const project = resolveActiveProject();
4646
4730
  const rows = getAllSessions().sort((a, b) => new Date(b.session.updatedAt).getTime() - new Date(a.session.updatedAt).getTime());
4731
+ console.log(`${DIM}Project: ${project.slug} (${project.name})${RESET}`);
4647
4732
  if (rows.length === 0) {
4648
4733
  console.log("No sessions.");
4649
4734
  return;
@@ -4768,15 +4853,22 @@ function renderTimingFooter(sessionId) {
4768
4853
  lines.push(`${DIM}Duration: ${formatDuration(durationS * 1000)} \xB7 Claude: ${formatDuration(claudeWorkS * 1000)} (${activePctVal}%) \xB7 Wait: ${formatDuration(userWaitS * 1000)} (${100 - activePctVal}%)${RESET}`);
4769
4854
  return lines;
4770
4855
  }
4856
+ function renderBreadcrumb(projectSlug, projectName, sessionName) {
4857
+ const segments = sessionName.split("/").filter(Boolean);
4858
+ const trail = segments.join(` ${DIM}\u203A${RESET} `);
4859
+ return `${DIM}${projectSlug} (${projectName})${RESET} ${DIM}\xB7${RESET} ${BOLD}${trail}${RESET}`;
4860
+ }
4771
4861
  function showSessionLog(session, sessionName, isJson) {
4772
4862
  const sessionId = session.id;
4773
4863
  const rawEvents = getEventsBySession(sessionId);
4774
4864
  const enriched = enrichAll(rawEvents);
4775
4865
  const compacted = compact(enriched);
4866
+ const project = resolveActiveProject();
4776
4867
  if (isJson) {
4777
4868
  const stats = getSessionStats(sessionId);
4778
4869
  const conversations2 = getConversationsBySession(sessionId);
4779
4870
  console.log(JSON.stringify({
4871
+ project: { slug: project.slug, name: project.name },
4780
4872
  session: { ...session, name: sessionName },
4781
4873
  stats,
4782
4874
  conversations: conversations2,
@@ -4794,6 +4886,8 @@ function showSessionLog(session, sessionName, isJson) {
4794
4886
  }
4795
4887
  const segments = segmentByConversation(compacted);
4796
4888
  const allLines = [];
4889
+ allLines.push(renderBreadcrumb(project.slug, project.name, sessionName));
4890
+ allLines.push("");
4797
4891
  for (let i = 0;i < segments.length; i++) {
4798
4892
  const segLines = renderSegment(segments[i], i, segments.length);
4799
4893
  allLines.push(...segLines);
@@ -4815,6 +4909,8 @@ var init_log = __esm(() => {
4815
4909
  init_compact();
4816
4910
  init_timing();
4817
4911
  init_format();
4912
+ init_resolve();
4913
+ init_cli_flag();
4818
4914
  STATUS_DOTS2 = {
4819
4915
  active: ansi(32, "\u25CF"),
4820
4916
  waiting: ansi(33, "\u25CF"),
@@ -4822,8 +4918,10 @@ var init_log = __esm(() => {
4822
4918
  archived: `${DIM}\u25CB${RESET}`
4823
4919
  };
4824
4920
  register("log", async (args) => {
4825
- const isJson = args.includes("--json");
4826
- const filteredArgs = args.filter((a) => !a.startsWith("--"));
4921
+ const { project: projectSlug, rest: argsWithoutProject } = extractProjectFlag2(args);
4922
+ applyProjectFlag(projectSlug);
4923
+ const isJson = argsWithoutProject.includes("--json");
4924
+ const filteredArgs = argsWithoutProject.filter((a) => !a.startsWith("--"));
4827
4925
  const target = filteredArgs[0];
4828
4926
  if (!target) {
4829
4927
  showAllSessions();
@@ -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
  };
@@ -1324,7 +1325,7 @@ var OPTIONS = [
1324
1325
  {
1325
1326
  action: "resume",
1326
1327
  label: "Resume",
1327
- hint: "Start a new conversation in this session"
1328
+ hint: "Continue a conversation or start a new one"
1328
1329
  }
1329
1330
  ];
1330
1331
  function Exit({ sessionId, onAction }) {
@@ -1730,71 +1731,146 @@ if (!screen || !outputPath) {
1730
1731
  console.error("Usage: run-screen <screen> <outputPath> [args...]");
1731
1732
  process.exit(1);
1732
1733
  }
1734
+ var RESULT_PATH = outputPath;
1735
+ var DEBUG_PATH = process.env.BERTRAND_DEBUG_TUI || null;
1736
+ var START_HR = process.hrtime.bigint();
1737
+ function phase(name, detail) {
1738
+ if (!DEBUG_PATH)
1739
+ return;
1740
+ const elapsedMs = Number(process.hrtime.bigint() - START_HR) / 1e6;
1741
+ const line = `${elapsedMs.toFixed(2).padStart(8)}ms ${name}${detail ? " " + detail : ""}
1742
+ `;
1743
+ try {
1744
+ appendFileSync(DEBUG_PATH, line);
1745
+ } catch {}
1746
+ }
1747
+ phase("spawn", `screen=${screen} pid=${process.pid} isTTY=${process.stdout.isTTY ?? "?"} term=${process.env.TERM_PROGRAM || process.env.TERM || "?"}`);
1733
1748
  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);
1749
+ var resultWritten = false;
1750
+ function writeResult() {
1751
+ if (resultWritten)
1752
+ return;
1753
+ resultWritten = true;
1754
+ try {
1755
+ writeFileSync2(RESULT_PATH, JSON.stringify(result));
1756
+ } catch {}
1757
+ }
1758
+ process.on("SIGINT", writeResult);
1759
+ process.on("SIGTERM", writeResult);
1760
+ process.on("SIGHUP", writeResult);
1761
+ process.on("uncaughtException", writeResult);
1762
+ process.on("unhandledRejection", writeResult);
1763
+ var renderCount = 0;
1764
+ var onRender = (m) => {
1765
+ renderCount++;
1766
+ phase(`paint #${renderCount}`, `cells=${m.cellsChanged} took=${m.renderTime.toFixed(2)}ms`);
1767
+ };
1768
+ try {
1769
+ switch (screen) {
1770
+ case "launch": {
1771
+ let selection = { type: "quit" };
1772
+ result = selection;
1773
+ phase("render() pre");
1774
+ const app = render(/* @__PURE__ */ jsxDEV10(Launch, {
1775
+ onSelect: (s) => {
1776
+ selection = s;
1777
+ result = selection;
1778
+ }
1779
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1780
+ phase("render() post");
1781
+ app.screen.invalidate();
1782
+ app.requestRepaint();
1783
+ phase("post-mount invalidate+requestRepaint");
1784
+ await app.waitUntilExit();
1785
+ phase("waitUntilExit returned");
1786
+ app.unmount();
1787
+ phase("unmount done");
1788
+ result = selection;
1789
+ break;
1764
1790
  }
1765
- let action = "save";
1766
- const app = render(/* @__PURE__ */ jsxDEV10(Exit, {
1767
- sessionId,
1768
- onAction: (a) => {
1769
- action = a;
1791
+ case "project-picker": {
1792
+ let selection = { type: "quit" };
1793
+ result = selection;
1794
+ phase("render() pre");
1795
+ const app = render(/* @__PURE__ */ jsxDEV10(ProjectPicker, {
1796
+ onSelect: (s) => {
1797
+ selection = s;
1798
+ result = selection;
1799
+ }
1800
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1801
+ phase("render() post");
1802
+ app.screen.invalidate();
1803
+ app.requestRepaint();
1804
+ phase("post-mount invalidate+requestRepaint");
1805
+ await app.waitUntilExit();
1806
+ phase("waitUntilExit returned");
1807
+ app.unmount();
1808
+ phase("unmount done");
1809
+ result = selection;
1810
+ break;
1811
+ }
1812
+ case "exit": {
1813
+ const sessionId = args[0];
1814
+ if (!sessionId) {
1815
+ console.error("exit requires sessionId");
1816
+ process.exit(1);
1770
1817
  }
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);
1818
+ let action = "save";
1819
+ result = action;
1820
+ phase("render() pre");
1821
+ const app = render(/* @__PURE__ */ jsxDEV10(Exit, {
1822
+ sessionId,
1823
+ onAction: (a) => {
1824
+ action = a;
1825
+ result = action;
1826
+ }
1827
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1828
+ phase("render() post");
1829
+ app.screen.invalidate();
1830
+ app.requestRepaint();
1831
+ phase("post-mount invalidate+requestRepaint");
1832
+ await app.waitUntilExit();
1833
+ phase("waitUntilExit returned");
1834
+ app.unmount();
1835
+ phase("unmount done");
1836
+ result = action;
1837
+ break;
1782
1838
  }
1783
- let selection = { type: "back" };
1784
- const app = render(/* @__PURE__ */ jsxDEV10(Resume, {
1785
- sessionId,
1786
- onSelect: (s) => {
1787
- selection = s;
1839
+ case "resume": {
1840
+ const sessionId = args[0];
1841
+ if (!sessionId) {
1842
+ console.error("resume requires sessionId");
1843
+ process.exit(1);
1788
1844
  }
1789
- }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true });
1790
- await app.waitUntilExit();
1791
- app.unmount();
1792
- result = selection;
1793
- break;
1845
+ let selection = { type: "back" };
1846
+ result = selection;
1847
+ phase("render() pre");
1848
+ const app = render(/* @__PURE__ */ jsxDEV10(Resume, {
1849
+ sessionId,
1850
+ onSelect: (s) => {
1851
+ selection = s;
1852
+ result = selection;
1853
+ }
1854
+ }, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
1855
+ phase("render() post");
1856
+ app.screen.invalidate();
1857
+ app.requestRepaint();
1858
+ phase("post-mount invalidate+requestRepaint");
1859
+ await app.waitUntilExit();
1860
+ phase("waitUntilExit returned");
1861
+ app.unmount();
1862
+ phase("unmount done");
1863
+ result = selection;
1864
+ break;
1865
+ }
1866
+ default:
1867
+ console.error(`Unknown screen: ${screen}`);
1868
+ process.exit(1);
1794
1869
  }
1795
- default:
1796
- console.error(`Unknown screen: ${screen}`);
1797
- process.exit(1);
1870
+ writeResult();
1871
+ phase("result written");
1872
+ } finally {
1873
+ writeResult();
1874
+ phase("finally complete");
1798
1875
  }
1799
- writeFileSync2(outputPath, JSON.stringify(result));
1800
1876
  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.21.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },