@sealant/mend 0.27.4 → 0.28.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.
Files changed (60) hide show
  1. package/README.md +32 -10
  2. package/dist/{chunk-BMMHU2SH.js → chunk-7JPOKREB.js} +1 -1
  3. package/dist/{dashboard-S56N7YOG.js → dashboard-4WQC5MZ4.js} +1388 -602
  4. package/dist/main.js +5 -3
  5. package/man/mend-accounts.1 +1 -1
  6. package/man/mend-adopt.1 +1 -1
  7. package/man/mend-attach.1 +1 -1
  8. package/man/mend-codex.1 +1 -1
  9. package/man/mend-completions.1 +1 -1
  10. package/man/mend-connect.1 +1 -1
  11. package/man/mend-continue.1 +1 -1
  12. package/man/mend-doctor.1 +1 -1
  13. package/man/mend-dotfiles-sync.1 +1 -1
  14. package/man/mend-dotfiles.1 +1 -1
  15. package/man/mend-env-cluster.1 +1 -1
  16. package/man/mend-env-load.1 +1 -1
  17. package/man/mend-env-show.1 +1 -1
  18. package/man/mend-help.1 +1 -1
  19. package/man/mend-keys-init.1 +1 -1
  20. package/man/mend-keys-mode.1 +1 -1
  21. package/man/mend-keys-share.1 +1 -1
  22. package/man/mend-keys-show.1 +1 -1
  23. package/man/mend-login.1 +1 -1
  24. package/man/mend-logout.1 +1 -1
  25. package/man/mend-man.1 +1 -1
  26. package/man/mend-pair.1 +1 -1
  27. package/man/mend-projects.1 +1 -1
  28. package/man/mend-refresh.1 +1 -1
  29. package/man/mend-rejoin.1 +1 -1
  30. package/man/mend-resume.1 +1 -1
  31. package/man/mend-run.1 +1 -1
  32. package/man/mend-server-logs.1 +1 -1
  33. package/man/mend-server-restart.1 +1 -1
  34. package/man/mend-server-setup.1 +1 -1
  35. package/man/mend-server-start.1 +1 -1
  36. package/man/mend-server-status.1 +1 -1
  37. package/man/mend-server-stop.1 +1 -1
  38. package/man/mend-server-upgrade.1 +1 -1
  39. package/man/mend-server.1 +1 -1
  40. package/man/mend-service-add.1 +1 -1
  41. package/man/mend-service-connect.1 +1 -1
  42. package/man/mend-service-init.1 +1 -1
  43. package/man/mend-service-list.1 +1 -1
  44. package/man/mend-service-logs.1 +1 -1
  45. package/man/mend-service-restart.1 +1 -1
  46. package/man/mend-service-run.1 +1 -1
  47. package/man/mend-service-stop.1 +1 -1
  48. package/man/mend-sessions.1 +1 -1
  49. package/man/mend-shell.1 +1 -1
  50. package/man/mend-skills-push.1 +1 -1
  51. package/man/mend-skills.1 +1 -1
  52. package/man/mend-ssh-setup.1 +1 -1
  53. package/man/mend-ssh.1 +1 -1
  54. package/man/mend-stop.1 +1 -1
  55. package/man/mend-ui.1 +6 -2
  56. package/man/mend-uninstall.1 +1 -1
  57. package/man/mend-version.1 +1 -1
  58. package/man/mend-worktrees.1 +1 -1
  59. package/man/mend.1 +1 -1
  60. package/package.json +3 -3
@@ -8,7 +8,7 @@ import {
8
8
  normalizeProjectName,
9
9
  pendingId,
10
10
  repositoryCloneUrlIssue
11
- } from "./chunk-BMMHU2SH.js";
11
+ } from "./chunk-7JPOKREB.js";
12
12
 
13
13
  // src/dashboard.tsx
14
14
  import { CliRenderEvents, createCliRenderer } from "@opentui/core";
@@ -20,7 +20,7 @@ import {
20
20
  useQuery as useQuery2,
21
21
  useQueryClient as useQueryClient2
22
22
  } from "@tanstack/react-query";
23
- import { useEffect as useEffect2, useRef as useRef2, useState as useState2, useSyncExternalStore } from "react";
23
+ import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2, useSyncExternalStore } from "react";
24
24
 
25
25
  // src/dashboard-adoption.ts
26
26
  var deriveAdoptOffer = (facts) => {
@@ -143,7 +143,9 @@ var deriveWorktrees = (data, projectId) => {
143
143
  const item = toSessionItem(data, detail);
144
144
  const sorted = detail.sessions.filter((session) => !isDeadEnd(session)).toSorted(bySessionRecency);
145
145
  const groups = [];
146
- if (detail.worktrees !== void 0) {
146
+ if (detail.worktrees === void 0) {
147
+ for (const session of sorted) groups.push(pseudoGroup(session, item));
148
+ } else {
147
149
  const claimed = /* @__PURE__ */ new Set();
148
150
  for (const worktree of detail.worktrees) {
149
151
  const members = sorted.filter((session) => session.worktreeId === worktree.id);
@@ -164,8 +166,6 @@ var deriveWorktrees = (data, projectId) => {
164
166
  if (claimed.has(session.id)) continue;
165
167
  groups.push(pseudoGroup(session, item));
166
168
  }
167
- } else {
168
- for (const session of sorted) groups.push(pseudoGroup(session, item));
169
169
  }
170
170
  return groups.toSorted((a, b) => {
171
171
  if (a.live > 0 !== b.live > 0) return a.live > 0 ? -1 : 1;
@@ -185,13 +185,6 @@ var pseudoGroup = (session, item) => ({
185
185
  live: LIVE_STATUSES.has(session.status) ? 1 : 0,
186
186
  annotation: item(session).annotation
187
187
  });
188
- var deriveRows = (groups) => groups.flatMap(
189
- (group) => [
190
- { kind: "worktree", group },
191
- ...group.sessions.map((item) => ({ kind: "session", group, item }))
192
- ]
193
- );
194
- var rowKeyOf = (row) => row.kind === "worktree" ? `wt:${row.group.key}` : `s:${row.item.session.id}`;
195
188
  var deriveHarnesses = (resuming) => {
196
189
  if (resuming !== null) {
197
190
  const others = Object.keys(HARNESS_COMMANDS).filter((h) => h !== resuming.harness);
@@ -218,6 +211,20 @@ var deriveHarnesses = (resuming) => {
218
211
  })
219
212
  );
220
213
  };
214
+ var sessionDisplayName = (session) => {
215
+ if (session.label !== null && session.label !== "") return session.label;
216
+ if (isPendingId(session.id)) return `${session.harness} \xB7 starting`;
217
+ return `${session.harness} ${session.id.slice(0, 8)}`;
218
+ };
219
+ var groupBaseLabel = (group) => {
220
+ if (group.baseRef !== null && group.baseRef !== "") return group.baseRef;
221
+ const sha = group.sessions[0]?.session.baseSha ?? "";
222
+ return sha === "" ? "base unknown" : sha.slice(0, 7);
223
+ };
224
+ var groupActivityAt = (group) => group.sessions.reduce(
225
+ (newest, item) => item.session.createdAt > newest ? item.session.createdAt : newest,
226
+ group.createdAt
227
+ );
221
228
  var foldGroupStatus = (group) => {
222
229
  const statuses = group.sessions.map((item) => item.session.status);
223
230
  if (statuses.includes("waiting")) return "waiting";
@@ -227,12 +234,33 @@ var foldGroupStatus = (group) => {
227
234
  };
228
235
  var liveShellOf = (processes) => processes.findLast((process2) => process2.kind === "shell" && process2.exitedAt === null) ?? null;
229
236
  var liveProtocolOf = (processes) => processes.find((process2) => process2.kind === "agent-protocol" && process2.exitedAt === null) ?? null;
230
- var enterTargetOf = (sessions) => {
231
- const live = sessions.filter((session) => LIVE_STATUSES.has(session.status));
232
- const attachable = live.find((session) => session.status !== "starting");
233
- if (attachable !== void 0) return { kind: "attach", session: attachable };
234
- const starting = live[0];
235
- return starting === void 0 ? { kind: "none" } : { kind: "wait", session: starting };
237
+ var planAttach = (session) => {
238
+ if (session === null) return { kind: "none" };
239
+ if (isPendingId(session.id)) return { kind: "pending", session };
240
+ if (!LIVE_STATUSES.has(session.status)) return { kind: "settled", session };
241
+ if (session.status === "starting") return { kind: "starting", session };
242
+ return { kind: "attach", session };
243
+ };
244
+ var planResume = (session) => {
245
+ if (session === null) return { kind: "none" };
246
+ if (isPendingId(session.id)) return { kind: "pending", session };
247
+ if (LIVE_STATUSES.has(session.status)) return { kind: "live", session };
248
+ return { kind: "resume", session };
249
+ };
250
+ var createLaunchGate = () => {
251
+ const taken = /* @__PURE__ */ new Set();
252
+ return {
253
+ take: (key) => {
254
+ if (taken.has(key)) return false;
255
+ taken.add(key);
256
+ return true;
257
+ },
258
+ release: (key) => {
259
+ taken.delete(key);
260
+ },
261
+ held: (key) => taken.has(key),
262
+ count: () => taken.size
263
+ };
236
264
  };
237
265
  var markSessionStopped = (data, sessionId) => {
238
266
  const processesBySession = new Map(data.processesBySession);
@@ -290,6 +318,17 @@ var filterBranches = (branches, query) => {
290
318
  return b.branch.committedAt.localeCompare(a.branch.committedAt);
291
319
  }).map((entry) => entry.branch);
292
320
  };
321
+ var baseStepNotice = (state) => {
322
+ if (state.branchError !== null) {
323
+ return `branches unreadable \u2014 ${state.branchError} \xB7 enter uses the default`;
324
+ }
325
+ if (state.branches === null) return "reading branches\u2026";
326
+ if (state.branches.length === 0) return "no branches read \u2014 enter uses the default branch";
327
+ if (filterBranches(state.branches, state.query).length === 0) {
328
+ return "no branch matches \u2014 enter uses the default branch";
329
+ }
330
+ return null;
331
+ };
293
332
  var advanceFromBase = (current) => {
294
333
  const chosen = filterBranches(current.branches ?? [], current.query)[current.baseIndex];
295
334
  return {
@@ -298,6 +337,366 @@ var advanceFromBase = (current) => {
298
337
  step: "harness"
299
338
  };
300
339
  };
340
+ var COLUMNS = ["projects", "worktrees", "sessions", "detail"];
341
+ var NAV_SECTIONS = ["projects", "worktrees", "sessions"];
342
+ var isNavSection = (column) => column !== "detail";
343
+ var expandedSection = (focus, lastNav) => isNavSection(focus) ? focus : lastNav;
344
+ var SIDEBAR_SHARE = 0.25;
345
+ var MIN_SIDEBAR_WIDTH = 24;
346
+ var MIN_DETAIL_WIDTH = 48;
347
+ var SPLIT_MIN_WIDTH = MIN_SIDEBAR_WIDTH + MIN_DETAIL_WIDTH;
348
+ var MIN_SECTION_HEIGHT = 3;
349
+ var CHROME_ROWS = 3;
350
+ var stackSections = (body, open) => {
351
+ const room = body - (NAV_SECTIONS.length - 1) * MIN_SECTION_HEIGHT;
352
+ if (room < MIN_SECTION_HEIGHT) {
353
+ const height = Math.max(MIN_SECTION_HEIGHT, body);
354
+ return [{ section: open, expanded: true, height, rows: height - 2 }];
355
+ }
356
+ return NAV_SECTIONS.map(
357
+ (section) => section === open ? { section, expanded: true, height: room, rows: room - 2 } : { section, expanded: false, height: MIN_SECTION_HEIGHT, rows: 1 }
358
+ );
359
+ };
360
+ var planLayout = (width, height, focus, lastNav) => {
361
+ const split = width >= SPLIT_MIN_WIDTH;
362
+ const navVisible = split || isNavSection(focus);
363
+ const sidebarOuter = split ? Math.max(
364
+ MIN_SIDEBAR_WIDTH,
365
+ Math.min(width - MIN_DETAIL_WIDTH, Math.round(width * SIDEBAR_SHARE))
366
+ ) : navVisible ? Math.max(4, width) : 0;
367
+ const detailOuter = split ? width - sidebarOuter : navVisible ? 0 : Math.max(4, width);
368
+ const build = (body2, breadcrumb) => {
369
+ const sections = navVisible ? stackSections(body2, expandedSection(focus, lastNav)) : [];
370
+ const shown = new Set(sections.map((section) => section.section));
371
+ if (detailOuter > 0) shown.add("detail");
372
+ return {
373
+ split,
374
+ sidebarWidth: navVisible ? sidebarOuter : 0,
375
+ sections,
376
+ detailWidth: detailOuter === 0 ? 0 : Math.max(1, detailOuter - 2),
377
+ detailRows: detailOuter === 0 ? 0 : Math.max(1, body2 - 2),
378
+ offscreen: COLUMNS.filter((column) => !shown.has(column)),
379
+ breadcrumb
380
+ };
381
+ };
382
+ const body = Math.max(MIN_SECTION_HEIGHT, height - CHROME_ROWS);
383
+ const full = build(body, false);
384
+ if (full.offscreen.length === 0) return full;
385
+ return body - 1 >= MIN_SECTION_HEIGHT ? build(body - 1, true) : full;
386
+ };
387
+ var stepColumn = (focus, delta) => {
388
+ const at = COLUMNS.indexOf(focus);
389
+ if (at === -1) return "sessions";
390
+ return COLUMNS[Math.max(0, Math.min(COLUMNS.length - 1, at + delta))] ?? focus;
391
+ };
392
+ var fitHints = (hints, width) => {
393
+ const kept = [];
394
+ for (const hint of hints) {
395
+ const candidate = [...kept, hint].join(" \xB7 ");
396
+ const wouldTruncate = kept.length + 1 < hints.length;
397
+ if (candidate.length + (wouldTruncate ? 2 : 0) > width) break;
398
+ kept.push(hint);
399
+ }
400
+ if (kept.length === hints.length) return kept.join(" \xB7 ");
401
+ return kept.length === 0 ? "\u2026" : `${kept.join(" \xB7 ")} \u2026`;
402
+ };
403
+ var clampIndex = (length, index) => length === 0 ? 0 : Math.max(0, Math.min(length - 1, index));
404
+ var KEY_BINDINGS = [
405
+ {
406
+ verb: "moveUp",
407
+ keys: ["up", "k"],
408
+ shift: false,
409
+ hints: {
410
+ projects: "\u2191\u2193 move",
411
+ worktrees: "\u2191\u2193 move",
412
+ sessions: "\u2191\u2193 move",
413
+ detail: "\u2191\u2193 scroll"
414
+ }
415
+ },
416
+ { verb: "moveDown", keys: ["down", "j"], shift: false, hints: {} },
417
+ { verb: "pageUp", keys: ["pageup"], shift: false, hints: {} },
418
+ { verb: "pageDown", keys: ["pagedown"], shift: false, hints: {} },
419
+ {
420
+ verb: "columnRight",
421
+ keys: ["return", "linefeed", "l", "right", "tab"],
422
+ shift: false,
423
+ hints: { projects: "\u2192 worktrees", worktrees: "\u2190\u2192 panes", sessions: "\u2190\u2192 panes" }
424
+ },
425
+ {
426
+ verb: "columnLeft",
427
+ keys: ["left", "h", "-", "backspace"],
428
+ shift: false,
429
+ hints: { detail: "\u2190 sessions" }
430
+ },
431
+ { verb: "columnLeft", keys: ["backtab"], shift: "any", hints: {} },
432
+ { verb: "columnLeft", keys: ["tab"], shift: true, hints: {} },
433
+ {
434
+ verb: "attach",
435
+ keys: ["a"],
436
+ shift: false,
437
+ hints: { worktrees: "a attach", sessions: "a attach", detail: "a attach" }
438
+ },
439
+ {
440
+ verb: "resume",
441
+ keys: ["r"],
442
+ shift: false,
443
+ hints: { worktrees: "r resume", sessions: "r resume", detail: "r resume" }
444
+ },
445
+ {
446
+ verb: "newSession",
447
+ keys: ["n"],
448
+ shift: false,
449
+ hints: {
450
+ projects: "n/w new worktree",
451
+ worktrees: "n new session",
452
+ sessions: "n new session"
453
+ }
454
+ },
455
+ {
456
+ verb: "newWorktree",
457
+ keys: ["w"],
458
+ shift: false,
459
+ hints: { worktrees: "w new worktree", sessions: "w new worktree" }
460
+ },
461
+ {
462
+ verb: "stop",
463
+ keys: ["k"],
464
+ shift: true,
465
+ hints: { worktrees: "\u21E7K stop all", sessions: "\u21E7K stop" }
466
+ },
467
+ { verb: "stop", keys: ["x"], shift: false, hints: {} },
468
+ {
469
+ verb: "remove",
470
+ keys: ["d"],
471
+ shift: true,
472
+ hints: { worktrees: "\u21E7D remove worktree", sessions: "\u21E7D remove" }
473
+ },
474
+ {
475
+ verb: "review",
476
+ keys: ["v"],
477
+ shift: false,
478
+ hints: { worktrees: "v review", sessions: "v review", detail: "v review" }
479
+ },
480
+ {
481
+ verb: "rename",
482
+ keys: ["e"],
483
+ shift: false,
484
+ hints: { sessions: "e rename", detail: "e rename" }
485
+ },
486
+ { verb: "openWeb", keys: ["o"], shift: false, hints: { sessions: "o web", detail: "o web" } },
487
+ {
488
+ verb: "refresh",
489
+ keys: ["r"],
490
+ shift: true,
491
+ hints: { projects: "\u21E7R refresh", worktrees: "\u21E7R refresh", detail: "\u21E7R refresh" }
492
+ },
493
+ {
494
+ verb: "quit",
495
+ keys: ["q"],
496
+ shift: false,
497
+ hints: { projects: "q quit", worktrees: "q quit", sessions: "q quit", detail: "q quit" }
498
+ }
499
+ ];
500
+ var verbForKey = (name, shift) => KEY_BINDINGS.find(
501
+ (binding) => binding.keys.includes(name) && (binding.shift === "any" || binding.shift === shift)
502
+ )?.verb ?? null;
503
+ var verbHints = (focus) => KEY_BINDINGS.flatMap((binding) => {
504
+ const hint = binding.hints[focus];
505
+ return hint === void 0 ? [] : [hint];
506
+ });
507
+
508
+ // src/dashboard-preview.ts
509
+ import { stripVTControlCharacters } from "node:util";
510
+ var BLOCK_LINES = {
511
+ user: 6,
512
+ assistant: 14,
513
+ reasoning: 4,
514
+ command: 3,
515
+ output: 5,
516
+ meta: 1,
517
+ blank: 1
518
+ };
519
+ var CONTROL_STRING = (
520
+ // eslint-disable-next-line no-control-regex -- matching control bytes is the point
521
+ /(?:\u001b[\]P^_X]|[\u0090\u0098\u009d\u009e\u009f])[\s\S]*?(?:\u0007|\u001b\\|\u009c|$)/g
522
+ );
523
+ var BELL = "\x07";
524
+ var UNSAFE_CONTROLS = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
525
+ var lastWrite = (line) => line.includes("\r") ? line.split("\r").filter((part) => part !== "").at(-1) ?? "" : line;
526
+ var plainText = (text) => stripVTControlCharacters(text.replaceAll(CONTROL_STRING, "").replaceAll(BELL, "")).replaceAll("\r\n", "\n").split("\n").map(lastWrite).join("\n").replaceAll(UNSAFE_CONTROLS, "");
527
+ var GRAPHEMES = new Intl.Segmenter("en", { granularity: "grapheme" });
528
+ var NON_ASCII = /[^\u0020-\u007e\t]/;
529
+ var ZERO_WIDTH = /^[\p{Mn}\p{Me}\p{Cf}]/u;
530
+ var EMOJI_PRESENTATION = "\uFE0F";
531
+ var WIDE_RANGES = [
532
+ [4352, 4447],
533
+ // Hangul Jamo
534
+ [11904, 12350],
535
+ // CJK radicals, Kangxi, CJK symbols and punctuation
536
+ [12353, 42191],
537
+ // Kana, Bopomofo, CJK unified ideographs, Yi
538
+ [43360, 43391],
539
+ // Hangul Jamo Extended-A
540
+ [44032, 55203],
541
+ // Hangul syllables
542
+ [63744, 64255],
543
+ // CJK compatibility ideographs
544
+ [65040, 65049],
545
+ // Vertical forms
546
+ [65072, 65135],
547
+ // CJK compatibility forms, small form variants
548
+ [65280, 65376],
549
+ // Fullwidth forms
550
+ [65504, 65510],
551
+ // Fullwidth signs
552
+ [94176, 94180],
553
+ // Tangut and Nushu marks
554
+ [94208, 101589],
555
+ // Tangut
556
+ [110592, 111359],
557
+ // Kana supplement and extensions
558
+ [127488, 127743],
559
+ // Enclosed ideographic supplement
560
+ [127744, 129791],
561
+ // Emoji planes
562
+ [131072, 262141]
563
+ // CJK unified ideographs, extension B onwards
564
+ ];
565
+ var isWide = (code) => WIDE_RANGES.some(([low, high]) => code >= low && code <= high);
566
+ var graphemeWidth = (cluster) => {
567
+ if (ZERO_WIDTH.test(cluster)) return 0;
568
+ if (cluster.includes(EMOJI_PRESENTATION)) return 2;
569
+ return isWide(cluster.codePointAt(0) ?? 0) ? 2 : 1;
570
+ };
571
+ var displayWidth = (text) => {
572
+ if (!NON_ASCII.test(text)) return text.length;
573
+ let width = 0;
574
+ for (const { segment } of GRAPHEMES.segment(text)) width += graphemeWidth(segment);
575
+ return width;
576
+ };
577
+ var cutToWidth = (text, limit) => {
578
+ if (!NON_ASCII.test(text)) return [text.slice(0, limit), text.slice(limit)];
579
+ let width = 0;
580
+ for (const { segment, index } of GRAPHEMES.segment(text)) {
581
+ const next = width + graphemeWidth(segment);
582
+ if (next > limit) {
583
+ const at = index === 0 ? segment.length : index;
584
+ return [text.slice(0, at), text.slice(at)];
585
+ }
586
+ width = next;
587
+ }
588
+ return [text, ""];
589
+ };
590
+ var wrapText = (text, width) => {
591
+ const limit = Math.max(8, width);
592
+ const out = [];
593
+ for (const paragraph of plainText(text).replaceAll(" ", " ").split("\n")) {
594
+ const trimmed = paragraph.trimEnd();
595
+ if (trimmed === "") {
596
+ out.push("");
597
+ continue;
598
+ }
599
+ let line = "";
600
+ let lineWidth = 0;
601
+ for (const word of trimmed.split(/ +/)) {
602
+ const wordWidth = displayWidth(word);
603
+ if (line === "") {
604
+ line = word;
605
+ lineWidth = wordWidth;
606
+ } else if (lineWidth + 1 + wordWidth <= limit) {
607
+ line = `${line} ${word}`;
608
+ lineWidth += 1 + wordWidth;
609
+ } else {
610
+ out.push(line);
611
+ line = word;
612
+ lineWidth = wordWidth;
613
+ }
614
+ while (lineWidth > limit) {
615
+ const [head, rest] = cutToWidth(line, limit);
616
+ out.push(head);
617
+ line = rest;
618
+ lineWidth = displayWidth(rest);
619
+ }
620
+ }
621
+ if (line !== "") out.push(line);
622
+ }
623
+ return out.length === 0 ? [""] : out;
624
+ };
625
+ var isBlank = (line) => line?.kind === "blank";
626
+ var previewLines = (transcript, options) => {
627
+ const width = Math.max(8, options.width);
628
+ const lines = [];
629
+ const push = (kind, text) => {
630
+ const wrapped = wrapText(text, width);
631
+ const budget = BLOCK_LINES[kind];
632
+ for (const [index, line] of wrapped.slice(0, budget).entries()) {
633
+ lines.push({ kind, text: line, lead: index === 0 });
634
+ }
635
+ if (wrapped.length > budget) {
636
+ lines.push({ kind: "meta", text: `\u2026 ${wrapped.length - budget} more lines`, lead: false });
637
+ }
638
+ };
639
+ const separate = () => {
640
+ if (lines.length > 0 && !isBlank(lines.at(-1))) {
641
+ lines.push({ kind: "blank", text: "", lead: false });
642
+ }
643
+ };
644
+ for (const event of transcript.events) {
645
+ const text = plainText(event.text ?? "").trim();
646
+ switch (event.kind) {
647
+ case "user":
648
+ if (text === "") break;
649
+ separate();
650
+ push("user", text);
651
+ break;
652
+ case "assistant":
653
+ if (text === "") break;
654
+ separate();
655
+ push("assistant", text);
656
+ break;
657
+ case "reasoning":
658
+ if (text === "") break;
659
+ push("reasoning", text);
660
+ break;
661
+ case "tool": {
662
+ const command = plainText(event.command ?? "").trim();
663
+ push("command", command === "" ? event.name ?? "tool" : command);
664
+ const output = plainText(event.output ?? "").trimEnd();
665
+ if (output !== "") push("output", output);
666
+ break;
667
+ }
668
+ default:
669
+ break;
670
+ }
671
+ }
672
+ while (lines.length > 0 && isBlank(lines[0])) lines.shift();
673
+ while (lines.length > 0 && isBlank(lines.at(-1))) lines.pop();
674
+ const max = Math.max(1, options.maxLines);
675
+ if (lines.length <= max) return lines;
676
+ const kept = lines.slice(lines.length - (max - 1));
677
+ return [
678
+ { kind: "meta", text: `\u2026 ${lines.length - kept.length} earlier lines`, lead: true },
679
+ ...kept
680
+ ];
681
+ };
682
+ var previewWindow = (lines, rows, offset) => {
683
+ const height = Math.max(1, rows);
684
+ const furthest = Math.max(0, lines.length - height);
685
+ const clamped = Math.max(0, Math.min(furthest, Math.trunc(offset)));
686
+ const end = lines.length - clamped;
687
+ const start = Math.max(0, end - height);
688
+ return {
689
+ lines: lines.slice(start, end),
690
+ offset: clamped,
691
+ earlier: start,
692
+ later: lines.length - end
693
+ };
694
+ };
695
+ var TRANSCRIPT_KEY = (sessionId) => [
696
+ "transcript",
697
+ sessionId
698
+ ];
699
+ var fetchTranscript = (api, sessionId) => api("GET", `/sessions/${sessionId}/transcript`);
301
700
 
302
701
  // src/review-workflow.ts
303
702
  var reviewTargetForSession = (session, annotation, projectName) => annotation?.changeId === null || annotation?.changeId === void 0 ? null : { session, changeId: annotation.changeId, projectName };
@@ -508,22 +907,25 @@ var openUrl = (target) => {
508
907
  };
509
908
 
510
909
  // src/tui-theme.ts
511
- var INK = "#eeece8";
512
- var INK_2 = "#c9c7c2";
513
- var MUTED = "#a3a09a";
514
- var FAINT = "#807d76";
515
- var RULE = "#37374a";
516
- var COBALT = "#6f96f5";
517
- var WASH = "#2c2c35";
518
- var SURFACE = "#17171d";
519
- var GREEN = "#69c189";
520
- var RED = "#e87f76";
521
- var ADD_WASH = "#20362a";
522
- var DELETE_WASH = "#3c2b2e";
523
- var SYNTAX_KEYWORD = "#cda6f2";
524
- var SYNTAX_STRING = "#98cfa8";
525
- var SYNTAX_NUMBER = "#e6c37e";
526
- var SYNTAX_FUNCTION = "#94bdf7";
910
+ var CANVAS = "#1f2430";
911
+ var PANEL = "#242936";
912
+ var SURFACE = "#282e3b";
913
+ var INK = "#e6e1cf";
914
+ var INK_2 = "#d1d4dc";
915
+ var MUTED = "#c2cad7";
916
+ var FAINT = "#b0bbcc";
917
+ var RULE = "#78859a";
918
+ var ACCENT = "#ffcc66";
919
+ var WASH = "#2d3445";
920
+ var GREEN = "#87d96c";
921
+ var RED = "#f27983";
922
+ var ERROR = "#ff6666";
923
+ var ADD_WASH = "#384c41";
924
+ var DELETE_WASH = "#4d3945";
925
+ var SYNTAX_KEYWORD = "#ffa659";
926
+ var SYNTAX_STRING = "#d5ff80";
927
+ var SYNTAX_NUMBER = "#dfbfff";
928
+ var SYNTAX_FUNCTION = "#ffcd66";
527
929
 
528
930
  // src/review.tsx
529
931
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
@@ -592,7 +994,7 @@ var anchorOf = (comment) => comment.file === null ? "change" : `${comment.file}$
592
994
  var passFact = (pass) => {
593
995
  const label = pass.kind === "suggest" ? "suggestions" : pass.kind;
594
996
  if (pass.status === "running") return { text: `${label} running`, color: INK_2 };
595
- if (pass.status === "failed") return { text: `${label} failed`, color: RED };
997
+ if (pass.status === "failed") return { text: `${label} failed`, color: ERROR };
596
998
  return {
597
999
  text: `${label} observed \xB7 ${pass.findings ?? 0} ${pass.kind === "tour" ? "stops" : "drafts"}`,
598
1000
  color: MUTED
@@ -600,7 +1002,7 @@ var passFact = (pass) => {
600
1002
  };
601
1003
  var FileRow = ({ file, selected }) => /* @__PURE__ */ jsxs("box", { height: 2, flexShrink: 0, backgroundColor: selected ? WASH : "transparent", children: [
602
1004
  /* @__PURE__ */ jsxs("text", { height: 1, bg: "transparent", children: [
603
- /* @__PURE__ */ jsx("span", { fg: selected ? COBALT : FAINT, children: selected ? "\u258C " : " " }),
1005
+ /* @__PURE__ */ jsx("span", { fg: selected ? ACCENT : FAINT, children: selected ? "\u258C " : " " }),
604
1006
  /* @__PURE__ */ jsx("span", { fg: INK, children: file.path }),
605
1007
  file.binary ? /* @__PURE__ */ jsx("span", { fg: MUTED, children: " \xB7 binary" }) : null,
606
1008
  file.likelyGenerated ? /* @__PURE__ */ jsx("span", { fg: FAINT, children: " \xB7 inferred \xB7 likely generated" }) : null
@@ -631,7 +1033,7 @@ var CommentRow = ({
631
1033
  selected
632
1034
  }) => /* @__PURE__ */ jsxs("box", { height: 2, flexShrink: 0, backgroundColor: selected ? WASH : "transparent", children: [
633
1035
  /* @__PURE__ */ jsxs("text", { height: 1, bg: "transparent", children: [
634
- /* @__PURE__ */ jsx("span", { fg: selected ? COBALT : FAINT, children: selected ? "\u258C " : " " }),
1036
+ /* @__PURE__ */ jsx("span", { fg: selected ? ACCENT : FAINT, children: selected ? "\u258C " : " " }),
635
1037
  /* @__PURE__ */ jsx("span", { fg: stateColor(comment.state), children: comment.state }),
636
1038
  /* @__PURE__ */ jsxs("span", { fg: FAINT, children: [
637
1039
  " \xB7 ",
@@ -661,6 +1063,7 @@ var Description = ({
661
1063
  border: true,
662
1064
  borderStyle: "rounded",
663
1065
  borderColor: RULE,
1066
+ titleColor: INK,
664
1067
  title: " change description ",
665
1068
  titleAlignment: "left",
666
1069
  backgroundColor: "transparent",
@@ -726,6 +1129,7 @@ var CompactTourEvidence = ({
726
1129
  border: true,
727
1130
  borderStyle: "rounded",
728
1131
  borderColor: RULE,
1132
+ titleColor: INK,
729
1133
  title: ` tour stop ${index + 1}/${total} \xB7 ${stop.grounded ? "direct record" : "inferred reading"} `,
730
1134
  titleAlignment: "left",
731
1135
  backgroundColor: "transparent",
@@ -756,6 +1160,7 @@ var EvidenceCard = ({
756
1160
  border: true,
757
1161
  borderStyle: "rounded",
758
1162
  borderColor: RULE,
1163
+ titleColor: INK,
759
1164
  title: ` ${anchorOf(comment)} \xB7 ${comment.authorKind === "mend" ? "Mend" : "You"} `,
760
1165
  titleAlignment: "left",
761
1166
  backgroundColor: "transparent",
@@ -791,7 +1196,7 @@ var EditorPanel = ({
791
1196
  {
792
1197
  border: true,
793
1198
  borderStyle: "rounded",
794
- borderColor: COBALT,
1199
+ borderColor: ACCENT,
795
1200
  title,
796
1201
  titleAlignment: "left",
797
1202
  backgroundColor: "transparent",
@@ -972,7 +1377,7 @@ function ReviewScreen({
972
1377
  (anchor) => anchor.newLine !== null && anchor.newLine >= Math.min(start, selectedLine) && anchor.newLine <= Math.max(start, selectedLine)
973
1378
  );
974
1379
  for (const anchor of highlighted) {
975
- diffRef.current.setLineColor(anchor.unifiedRow, { gutter: COBALT, content: WASH });
1380
+ diffRef.current.setLineColor(anchor.unifiedRow, { gutter: ACCENT, content: WASH });
976
1381
  }
977
1382
  return () => {
978
1383
  if (diffRef.current === null) return;
@@ -1307,7 +1712,7 @@ function ReviewScreen({
1307
1712
  if (key.name === "z") return setShowWhitespace((current) => !current);
1308
1713
  });
1309
1714
  if (data === void 0) {
1310
- return /* @__PURE__ */ jsx("box", { flexGrow: 1, backgroundColor: "transparent", alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx("text", { bg: "transparent", children: /* @__PURE__ */ jsx("span", { fg: failureReason === null ? INK_2 : MUTED, children: failureReason === null ? "Loading the live change\u2026" : `${errorMessage(failureReason)} \xB7 retrying` }) }) });
1715
+ return /* @__PURE__ */ jsx("box", { flexGrow: 1, backgroundColor: CANVAS, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx("text", { bg: "transparent", children: /* @__PURE__ */ jsx("span", { fg: failureReason === null ? INK_2 : MUTED, children: failureReason === null ? "Loading the live change\u2026" : `${errorMessage(failureReason)} \xB7 retrying` }) }) });
1311
1716
  }
1312
1717
  const additions = data.wire.files.reduce((sum, file) => sum + file.additions, 0);
1313
1718
  const deletions = data.wire.files.reduce((sum, file) => sum + file.deletions, 0);
@@ -1322,7 +1727,8 @@ function ReviewScreen({
1322
1727
  {
1323
1728
  border: true,
1324
1729
  borderStyle: "rounded",
1325
- borderColor: focus === "files" ? COBALT : RULE,
1730
+ borderColor: focus === "files" ? ACCENT : RULE,
1731
+ titleColor: focus === "files" ? ACCENT : INK,
1326
1732
  title: ` files \xB7 ${files.length} `,
1327
1733
  titleAlignment: "left",
1328
1734
  backgroundColor: "transparent",
@@ -1354,7 +1760,8 @@ function ReviewScreen({
1354
1760
  {
1355
1761
  border: true,
1356
1762
  borderStyle: "rounded",
1357
- borderColor: focus === "comments" ? COBALT : RULE,
1763
+ borderColor: focus === "comments" ? ACCENT : RULE,
1764
+ titleColor: focus === "comments" ? ACCENT : INK,
1358
1765
  title: ` comments \xB7 ${openCount} open${draftCount > 0 ? ` \xB7 ${draftCount} draft` : ""} `,
1359
1766
  titleAlignment: "left",
1360
1767
  backgroundColor: "transparent",
@@ -1379,160 +1786,166 @@ function ReviewScreen({
1379
1786
  }
1380
1787
  )
1381
1788
  ] }) : null;
1382
- return /* @__PURE__ */ jsxs("box", { flexGrow: 1, flexDirection: "column", backgroundColor: "transparent", children: [
1383
- /* @__PURE__ */ jsxs("box", { height: 2, flexShrink: 0, flexDirection: "column", backgroundColor: "transparent", children: [
1384
- /* @__PURE__ */ jsxs("text", { height: 1, bg: "transparent", children: [
1385
- /* @__PURE__ */ jsx("span", { fg: INK, children: " mend" }),
1386
- /* @__PURE__ */ jsx("span", { fg: FAINT, children: " / " }),
1387
- /* @__PURE__ */ jsx("span", { fg: MUTED, children: projectName }),
1388
- /* @__PURE__ */ jsx("span", { fg: FAINT, children: " / " }),
1389
- /* @__PURE__ */ jsx("span", { fg: INK, children: "review" }),
1390
- /* @__PURE__ */ jsxs("span", { fg: FAINT, children: [
1391
- " ",
1392
- "\xB7 ",
1393
- session.harness,
1394
- " ",
1395
- session.id.slice(0, 8)
1396
- ] }),
1397
- isFetching ? /* @__PURE__ */ jsx("span", { fg: FAINT, children: " \xB7 syncing" }) : null,
1398
- /* @__PURE__ */ jsx("span", { fg: checkpointState === "failed" ? MUTED : FAINT, children: checkpointState === "recording" ? " \xB7 checkpointing" : checkpointState === "failed" ? " \xB7 checkpoint incomplete" : " \xB7 checkpoint recorded" })
1399
- ] }),
1400
- /* @__PURE__ */ jsxs("text", { height: 1, bg: "transparent", children: [
1401
- /* @__PURE__ */ jsxs("span", { fg: FAINT, children: [
1402
- " ",
1403
- data.wire.change.branch.replace(/^mend\/session\//, "session ")
1404
- ] }),
1405
- /* @__PURE__ */ jsxs("span", { fg: FAINT, children: [
1406
- " \xB7 worktree vs ",
1407
- data.wire.change.baseSha.slice(0, 12),
1408
- " \xB7 "
1409
- ] }),
1410
- /* @__PURE__ */ jsxs("span", { fg: MUTED, children: [
1411
- files.length,
1412
- " files"
1413
- ] }),
1414
- /* @__PURE__ */ jsxs("span", { fg: GREEN, children: [
1415
- " +",
1416
- additions
1417
- ] }),
1418
- /* @__PURE__ */ jsxs("span", { fg: RED, children: [
1419
- " \u2212",
1420
- deletions
1789
+ return (
1790
+ // The review screen replaces the dashboard root rather than nesting inside
1791
+ // it, so it paints its own canvas; without this the terminal's own
1792
+ // background shows through and the screen stops matching the dashboard.
1793
+ /* @__PURE__ */ jsxs("box", { flexGrow: 1, flexDirection: "column", backgroundColor: CANVAS, children: [
1794
+ /* @__PURE__ */ jsxs("box", { height: 2, flexShrink: 0, flexDirection: "column", backgroundColor: "transparent", children: [
1795
+ /* @__PURE__ */ jsxs("text", { height: 1, bg: "transparent", children: [
1796
+ /* @__PURE__ */ jsx("span", { fg: INK, children: " mend" }),
1797
+ /* @__PURE__ */ jsx("span", { fg: FAINT, children: " / " }),
1798
+ /* @__PURE__ */ jsx("span", { fg: MUTED, children: projectName }),
1799
+ /* @__PURE__ */ jsx("span", { fg: FAINT, children: " / " }),
1800
+ /* @__PURE__ */ jsx("span", { fg: INK, children: "review" }),
1801
+ /* @__PURE__ */ jsxs("span", { fg: FAINT, children: [
1802
+ " ",
1803
+ "\xB7 ",
1804
+ session.harness,
1805
+ " ",
1806
+ session.id.slice(0, 8)
1807
+ ] }),
1808
+ isFetching ? /* @__PURE__ */ jsx("span", { fg: FAINT, children: " \xB7 syncing" }) : null,
1809
+ /* @__PURE__ */ jsx("span", { fg: checkpointState === "failed" ? MUTED : FAINT, children: checkpointState === "recording" ? " \xB7 checkpointing" : checkpointState === "failed" ? " \xB7 checkpoint incomplete" : " \xB7 checkpoint recorded" })
1421
1810
  ] }),
1422
- /* @__PURE__ */ jsxs("span", { fg: MUTED, children: [
1423
- " \xB7 ",
1424
- openCount,
1425
- " open"
1811
+ /* @__PURE__ */ jsxs("text", { height: 1, bg: "transparent", children: [
1812
+ /* @__PURE__ */ jsxs("span", { fg: FAINT, children: [
1813
+ " ",
1814
+ data.wire.change.branch.replace(/^mend\/session\//, "session ")
1815
+ ] }),
1816
+ /* @__PURE__ */ jsxs("span", { fg: FAINT, children: [
1817
+ " \xB7 worktree vs ",
1818
+ data.wire.change.baseSha.slice(0, 12),
1819
+ " \xB7 "
1820
+ ] }),
1821
+ /* @__PURE__ */ jsxs("span", { fg: MUTED, children: [
1822
+ files.length,
1823
+ " files"
1824
+ ] }),
1825
+ /* @__PURE__ */ jsxs("span", { fg: GREEN, children: [
1826
+ " +",
1827
+ additions
1828
+ ] }),
1829
+ /* @__PURE__ */ jsxs("span", { fg: RED, children: [
1830
+ " \u2212",
1831
+ deletions
1832
+ ] }),
1833
+ /* @__PURE__ */ jsxs("span", { fg: MUTED, children: [
1834
+ " \xB7 ",
1835
+ openCount,
1836
+ " open"
1837
+ ] })
1426
1838
  ] })
1427
- ] })
1428
- ] }),
1429
- /* @__PURE__ */ jsx(
1430
- Description,
1431
- {
1432
- data,
1433
- tourIndex: activeTourIndex,
1434
- stale: tourStale,
1435
- width,
1436
- height: descriptionHeight
1437
- }
1438
- ),
1439
- /* @__PURE__ */ jsxs("box", { flexGrow: 1, minHeight: 0, flexDirection: "row", backgroundColor: "transparent", children: [
1440
- sidebar,
1441
- /* @__PURE__ */ jsxs(
1442
- "box",
1839
+ ] }),
1840
+ /* @__PURE__ */ jsx(
1841
+ Description,
1443
1842
  {
1444
- flexGrow: 1,
1445
- minWidth: 0,
1446
- minHeight: 0,
1447
- flexDirection: "column",
1448
- backgroundColor: "transparent",
1449
- children: [
1450
- /* @__PURE__ */ jsx(
1451
- "box",
1452
- {
1453
- border: true,
1454
- borderStyle: "rounded",
1455
- borderColor: focus === "diff" ? COBALT : RULE,
1456
- title: selectedFile === null ? " diff " : ` ${selectedFile.path} \xB7 ${view}${selectedFile.likelyGenerated ? " \xB7 inferred likely generated" : ""}${showWhitespace ? " \xB7 whitespace" : ""}${wrap ? " \xB7 wrapped" : ""} `,
1457
- titleAlignment: "left",
1458
- backgroundColor: "transparent",
1459
- flexGrow: 1,
1460
- minHeight: 0,
1461
- children: selectedFile === null || selectedFile.patch === "" || selectedFile.binary ? /* @__PURE__ */ jsx("box", { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx("text", { fg: FAINT, bg: "transparent", children: files.length === 0 ? "The worktree matches its base \u2014 nothing to review yet." : selectedFile?.binary === true ? "Binary change \u2014 text diff unavailable; statistics remain reviewable." : "This file has no renderable text patch." }) }) : /* @__PURE__ */ jsx(
1462
- "scrollbox",
1463
- {
1464
- ref: diffScrollRef,
1465
- focused: focus === "diff",
1466
- flexGrow: 1,
1467
- minHeight: 0,
1468
- scrollX: true,
1469
- style: {
1470
- rootOptions: { backgroundColor: "transparent", border: false },
1471
- wrapperOptions: { backgroundColor: "transparent" },
1472
- viewportOptions: { backgroundColor: "transparent" },
1473
- contentOptions: { backgroundColor: "transparent" }
1474
- },
1475
- children: /* @__PURE__ */ jsx(
1476
- "diff",
1477
- {
1478
- ref: diffRef,
1479
- diff: displayPatch,
1480
- view,
1481
- syncScroll: true,
1482
- ...selectedFile.filetype === void 0 ? {} : { filetype: selectedFile.filetype },
1483
- syntaxStyle,
1484
- wrapMode: wrap ? "word" : "none",
1485
- showLineNumbers: true,
1486
- fg: INK,
1487
- lineNumberFg: FAINT,
1488
- lineNumberBg: "transparent",
1489
- contextBg: "transparent",
1490
- contextContentBg: "transparent",
1491
- addedBg: ADD_WASH,
1492
- addedContentBg: ADD_WASH,
1493
- removedBg: DELETE_WASH,
1494
- removedContentBg: DELETE_WASH,
1495
- addedSignColor: GREEN,
1496
- removedSignColor: RED,
1497
- addedLineNumberBg: "transparent",
1498
- removedLineNumberBg: "transparent",
1499
- selectionBg: WASH,
1500
- selectionFg: INK,
1501
- width: "100%",
1502
- height: displayRows,
1503
- flexShrink: 0
1843
+ data,
1844
+ tourIndex: activeTourIndex,
1845
+ stale: tourStale,
1846
+ width,
1847
+ height: descriptionHeight
1848
+ }
1849
+ ),
1850
+ /* @__PURE__ */ jsxs("box", { flexGrow: 1, minHeight: 0, flexDirection: "row", backgroundColor: "transparent", children: [
1851
+ sidebar,
1852
+ /* @__PURE__ */ jsxs(
1853
+ "box",
1854
+ {
1855
+ flexGrow: 1,
1856
+ minWidth: 0,
1857
+ minHeight: 0,
1858
+ flexDirection: "column",
1859
+ backgroundColor: "transparent",
1860
+ children: [
1861
+ /* @__PURE__ */ jsx(
1862
+ "box",
1863
+ {
1864
+ border: true,
1865
+ borderStyle: "rounded",
1866
+ borderColor: focus === "diff" ? ACCENT : RULE,
1867
+ titleColor: focus === "diff" ? ACCENT : INK,
1868
+ title: selectedFile === null ? " diff " : ` ${selectedFile.path} \xB7 ${view}${selectedFile.likelyGenerated ? " \xB7 inferred likely generated" : ""}${showWhitespace ? " \xB7 whitespace" : ""}${wrap ? " \xB7 wrapped" : ""} `,
1869
+ titleAlignment: "left",
1870
+ backgroundColor: "transparent",
1871
+ flexGrow: 1,
1872
+ minHeight: 0,
1873
+ children: selectedFile === null || selectedFile.patch === "" || selectedFile.binary ? /* @__PURE__ */ jsx("box", { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx("text", { fg: FAINT, bg: "transparent", children: files.length === 0 ? "The worktree matches its base \u2014 nothing to review yet." : selectedFile?.binary === true ? "Binary change \u2014 text diff unavailable; statistics remain reviewable." : "This file has no renderable text patch." }) }) : /* @__PURE__ */ jsx(
1874
+ "scrollbox",
1875
+ {
1876
+ ref: diffScrollRef,
1877
+ focused: focus === "diff",
1878
+ flexGrow: 1,
1879
+ minHeight: 0,
1880
+ scrollX: true,
1881
+ style: {
1882
+ rootOptions: { backgroundColor: "transparent", border: false },
1883
+ wrapperOptions: { backgroundColor: "transparent" },
1884
+ viewportOptions: { backgroundColor: "transparent" },
1885
+ contentOptions: { backgroundColor: "transparent" }
1504
1886
  },
1505
- `${selectedFile.path}-${view}-${wrap}-${showWhitespace}`
1506
- )
1507
- }
1508
- )
1509
- }
1510
- ),
1511
- focus === "comments" && detailComment !== null ? /* @__PURE__ */ jsx(EvidenceCard, { comment: detailComment, width: wide ? width - 38 : width }) : height < 30 && activeTourStop !== null ? /* @__PURE__ */ jsx(
1512
- CompactTourEvidence,
1513
- {
1514
- stop: activeTourStop,
1515
- index: activeTourIndex,
1516
- total: tourStops.length,
1517
- width
1518
- }
1519
- ) : detailComment === null ? null : /* @__PURE__ */ jsx(EvidenceCard, { comment: detailComment, width: wide ? width - 38 : width })
1520
- ]
1887
+ children: /* @__PURE__ */ jsx(
1888
+ "diff",
1889
+ {
1890
+ ref: diffRef,
1891
+ diff: displayPatch,
1892
+ view,
1893
+ syncScroll: true,
1894
+ ...selectedFile.filetype === void 0 ? {} : { filetype: selectedFile.filetype },
1895
+ syntaxStyle,
1896
+ wrapMode: wrap ? "word" : "none",
1897
+ showLineNumbers: true,
1898
+ fg: INK,
1899
+ lineNumberFg: FAINT,
1900
+ lineNumberBg: "transparent",
1901
+ contextBg: "transparent",
1902
+ contextContentBg: "transparent",
1903
+ addedBg: ADD_WASH,
1904
+ addedContentBg: ADD_WASH,
1905
+ removedBg: DELETE_WASH,
1906
+ removedContentBg: DELETE_WASH,
1907
+ addedSignColor: GREEN,
1908
+ removedSignColor: RED,
1909
+ addedLineNumberBg: "transparent",
1910
+ removedLineNumberBg: "transparent",
1911
+ selectionBg: WASH,
1912
+ selectionFg: INK,
1913
+ width: "100%",
1914
+ height: displayRows,
1915
+ flexShrink: 0
1916
+ },
1917
+ `${selectedFile.path}-${view}-${wrap}-${showWhitespace}`
1918
+ )
1919
+ }
1920
+ )
1921
+ }
1922
+ ),
1923
+ focus === "comments" && detailComment !== null ? /* @__PURE__ */ jsx(EvidenceCard, { comment: detailComment, width: wide ? width - 38 : width }) : height < 30 && activeTourStop !== null ? /* @__PURE__ */ jsx(
1924
+ CompactTourEvidence,
1925
+ {
1926
+ stop: activeTourStop,
1927
+ index: activeTourIndex,
1928
+ total: tourStops.length,
1929
+ width
1930
+ }
1931
+ ) : detailComment === null ? null : /* @__PURE__ */ jsx(EvidenceCard, { comment: detailComment, width: wide ? width - 38 : width })
1932
+ ]
1933
+ }
1934
+ )
1935
+ ] }),
1936
+ editor === null ? null : /* @__PURE__ */ jsx(
1937
+ EditorPanel,
1938
+ {
1939
+ editor,
1940
+ refValue: editorRef,
1941
+ busy,
1942
+ onSubmit: () => void submitEditor()
1521
1943
  }
1522
- )
1523
- ] }),
1524
- editor === null ? null : /* @__PURE__ */ jsx(
1525
- EditorPanel,
1526
- {
1527
- editor,
1528
- refValue: editorRef,
1529
- busy,
1530
- onSubmit: () => void submitEditor()
1531
- }
1532
- ),
1533
- /* @__PURE__ */ jsx("text", { height: 1, fg: status === "" ? FAINT : INK_2, bg: "transparent", children: status === "" ? ` m read \xB7 g suggest \xB7 t tour \xB7 ,/. tour stops \xB7 s draft review${data.followUp?.status === "pending" ? " \xB7 y deliver & relaunch" : ""} \xB7 o web \xB7 r refresh` : ` ${status}` }),
1534
- /* @__PURE__ */ jsx("text", { height: 1, fg: FAINT, bg: "transparent", children: footer })
1535
- ] });
1944
+ ),
1945
+ /* @__PURE__ */ jsx("text", { height: 1, fg: status === "" ? FAINT : INK_2, bg: "transparent", children: status === "" ? ` m read \xB7 g suggest \xB7 t tour \xB7 ,/. tour stops \xB7 s draft review${data.followUp?.status === "pending" ? " \xB7 y deliver & relaunch" : ""} \xB7 o web \xB7 r refresh` : ` ${status}` }),
1946
+ /* @__PURE__ */ jsx("text", { height: 1, fg: FAINT, bg: "transparent", children: footer })
1947
+ ] })
1948
+ );
1536
1949
  }
1537
1950
 
1538
1951
  // src/workbench-events.ts
@@ -1567,10 +1980,13 @@ var eventFamilies = (payload) => {
1567
1980
  case "worktree":
1568
1981
  case "session-process":
1569
1982
  return ["workbench"];
1570
- // Session facts feed both surfaces: rows and annotations on the
1571
- // dashboard; status and the follow-up banner inside a review.
1983
+ // Settlement refreshes the final transcript immediately. Conversation
1984
+ // items can arrive many times per second; the live preview's bounded
1985
+ // polling reads those instead of repeatedly fetching the whole record.
1572
1986
  case "session":
1987
+ return ["workbench", "review", "transcript"];
1573
1988
  case "agent-conversation":
1989
+ return ["workbench", "review"];
1574
1990
  // A comment moves the review AND the dashboard's open-comment counts.
1575
1991
  case "session-change":
1576
1992
  case "review-comment":
@@ -1588,9 +2004,10 @@ var STATUS_COLOR = {
1588
2004
  waiting: INK_2,
1589
2005
  idle: INK_2,
1590
2006
  completed: FAINT,
1591
- failed: RED,
2007
+ failed: ERROR,
1592
2008
  stopped: FAINT
1593
2009
  };
2010
+ var AGENT_LIVE_STATUSES = /* @__PURE__ */ new Set(["starting", "running", "waiting"]);
1594
2011
  var timeAgo = (iso) => {
1595
2012
  const ms = Date.now() - new Date(iso).getTime();
1596
2013
  if (!Number.isFinite(ms) || ms < 0) return "";
@@ -1601,8 +2018,17 @@ var timeAgo = (iso) => {
1601
2018
  if (hours < 24) return `${hours}h ago`;
1602
2019
  return `${Math.floor(hours / 24)}d ago`;
1603
2020
  };
1604
- var AGENT_LIVE_STATUSES = /* @__PURE__ */ new Set(["starting", "running", "waiting"]);
1605
- var Gutter = ({ selected }) => /* @__PURE__ */ jsx2("span", { fg: selected ? COBALT : FAINT, children: selected ? "\u258C " : " " });
2021
+ var shortAge = (iso) => timeAgo(iso).replace(" ago", "");
2022
+ var fit = (text, width) => text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
2023
+ var COLUMN_TITLE = {
2024
+ projects: "projects",
2025
+ worktrees: "worktrees",
2026
+ sessions: "sessions",
2027
+ detail: "session"
2028
+ };
2029
+ var WORKTREE_ROW_HEIGHT = 2;
2030
+ var SESSION_ROW_HEIGHT = 2;
2031
+ var Gutter = ({ selected }) => /* @__PURE__ */ jsx2("span", { fg: selected ? ACCENT : FAINT, children: selected ? "\u258C " : " " });
1606
2032
  var Pane = ({
1607
2033
  title,
1608
2034
  focused,
@@ -1615,10 +2041,12 @@ var Pane = ({
1615
2041
  {
1616
2042
  border: true,
1617
2043
  borderStyle: "rounded",
1618
- borderColor: focused ? COBALT : RULE,
2044
+ borderColor: focused ? ACCENT : RULE,
2045
+ titleColor: focused ? ACCENT : INK,
1619
2046
  title: ` ${title} `,
1620
2047
  titleAlignment: "left",
1621
- backgroundColor: "transparent",
2048
+ backgroundColor: PANEL,
2049
+ overflow: "hidden",
1622
2050
  ...width === void 0 ? {} : { width, flexShrink: 0, minHeight: 0 },
1623
2051
  ...height === void 0 ? {} : { height, flexShrink: 0 },
1624
2052
  ...grow === true ? { flexGrow: 1, flexShrink: 1, minHeight: 0, minWidth: 0 } : {},
@@ -1626,135 +2054,182 @@ var Pane = ({
1626
2054
  children
1627
2055
  }
1628
2056
  );
2057
+ var SummaryRow = ({
2058
+ name,
2059
+ fact,
2060
+ width,
2061
+ empty
2062
+ }) => {
2063
+ const factWidth = fact === "" ? 0 : Math.min(fact.length, Math.max(0, width - 8));
2064
+ const nameWidth = Math.max(4, width - factWidth - 3);
2065
+ return /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
2066
+ /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " " }),
2067
+ /* @__PURE__ */ jsx2("span", { fg: empty ? FAINT : INK_2, children: fit(name, nameWidth - 1).padEnd(nameWidth) }),
2068
+ factWidth === 0 ? null : /* @__PURE__ */ jsx2("span", { fg: FAINT, children: fit(fact, factWidth) })
2069
+ ] });
2070
+ };
1629
2071
  var paneScrollStyle = {
1630
2072
  rootOptions: { backgroundColor: "transparent", border: false },
1631
2073
  wrapperOptions: { backgroundColor: "transparent" },
1632
2074
  viewportOptions: { backgroundColor: "transparent" },
1633
2075
  contentOptions: { backgroundColor: "transparent" }
1634
2076
  };
2077
+ var EmptyNote = ({ text }) => /* @__PURE__ */ jsx2("text", { height: 1, fg: FAINT, bg: "transparent", children: ` ${text}` });
1635
2078
  var ProjectRow = ({
1636
2079
  item,
1637
2080
  selected,
1638
- nameWidth
1639
- }) => /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: selected ? WASH : "transparent", children: /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
1640
- /* @__PURE__ */ jsx2(Gutter, { selected }),
1641
- /* @__PURE__ */ jsx2("span", { fg: INK, children: item.project.name.padEnd(nameWidth) }),
1642
- /* @__PURE__ */ jsx2("span", { fg: item.total === 0 ? FAINT : MUTED, children: ` ${item.total}` }),
1643
- item.live > 0 ? /* @__PURE__ */ jsx2("span", { fg: MUTED, children: ` \xB7 ${item.live} live` }) : null,
1644
- item.open > 0 ? /* @__PURE__ */ jsx2("span", { fg: MUTED, children: ` \xB7 ${item.open}` }) : null
1645
- ] }) });
1646
- var ProcessLines = ({
1647
- processes,
1648
- services,
1649
- indent
1650
- }) => /* @__PURE__ */ jsxs2(Fragment2, { children: [
1651
- processes.map((process2) => /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: "transparent", children: /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
1652
- /* @__PURE__ */ jsx2("span", { fg: FAINT, children: indent }),
1653
- /* @__PURE__ */ jsx2("span", { fg: INK_2, children: process2.kind === "shell" ? process2.label ?? "shell" : process2.harness ?? process2.kind }),
1654
- /* @__PURE__ */ jsxs2("span", { fg: FAINT, children: [
1655
- process2.kind === "agent-protocol" ? " \xB7 protocol" : process2.kind === "agent-external" ? " \xB7 external" : "",
1656
- ` ${process2.status}`
1657
- ] })
1658
- ] }) }, process2.id)),
1659
- services.map((service) => /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: "transparent", children: /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
1660
- /* @__PURE__ */ jsx2("span", { fg: FAINT, children: indent }),
1661
- /* @__PURE__ */ jsx2("span", { fg: service.status === "reachable" ? INK_2 : MUTED, children: `${service.label ?? service.id.slice(0, 6)} :${service.workspacePort ?? "?"}${service.protocol === "udp" ? "u" : ""}\u2192${service.hostPort ?? "?"}` }),
1662
- /* @__PURE__ */ jsx2("span", { fg: FAINT, children: ` ${service.status}` })
1663
- ] }) }, service.id))
1664
- ] });
1665
- var WorktreeHeaderRow = ({
1666
- group,
1667
- selected
2081
+ width
1668
2082
  }) => {
1669
- const folded = foldGroupStatus(group);
1670
- const color = group.live > 0 ? STATUS_COLOR[folded] ?? MUTED : FAINT;
1671
- const age = timeAgo(group.sessions.at(-1)?.session.createdAt ?? group.createdAt).replace(
1672
- " ago",
1673
- ""
1674
- );
1675
- const open = group.annotation?.openComments ?? 0;
2083
+ const nameWidth = Math.max(6, width - 9);
1676
2084
  return /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: selected ? WASH : "transparent", children: /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
1677
2085
  /* @__PURE__ */ jsx2(Gutter, { selected }),
1678
- /* @__PURE__ */ jsx2("span", { fg: INK, children: group.name.slice(0, 30).padEnd(31) }),
1679
- /* @__PURE__ */ jsx2("span", { fg: color, children: (group.live > 0 ? folded : "settled").padEnd(10) }),
1680
- /* @__PURE__ */ jsx2("span", { fg: FAINT, children: age.padEnd(9) }),
1681
- /* @__PURE__ */ jsx2("span", { fg: MUTED, children: `${group.sessions.length} session${group.sessions.length === 1 ? "" : "s"}` }),
1682
- open > 0 ? /* @__PURE__ */ jsx2("span", { fg: MUTED, children: ` \xB7 ${open} open` }) : null
2086
+ /* @__PURE__ */ jsx2("span", { fg: INK, children: fit(item.project.name, nameWidth).padEnd(nameWidth + 1) }),
2087
+ /* @__PURE__ */ jsx2("span", { fg: item.live > 0 ? MUTED : FAINT, children: item.live > 0 ? `${item.live}/${item.total}` : String(item.total) })
1683
2088
  ] }) });
1684
2089
  };
1685
- var SessionChildRow = ({
2090
+ var WorktreeRow = ({
2091
+ group,
2092
+ selected,
2093
+ width
2094
+ }) => {
2095
+ const folded = foldGroupStatus(group);
2096
+ const live = group.live > 0;
2097
+ const status = live ? folded : "settled";
2098
+ const color = live ? STATUS_COLOR[folded] ?? MUTED : FAINT;
2099
+ const open = group.annotation?.openComments ?? 0;
2100
+ const sessions = group.sessions.length;
2101
+ const nameWidth = Math.max(6, width - 5 - status.length);
2102
+ const factWidth = Math.max(8, width - 3);
2103
+ const facts = fitHints(
2104
+ [
2105
+ fit(groupBaseLabel(group), factWidth),
2106
+ shortAge(groupActivityAt(group)),
2107
+ ...open > 0 ? [`${open} open`] : [],
2108
+ ...sessions === 0 ? ["empty"] : sessions === 1 ? [] : [`${sessions} sessions`]
2109
+ ],
2110
+ factWidth
2111
+ );
2112
+ return /* @__PURE__ */ jsxs2("box", { flexShrink: 0, flexDirection: "column", backgroundColor: selected ? WASH : "transparent", children: [
2113
+ /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: "transparent", children: /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
2114
+ /* @__PURE__ */ jsx2(Gutter, { selected }),
2115
+ /* @__PURE__ */ jsx2("span", { fg: INK, children: fit(group.name, nameWidth - 1).padEnd(nameWidth) }),
2116
+ /* @__PURE__ */ jsx2("span", { fg: color, children: status })
2117
+ ] }) }),
2118
+ /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: "transparent", children: /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", fg: FAINT, children: ` ${facts}` }) })
2119
+ ] });
2120
+ };
2121
+ var SessionRow = ({
1686
2122
  item,
1687
- selected
2123
+ selected,
2124
+ width
1688
2125
  }) => {
1689
- const { session, services, processes } = item;
1690
- const color = STATUS_COLOR[session.status] ?? MUTED;
1691
- const age = timeAgo(session.createdAt).replace(" ago", "");
1692
- const name = session.label ?? `session ${session.id.slice(0, 8)}`;
1693
- return /* @__PURE__ */ jsxs2("box", { flexShrink: 0, flexDirection: "column", backgroundColor: "transparent", children: [
1694
- /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: selected ? WASH : "transparent", children: /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
2126
+ const { session, processes, services } = item;
2127
+ const status = session.status;
2128
+ const color = STATUS_COLOR[status] ?? MUTED;
2129
+ const agents = processes.filter((process2) => process2.kind !== "shell").length;
2130
+ const shells = processes.filter((process2) => process2.kind === "shell").length;
2131
+ const nameWidth = Math.max(6, width - 5 - status.length);
2132
+ const factWidth = Math.max(8, width - 3);
2133
+ const facts = fitHints(
2134
+ [
2135
+ session.harness,
2136
+ shortAge(session.createdAt),
2137
+ ...shells > 0 ? [`${shells} shell`] : [],
2138
+ ...agents > 0 ? [`${agents} agent`] : [],
2139
+ ...services.length > 0 ? [`${services.length} service`] : []
2140
+ ],
2141
+ factWidth
2142
+ );
2143
+ return /* @__PURE__ */ jsxs2("box", { flexShrink: 0, flexDirection: "column", backgroundColor: selected ? WASH : "transparent", children: [
2144
+ /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: "transparent", children: /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
1695
2145
  /* @__PURE__ */ jsx2(Gutter, { selected }),
1696
- /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \u2514 " }),
1697
- /* @__PURE__ */ jsx2("span", { fg: INK, children: name.slice(0, 26).padEnd(27) }),
1698
- /* @__PURE__ */ jsx2("span", { fg: color, children: session.status.padEnd(10) }),
1699
- /* @__PURE__ */ jsx2("span", { fg: FAINT, children: age.padEnd(9) }),
1700
- /* @__PURE__ */ jsx2("span", { fg: MUTED, children: session.harness.padEnd(10) })
2146
+ /* @__PURE__ */ jsx2("span", { fg: INK, children: fit(sessionDisplayName(session), nameWidth - 1).padEnd(nameWidth) }),
2147
+ /* @__PURE__ */ jsx2("span", { fg: color, children: status })
1701
2148
  ] }) }),
1702
- /* @__PURE__ */ jsx2(ProcessLines, { processes, services, indent: " \u2514 " })
2149
+ /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: "transparent", children: /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", fg: FAINT, children: ` ${facts}` }) })
1703
2150
  ] });
1704
2151
  };
1705
2152
  var HarnessRow = ({
1706
2153
  item,
1707
- selected
1708
- }) => /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: selected ? WASH : "transparent", children: /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
2154
+ selected,
2155
+ background
2156
+ }) => /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: selected ? WASH : background, children: /* @__PURE__ */ jsxs2("text", { height: 1, bg: selected ? WASH : background, children: [
1709
2157
  /* @__PURE__ */ jsx2(Gutter, { selected }),
1710
2158
  /* @__PURE__ */ jsx2("span", { fg: INK, children: item.label.padEnd(12) }),
1711
2159
  /* @__PURE__ */ jsx2("span", { fg: FAINT, children: item.hint })
1712
2160
  ] }) });
1713
- var SessionDetail = ({ item }) => {
1714
- if (item === null) {
1715
- return /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", fg: FAINT, children: " no session selected \u2014 n starts one" });
1716
- }
2161
+ var PREVIEW_GLYPH = {
2162
+ user: "\u203A ",
2163
+ assistant: " ",
2164
+ reasoning: "\xB7 ",
2165
+ command: "$ ",
2166
+ output: " ",
2167
+ meta: " ",
2168
+ blank: " "
2169
+ };
2170
+ var PREVIEW_COLOR = {
2171
+ user: INK,
2172
+ assistant: INK_2,
2173
+ reasoning: FAINT,
2174
+ command: MUTED,
2175
+ output: FAINT,
2176
+ meta: FAINT,
2177
+ blank: FAINT
2178
+ };
2179
+ var PreviewRow = ({ line }) => /* @__PURE__ */ jsx2("box", { height: 1, flexShrink: 0, backgroundColor: "transparent", children: /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
2180
+ /* @__PURE__ */ jsx2("span", { fg: FAINT, children: line.lead ? PREVIEW_GLYPH[line.kind] : " " }),
2181
+ /* @__PURE__ */ jsx2("span", { fg: PREVIEW_COLOR[line.kind], children: line.text })
2182
+ ] }) });
2183
+ var SessionFacts = ({
2184
+ group,
2185
+ item,
2186
+ rows
2187
+ }) => {
1717
2188
  const { session, annotation, services } = item;
1718
2189
  const color = STATUS_COLOR[session.status] ?? MUTED;
1719
2190
  const summary = session.summary?.split("\n")[0] ?? null;
1720
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
2191
+ const change = annotation ?? group?.annotation;
2192
+ const lines = [
2193
+ /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
2194
+ /* @__PURE__ */ jsx2("span", { children: " " }),
2195
+ /* @__PURE__ */ jsx2("span", { fg: INK, children: sessionDisplayName(session) }),
2196
+ /* @__PURE__ */ jsx2("span", { fg: FAINT, children: ` \xB7 ${session.harness}` })
2197
+ ] }, "name"),
1721
2198
  /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
1722
2199
  /* @__PURE__ */ jsx2("span", { children: " " }),
1723
2200
  /* @__PURE__ */ jsx2("span", { fg: color, children: session.status }),
1724
2201
  /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \xB7 " }),
1725
2202
  /* @__PURE__ */ jsx2("span", { fg: MUTED, children: session.branch }),
1726
- /* @__PURE__ */ jsxs2("span", { fg: FAINT, children: [
1727
- session.baseSha === "" ? "" : ` vs ${session.baseRef ?? session.baseSha.slice(0, 12)}`,
1728
- " \xB7 started ",
1729
- timeAgo(session.createdAt)
1730
- ] })
1731
- ] }),
2203
+ /* @__PURE__ */ jsx2("span", { fg: FAINT, children: session.baseSha === "" ? "" : ` vs ${session.baseRef ?? session.baseSha.slice(0, 12)}` })
2204
+ ] }, "status"),
2205
+ /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", fg: FAINT, children: ` started ${timeAgo(session.createdAt)} \xB7 ${isPendingId(session.id) ? "provisioning" : session.id.slice(0, 8)}` }, "started"),
1732
2206
  /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
1733
2207
  /* @__PURE__ */ jsx2("span", { children: " " }),
1734
- services.length === 0 ? /* @__PURE__ */ jsx2("span", { fg: FAINT, children: "no services running" }) : services.slice(0, 3).map((service, index) => /* @__PURE__ */ jsxs2("span", { children: [
2208
+ services.length === 0 ? /* @__PURE__ */ jsx2("span", { fg: FAINT, children: "no services running" }) : services.slice(0, 2).map((service, index) => /* @__PURE__ */ jsxs2("span", { children: [
1735
2209
  index > 0 ? /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \xB7 " }) : null,
1736
2210
  /* @__PURE__ */ jsx2("span", { fg: service.status === "reachable" ? INK_2 : MUTED, children: `${service.label ?? service.id.slice(0, 6)} :${service.workspacePort ?? "?"}${service.protocol === "udp" ? "u" : ""}\u2192${service.hostPort ?? "?"} ${service.status}` })
1737
2211
  ] }, service.id)),
1738
- services.length > 3 ? /* @__PURE__ */ jsx2("span", { fg: FAINT, children: ` \xB7 +${services.length - 3} more` }) : null
1739
- ] }),
2212
+ services.length > 2 ? /* @__PURE__ */ jsx2("span", { fg: FAINT, children: ` \xB7 +${services.length - 2} more` }) : null
2213
+ ] }, "services"),
1740
2214
  /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
1741
2215
  /* @__PURE__ */ jsx2("span", { children: " " }),
1742
- annotation === void 0 || annotation.openComments === 0 ? /* @__PURE__ */ jsx2("span", { fg: FAINT, children: "no open review comments" }) : /* @__PURE__ */ jsxs2("span", { fg: INK_2, children: [
1743
- annotation.openComments,
2216
+ change === void 0 || change.openComments === 0 ? /* @__PURE__ */ jsx2("span", { fg: FAINT, children: "no open review comments" }) : /* @__PURE__ */ jsxs2("span", { fg: INK_2, children: [
2217
+ change.openComments,
1744
2218
  " open comment",
1745
- annotation.openComments === 1 ? "" : "s"
2219
+ change.openComments === 1 ? "" : "s"
1746
2220
  ] }),
1747
- annotation?.pendingFollowUp === true ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
2221
+ change?.pendingFollowUp === true ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1748
2222
  /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \xB7 " }),
1749
2223
  /* @__PURE__ */ jsx2("span", { fg: INK_2, children: "follow-up pending" })
1750
2224
  ] }) : null,
1751
- annotation?.changeId != null ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
2225
+ change?.changeId == null ? null : /* @__PURE__ */ jsxs2(Fragment2, { children: [
1752
2226
  /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \xB7 " }),
1753
2227
  /* @__PURE__ */ jsx2("span", { fg: MUTED, children: "v reviews the change" })
1754
- ] }) : null
1755
- ] }),
1756
- /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", fg: summary === null ? FAINT : MUTED, children: ` ${summary ?? "no summary yet"}` })
1757
- ] });
2228
+ ] })
2229
+ ] }, "comments"),
2230
+ /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", fg: summary === null ? FAINT : MUTED, children: ` ${fit(summary ?? "no summary yet", 200)}` }, "summary")
2231
+ ];
2232
+ return /* @__PURE__ */ jsx2("box", { flexShrink: 0, flexDirection: "column", backgroundColor: "transparent", children: lines.slice(0, Math.max(0, rows)) });
1758
2233
  };
1759
2234
  var StatusLine = ({
1760
2235
  busy,
@@ -1773,18 +2248,18 @@ var StatusLine = ({
1773
2248
  }
1774
2249
  return void 0;
1775
2250
  }, [busy, status]);
1776
- const text = busy !== null ? ` ${busy} ${Math.max(0, Math.round((now - busyStarted) / 1e3))}s` : status !== null && Date.now() - status.at < 5e3 ? ` ${status.text}` : "";
2251
+ const text = busy === null ? status !== null && Date.now() - status.at < 5e3 ? ` ${status.text}` : "" : ` ${busy} ${Math.max(0, Math.round((now - busyStarted) / 1e3))}s`;
1777
2252
  return /* @__PURE__ */ jsx2("text", { height: 1, fg: INK_2, bg: "transparent", children: text });
1778
2253
  };
1779
2254
  var errorText = (error) => error instanceof Error ? error.message : String(error);
1780
- var keepSpanVisible = (scroll, top, height) => {
1781
- if (scroll === null) return;
1782
- const viewH = Math.max(1, scroll.viewport.height);
1783
- if (top < scroll.scrollTop) scroll.scrollTo(top);
2255
+ var keepSpanVisible = (scroll, top, height, viewRows) => {
2256
+ if (scroll === null || viewRows <= 0) return;
2257
+ const viewH = Math.max(1, viewRows);
2258
+ if (top < scroll.scrollTop || height > viewH) scroll.scrollTo(top);
1784
2259
  else if (top + height > scroll.scrollTop + viewH) scroll.scrollTo(top + height - viewH);
1785
2260
  };
1786
- var keepRowVisible = (scroll, index) => keepSpanVisible(scroll, index, 1);
1787
- var PROJECTS_PANE_WIDTH = 30;
2261
+ var PREVIEW_MAX_LINES = 300;
2262
+ var SESSION_FACT_ROWS = 6;
1788
2263
  var noShare = () => "off";
1789
2264
  var App = ({ ctx, onQuit }) => {
1790
2265
  const renderer = useRenderer();
@@ -1796,8 +2271,10 @@ var App = ({ ctx, onQuit }) => {
1796
2271
  retry: true,
1797
2272
  retryDelay: 1e3
1798
2273
  });
1799
- const [focusState, setFocus] = useState2("sessions");
2274
+ const [focus, setFocusState] = useState2("sessions");
2275
+ const [lastNav, setLastNav] = useState2("sessions");
1800
2276
  const [projectKey, setProjectKey] = useState2(null);
2277
+ const [worktreeKey, setWorktreeKey] = useState2(null);
1801
2278
  const [sessionKey, setSessionKey] = useState2(null);
1802
2279
  const [picker, setPicker] = useState2(null);
1803
2280
  const [pickerIndex, setPickerIndex] = useState2(0);
@@ -1811,34 +2288,73 @@ var App = ({ ctx, onQuit }) => {
1811
2288
  );
1812
2289
  const [editing, setEditing] = useState2(null);
1813
2290
  const [stopArmed, setStopArmed] = useState2(null);
2291
+ const [removeArmed, setRemoveArmed] = useState2(null);
1814
2292
  const [creating, setCreating] = useState2(null);
1815
2293
  const [adoptOffer, setAdoptOffer] = useState2(null);
1816
2294
  const adoptOfferDecided = useRef2(false);
1817
2295
  const [reviewing, setReviewing] = useState2(null);
1818
2296
  const lockRef = useRef2(false);
1819
- const modalRef = useRef2(false);
1820
- modalRef.current = editing !== null || reviewing !== null || picker !== null || creating !== null;
2297
+ const [gate] = useState2(createLaunchGate);
2298
+ const creatingRef = useRef2(creating);
2299
+ creatingRef.current = creating;
2300
+ const branchRequest = useRef2(0);
1821
2301
  const { width: terminalCols, height: terminalRows } = useTerminalDimensions2();
1822
- const showProjectsPane = terminalCols >= 72;
1823
- const showDetail = terminalRows >= 18;
1824
- const focus = showProjectsPane ? focusState : "sessions";
1825
2302
  const projectItems = deriveProjects(data);
1826
2303
  const homeProject = data === void 0 ? void 0 : matchProjectByCwd(data.projects, cwdFacts(ctx.cwd));
1827
2304
  const projectIndexRaw = projectKey === null ? -1 : projectItems.findIndex((p) => p.project.id === projectKey);
1828
- const projectIndex = projectIndexRaw !== -1 ? projectIndexRaw : Math.max(
2305
+ const projectIndex = projectIndexRaw === -1 ? Math.max(
1829
2306
  0,
1830
2307
  homeProject === void 0 ? 0 : projectItems.findIndex((p) => p.project.id === homeProject.id)
1831
- );
2308
+ ) : projectIndexRaw;
1832
2309
  const selectedProject = projectItems[projectIndex] ?? null;
1833
2310
  const worktreeGroups = deriveWorktrees(data, selectedProject?.project.id ?? null);
1834
- const rows = deriveRows(worktreeGroups);
1835
- const rowIndexRaw = sessionKey === null ? -1 : rows.findIndex((row) => rowKeyOf(row) === sessionKey);
1836
- const rowIndex = rowIndexRaw === -1 ? 0 : rowIndexRaw;
1837
- const selectedRow = rows[rowIndex] ?? null;
1838
- const selectedGroup = selectedRow?.group ?? null;
1839
- const selectedSession = selectedRow === null ? null : selectedRow.kind === "session" ? selectedRow.item : selectedRow.group.sessions.find((item) => LIVE_STATUSES.has(item.session.status)) ?? selectedRow.group.sessions[0] ?? null;
2311
+ const worktreeIndexRaw = worktreeKey === null ? -1 : worktreeGroups.findIndex((group) => group.key === worktreeKey);
2312
+ const sessionWorktreeIndex = worktreeGroups.findIndex(
2313
+ (group) => group.sessions.some((item) => item.session.id === sessionKey)
2314
+ );
2315
+ const worktreeIndex = worktreeIndexRaw === -1 ? Math.max(0, sessionWorktreeIndex) : worktreeIndexRaw;
2316
+ const selectedGroup = worktreeGroups[worktreeIndex] ?? null;
2317
+ const sessionItems = selectedGroup?.sessions ?? [];
2318
+ const sessionIndexRaw = sessionKey === null ? -1 : sessionItems.findIndex((item) => item.session.id === sessionKey);
2319
+ const sessionIndex = sessionIndexRaw === -1 ? 0 : sessionIndexRaw;
2320
+ const selectedItem = sessionItems[sessionIndex] ?? null;
2321
+ const selectedSession = selectedItem?.session ?? null;
1840
2322
  const pickerItems = picker === null ? [] : deriveHarnesses(picker.session);
1841
- const selectSession = (id) => setSessionKey(`s:${id}`);
2323
+ const layout = planLayout(terminalCols, terminalRows, focus, lastNav);
2324
+ const detailWidth = layout.detailWidth;
2325
+ const sectionRows = (section) => layout.sections.find((entry) => entry.section === section)?.rows ?? 0;
2326
+ const previewSessionId = selectedSession !== null && !isPendingId(selectedSession.id) ? selectedSession.id : null;
2327
+ const previewLive = selectedSession !== null && LIVE_STATUSES.has(selectedSession.status);
2328
+ const transcript = useQuery2({
2329
+ queryKey: TRANSCRIPT_KEY(previewSessionId ?? "none"),
2330
+ queryFn: () => fetchTranscript(ctx.api, previewSessionId ?? ""),
2331
+ enabled: previewSessionId !== null && selectedSession?.harness !== "shell" && detailWidth > 0,
2332
+ staleTime: 3e3,
2333
+ // A live agent writes while you watch; the record is a read, not an
2334
+ // attach, so following it costs one GET while that session is selected.
2335
+ refetchInterval: previewLive ? 8e3 : false,
2336
+ retry: 1
2337
+ });
2338
+ const preview = useMemo2(
2339
+ () => transcript.data === void 0 ? [] : previewLines(transcript.data, {
2340
+ width: Math.max(16, detailWidth - 6),
2341
+ maxLines: PREVIEW_MAX_LINES
2342
+ }),
2343
+ [transcript.data, detailWidth]
2344
+ );
2345
+ const [previewOffset, setPreviewOffset] = useState2(0);
2346
+ const factRows = Math.max(0, Math.min(SESSION_FACT_ROWS, layout.detailRows - 2));
2347
+ const showFactRule = factRows > 0 && layout.detailRows - factRows > 1;
2348
+ const previewRows = Math.max(1, layout.detailRows - factRows - (showFactRule ? 1 : 0));
2349
+ const previewView = previewWindow(preview, previewRows, previewOffset);
2350
+ const setFocus = (column) => {
2351
+ setFocusState(column);
2352
+ if (isNavSection(column)) setLastNav(column);
2353
+ };
2354
+ const selectSession = (id) => {
2355
+ setSessionKey(id);
2356
+ setPreviewOffset(0);
2357
+ };
1842
2358
  const say = (text) => setStatus({ text, at: Date.now() });
1843
2359
  const refetch = () => void queryClient.invalidateQueries({ queryKey: WORKBENCH_KEY });
1844
2360
  useEffect2(() => {
@@ -1859,19 +2375,30 @@ var App = ({ ctx, onQuit }) => {
1859
2375
  setAdoptOffer(deriveAdoptOffer(cwdFacts(ctx.cwd)));
1860
2376
  }, [data, homeProject]);
1861
2377
  const projectScrollRef = useRef2(null);
2378
+ const worktreeScrollRef = useRef2(null);
1862
2379
  const sessionScrollRef = useRef2(null);
2380
+ const projectRows = sectionRows("projects");
2381
+ const worktreeRows = sectionRows("worktrees");
2382
+ const sessionRows = sectionRows("sessions");
2383
+ useEffect2(() => {
2384
+ keepSpanVisible(projectScrollRef.current, projectIndex, 1, projectRows);
2385
+ }, [projectIndex, projectRows]);
1863
2386
  useEffect2(() => {
1864
- keepRowVisible(projectScrollRef.current, projectIndex);
1865
- }, [projectIndex]);
2387
+ keepSpanVisible(
2388
+ worktreeScrollRef.current,
2389
+ worktreeIndex * WORKTREE_ROW_HEIGHT,
2390
+ WORKTREE_ROW_HEIGHT,
2391
+ worktreeRows
2392
+ );
2393
+ }, [worktreeIndex, worktreeRows]);
1866
2394
  useEffect2(() => {
1867
- const rowHeight = (row) => row === void 0 || row.kind === "worktree" ? 1 : 1 + row.item.processes.length + row.item.services.length;
1868
- let top = 0;
1869
- for (const [index, row] of rows.entries()) {
1870
- if (index === rowIndex) break;
1871
- top += rowHeight(row);
1872
- }
1873
- keepSpanVisible(sessionScrollRef.current, top, rowHeight(rows[rowIndex]));
1874
- }, [rowIndex, rows]);
2395
+ keepSpanVisible(
2396
+ sessionScrollRef.current,
2397
+ sessionIndex * SESSION_ROW_HEIGHT,
2398
+ SESSION_ROW_HEIGHT,
2399
+ sessionRows
2400
+ );
2401
+ }, [sessionIndex, sessionRows]);
1875
2402
  const attachFlow = async (session) => {
1876
2403
  const short = session.id.slice(0, 8);
1877
2404
  lockRef.current = true;
@@ -1896,12 +2423,7 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
1896
2423
  `);
1897
2424
  await ctx.api("POST", `/sessions/${session.id}/handoff`, { to: "pty" });
1898
2425
  outcome = await ctx.attachTty(session.id, session.harness);
1899
- } else if (existing !== null) {
1900
- process.stdout.write(`no live terminal \u2014 rejoining the open shell
1901
-
1902
- `);
1903
- outcome = await ctx.attachTty(session.id, "shell", existing.id);
1904
- } else {
2426
+ } else if (existing === null) {
1905
2427
  const shell = await ctx.api(
1906
2428
  "POST",
1907
2429
  `/sessions/${session.id}/shell`
@@ -1910,17 +2432,22 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
1910
2432
 
1911
2433
  `);
1912
2434
  outcome = await ctx.attachTty(session.id, "shell", shell.id);
2435
+ } else {
2436
+ process.stdout.write(`no live terminal \u2014 rejoining the open shell
2437
+
2438
+ `);
2439
+ outcome = await ctx.attachTty(session.id, "shell", existing.id);
1913
2440
  }
1914
2441
  }
1915
2442
  } catch (error) {
1916
- say(error instanceof Error ? error.message : String(error));
2443
+ say(errorText(error));
1917
2444
  return;
1918
2445
  } finally {
1919
2446
  renderer.resume();
1920
2447
  lockRef.current = false;
1921
2448
  }
1922
2449
  say(
1923
- outcome === "unavailable" ? "attach unavailable \u2014 could not connect" : outcome === "detached" || outcome === "interrupted" ? `detached \u2014 ${short} keeps running` : outcome === "dropped" ? `disconnected \u2014 ${short} keeps running` : `session settled \xB7 ${short}`
2450
+ outcome === "unavailable" ? "attach unavailable \u2014 could not connect" : outcome === "detached" || outcome === "interrupted" ? `detached \u2014 ${short} keeps running` : outcome === "dropped" ? `disconnected \xB7 ${short} \u2014 refreshing session status` : `terminal ended \xB7 ${short} \u2014 refreshing session status`
1924
2451
  );
1925
2452
  refetch();
1926
2453
  };
@@ -1932,13 +2459,6 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
1932
2459
  const current = workbench();
1933
2460
  if (current !== void 0) queryClient.setQueryData(WORKBENCH_KEY, f(current));
1934
2461
  };
1935
- const attachIfIdle = async (session) => {
1936
- if (lockRef.current || modalRef.current) {
1937
- say(`session ready \xB7 ${session.id.slice(0, 8)} \u2014 enter attaches`);
1938
- return;
1939
- }
1940
- await attachFlow(session);
1941
- };
1942
2462
  const launchMutation = useMutation2({
1943
2463
  mutationFn: async (vars) => {
1944
2464
  const argv = HARNESS_COMMANDS[vars.harness];
@@ -1967,6 +2487,7 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
1967
2487
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1968
2488
  })
1969
2489
  );
2490
+ setWorktreeKey(vars.pendingKey);
1970
2491
  selectSession(vars.pendingKey);
1971
2492
  setBusy(`provisioning ${vars.harness} workspace \u2014 a first launch builds the harness image \xB7`);
1972
2493
  setBusyStarted(Date.now());
@@ -1976,15 +2497,21 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
1976
2497
  setBusy(null);
1977
2498
  say(errorText(error));
1978
2499
  },
1979
- onSuccess: async (session, vars) => {
2500
+ onSuccess: (session, vars) => {
1980
2501
  patchWorkbench(
1981
2502
  (current) => replaceSession(current, vars.projectId, vars.pendingKey, session)
1982
2503
  );
1983
- selectSession(session.id);
2504
+ setWorktreeKey(
2505
+ (current) => current === vars.pendingKey ? session.worktreeId ?? session.id : current
2506
+ );
2507
+ setSessionKey((current) => current === vars.pendingKey ? session.id : current);
1984
2508
  setBusy(null);
1985
- await attachIfIdle(session);
2509
+ say(`started \xB7 ${sessionDisplayName(session)} \u2014 a attaches`);
1986
2510
  },
1987
- onSettled: settleRefetch
2511
+ onSettled: (_data, _error, vars) => {
2512
+ gate.release(vars.gateKey);
2513
+ settleRefetch();
2514
+ }
1988
2515
  });
1989
2516
  const resumeMutation = useMutation2({
1990
2517
  mutationFn: (vars) => ctx.api("POST", `/sessions/${vars.session.id}/resume`, {
@@ -2000,7 +2527,7 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2000
2527
  );
2001
2528
  selectSession(vars.session.id);
2002
2529
  setBusy(
2003
- `resuming ${vars.session.id.slice(0, 8)} \u2014 a fresh workspace restores the saved state \xB7`
2530
+ `resuming ${sessionDisplayName(vars.session)} \u2014 a fresh workspace restores the saved state \xB7`
2004
2531
  );
2005
2532
  setBusyStarted(Date.now());
2006
2533
  },
@@ -2009,15 +2536,17 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2009
2536
  say(errorText(error));
2010
2537
  refetch();
2011
2538
  },
2012
- onSuccess: async (resumed, vars) => {
2539
+ onSuccess: (resumed, vars) => {
2013
2540
  patchWorkbench(
2014
2541
  (current) => replaceSession(current, vars.projectId, vars.session.id, resumed)
2015
2542
  );
2016
- selectSession(resumed.id);
2017
2543
  setBusy(null);
2018
- await attachIfIdle(resumed);
2544
+ say(`resumed \xB7 ${sessionDisplayName(resumed)} \u2014 a attaches`);
2019
2545
  },
2020
- onSettled: settleRefetch
2546
+ onSettled: (_data, _error, vars) => {
2547
+ gate.release(vars.gateKey);
2548
+ settleRefetch();
2549
+ }
2021
2550
  });
2022
2551
  const ADOPT_AUTH_MODES = [
2023
2552
  {
@@ -2058,8 +2587,9 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2058
2587
  return;
2059
2588
  }
2060
2589
  const project = result.project;
2061
- say(`adopted \xB7 ${project.name} \u2014 n starts a worktree`);
2590
+ say(`adopted \xB7 ${project.name} \u2014 w starts a worktree`);
2062
2591
  setProjectKey(project.id);
2592
+ setWorktreeKey(null);
2063
2593
  setSessionKey(null);
2064
2594
  },
2065
2595
  onSettled: settleRefetch
@@ -2089,7 +2619,7 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2089
2619
  onMutate: async (session) => {
2090
2620
  await queryClient.cancelQueries({ queryKey: WORKBENCH_KEY });
2091
2621
  patchWorkbench((current) => markSessionStopped(current, session.id));
2092
- say(`stopped \xB7 ${worktreeDisplayName(session)} \u2014 the record and review remain`);
2622
+ say(`stopped \xB7 ${sessionDisplayName(session)} \u2014 the record and review remain`);
2093
2623
  },
2094
2624
  onError: (error) => {
2095
2625
  say(errorText(error));
@@ -2133,24 +2663,29 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2133
2663
  setBusy(null);
2134
2664
  say(errorText(error));
2135
2665
  },
2136
- onSuccess: async (session, vars) => {
2666
+ onSuccess: (session, vars) => {
2137
2667
  patchWorkbench(
2138
2668
  (current) => replaceSession(current, vars.projectId, vars.pendingKey, session)
2139
2669
  );
2140
- selectSession(session.id);
2670
+ setSessionKey((current) => current === vars.pendingKey ? session.id : current);
2141
2671
  setBusy(null);
2142
- await attachIfIdle(session);
2672
+ say(`started \xB7 ${sessionDisplayName(session)} \u2014 a attaches`);
2143
2673
  },
2144
- onSettled: settleRefetch
2674
+ onSettled: (_data, _error, vars) => {
2675
+ gate.release(vars.gateKey);
2676
+ settleRefetch();
2677
+ }
2145
2678
  });
2146
2679
  const removeWorktreeMutation = useMutation2({
2147
- mutationFn: (group) => group.id !== null ? ctx.api("DELETE", `/worktrees/${group.id}`) : ctx.api("DELETE", `/sessions/${group.sessions[0]?.session.id ?? ""}`),
2680
+ mutationFn: (group) => group.id === null ? ctx.api("DELETE", `/sessions/${group.sessions[0]?.session.id ?? ""}`) : ctx.api("DELETE", `/worktrees/${group.id}`),
2148
2681
  onMutate: async (group) => {
2149
2682
  await queryClient.cancelQueries({ queryKey: WORKBENCH_KEY });
2150
2683
  const projectId = selectedProject?.project.id;
2151
2684
  if (projectId !== void 0) {
2152
2685
  patchWorkbench((current) => removeWorktreeGroup(current, projectId, group));
2153
2686
  }
2687
+ setWorktreeKey(null);
2688
+ setSessionKey(null);
2154
2689
  say(`removing worktree \xB7 ${group.name}`);
2155
2690
  },
2156
2691
  onError: (error) => {
@@ -2171,121 +2706,136 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2171
2706
  if (projectId !== void 0) {
2172
2707
  patchWorkbench((current) => removeSession(current, projectId, session.id));
2173
2708
  }
2174
- say(`removing session \xB7 ${worktreeDisplayName(session)}`);
2709
+ setSessionKey(null);
2710
+ say(`removing session \xB7 ${sessionDisplayName(session)}`);
2175
2711
  },
2176
2712
  onError: (error) => {
2177
2713
  say(errorText(error));
2178
2714
  refetch();
2179
2715
  },
2180
2716
  onSuccess: (_result, session) => {
2181
- say(`removed \xB7 ${worktreeDisplayName(session)} \u2014 the worktree remains`);
2717
+ say(`removed \xB7 ${sessionDisplayName(session)} \u2014 the worktree remains`);
2182
2718
  refetch();
2183
2719
  },
2184
2720
  onSettled: settleRefetch
2185
2721
  });
2186
- const [removeArmed, setRemoveArmed] = useState2(null);
2722
+ const confirmationVisible = (prefix) => busy === null && status !== null && Date.now() - status.at < 5e3 && status.text.startsWith(prefix);
2187
2723
  const armRemove = () => {
2188
- if (selectedRow?.kind === "session") {
2189
- const session = selectedRow.item.session;
2190
- if (isPendingId(session.id)) return;
2191
- if (AGENT_LIVE_STATUSES.has(session.status)) {
2192
- say(`the agent is still working \u2014 kill it first (\u21E7K) \xB7 ${worktreeDisplayName(session)}`);
2724
+ if (focus === "worktrees") {
2725
+ const group = selectedGroup;
2726
+ if (group === null || group.sessions.some((item) => isPendingId(item.session.id))) return;
2727
+ if (group.live > 0) {
2728
+ say(
2729
+ `${group.live} session${group.live === 1 ? "" : "s"} live \u2014 stop them first (\u21E7K) \xB7 ${group.name}`
2730
+ );
2193
2731
  return;
2194
2732
  }
2195
- if (removeArmed === session.id) {
2733
+ if (removeArmed === `wt:${group.key}` && confirmationVisible("press \u21E7D again")) {
2196
2734
  setRemoveArmed(null);
2197
- removeSessionMutation.mutate(session);
2735
+ removeWorktreeMutation.mutate(group);
2198
2736
  return;
2199
2737
  }
2200
- setRemoveArmed(session.id);
2201
- say(
2202
- `press again to remove session \xB7 ${worktreeDisplayName(session)} \u2014 its record goes, the worktree stays`
2203
- );
2738
+ setRemoveArmed(`wt:${group.key}`);
2739
+ const facts = group.sessions.length === 1 ? "its session and change go with it" : `${group.sessions.length} sessions and the change go with it`;
2740
+ say(`press \u21E7D again to remove worktree \xB7 ${group.name} \u2014 ${facts}`);
2204
2741
  return;
2205
2742
  }
2206
- const group = selectedGroup;
2207
- if (group === null || group.sessions.some((item) => isPendingId(item.session.id))) return;
2208
- if (group.live > 0) {
2209
- say(
2210
- `${group.live} session${group.live === 1 ? "" : "s"} live \u2014 stop them first \xB7 ${group.name}`
2211
- );
2743
+ const session = selectedSession;
2744
+ if (session === null || isPendingId(session.id)) return;
2745
+ if (AGENT_LIVE_STATUSES.has(session.status)) {
2746
+ say(`the agent is still working \u2014 stop it first (\u21E7K) \xB7 ${sessionDisplayName(session)}`);
2212
2747
  return;
2213
2748
  }
2214
- if (removeArmed === group.key) {
2749
+ if (removeArmed === session.id && confirmationVisible("press \u21E7D again")) {
2215
2750
  setRemoveArmed(null);
2216
- removeWorktreeMutation.mutate(group);
2751
+ removeSessionMutation.mutate(session);
2217
2752
  return;
2218
2753
  }
2219
- setRemoveArmed(group.key);
2220
- const facts = group.sessions.length === 1 ? "its session and change go with it" : `${group.sessions.length} sessions and the change go with it`;
2221
- say(`press again to remove worktree \xB7 ${group.name} \u2014 ${facts}`);
2754
+ setRemoveArmed(session.id);
2755
+ say(
2756
+ `press \u21E7D again to remove session \xB7 ${sessionDisplayName(session)} \u2014 its record goes, the worktree stays`
2757
+ );
2222
2758
  };
2223
2759
  const armStop = () => {
2224
- if (selectedRow?.kind === "worktree") {
2225
- const group = selectedRow.group;
2226
- const live = group.sessions.filter((item2) => LIVE_STATUSES.has(item2.session.status));
2760
+ if (focus === "worktrees") {
2761
+ const group = selectedGroup;
2762
+ if (group === null) return;
2763
+ const live = group.sessions.filter((item) => LIVE_STATUSES.has(item.session.status));
2227
2764
  if (live.length === 0) {
2228
2765
  say("nothing to stop \u2014 the worktree is settled");
2229
2766
  return;
2230
2767
  }
2231
- if (stopArmed === `wt:${group.key}`) {
2768
+ if (stopArmed === `wt:${group.key}` && confirmationVisible("press \u21E7K again")) {
2232
2769
  setStopArmed(null);
2233
- for (const item2 of live) stopMutation.mutate(item2.session);
2770
+ for (const item of live) stopMutation.mutate(item.session);
2234
2771
  return;
2235
2772
  }
2236
2773
  setStopArmed(`wt:${group.key}`);
2237
2774
  say(
2238
- `press again to stop ${live.length} live session${live.length === 1 ? "" : "s"} \xB7 ${group.name}`
2775
+ `press \u21E7K again to stop ${live.length} live session${live.length === 1 ? "" : "s"} \xB7 ${group.name}`
2239
2776
  );
2240
2777
  return;
2241
2778
  }
2242
- const item = selectedSession;
2243
- if (item === null || isPendingId(item.session.id)) return;
2244
- if (!LIVE_STATUSES.has(item.session.status)) {
2779
+ const session = selectedSession;
2780
+ if (session === null || isPendingId(session.id)) return;
2781
+ if (!LIVE_STATUSES.has(session.status)) {
2245
2782
  say("nothing to stop \u2014 the session is settled");
2246
2783
  return;
2247
2784
  }
2248
- if (stopArmed === item.session.id) {
2785
+ if (stopArmed === session.id && confirmationVisible("press \u21E7K again")) {
2249
2786
  setStopArmed(null);
2250
- stopMutation.mutate(item.session);
2787
+ stopMutation.mutate(session);
2251
2788
  return;
2252
2789
  }
2253
- setStopArmed(item.session.id);
2254
- say(`press again to stop \xB7 ${worktreeDisplayName(item.session)}`);
2790
+ setStopArmed(session.id);
2791
+ say(`press \u21E7K again to stop \xB7 ${sessionDisplayName(session)}`);
2255
2792
  };
2256
2793
  const openCreateModal = (projectId) => {
2257
- setCreating({
2794
+ const request = ++branchRequest.current;
2795
+ const draft = {
2258
2796
  projectId,
2259
2797
  step: "name",
2260
2798
  name: "",
2261
2799
  branches: null,
2800
+ branchError: null,
2262
2801
  query: "",
2263
2802
  baseIndex: 0,
2264
2803
  base: null,
2265
2804
  joins: false,
2266
2805
  harnessIndex: 0
2267
- });
2806
+ };
2807
+ creatingRef.current = draft;
2808
+ setCreating(draft);
2268
2809
  const prefill = projectId === homeProject?.id ? ctx.cwdBranch : null;
2269
2810
  void ctx.api("GET", `/projects/${projectId}/branches`).then(
2270
2811
  (branches) => setCreating((current) => {
2271
- if (current === null) return current;
2812
+ if (current?.projectId !== projectId || request !== branchRequest.current) return current;
2272
2813
  const match = prefill === null ? void 0 : branches.find((candidate) => candidate.name === prefill);
2273
2814
  const ordered = filterBranches(branches, "");
2274
2815
  return {
2275
2816
  ...current,
2276
2817
  branches,
2277
- base: match === void 0 || match.isDefault ? null : match.name,
2278
- baseIndex: match === void 0 ? 0 : Math.max(
2279
- 0,
2280
- ordered.findIndex((candidate) => candidate.name === match.name)
2281
- )
2818
+ branchError: null,
2819
+ // A late response may populate the list, but not change a base
2820
+ // the user has already selected or a filter they are navigating.
2821
+ ...current.step === "name" || current.step === "base" && current.query === "" && current.baseIndex === 0 ? {
2822
+ base: match === void 0 || match.isDefault ? null : match.name,
2823
+ baseIndex: match === void 0 ? 0 : Math.max(
2824
+ 0,
2825
+ ordered.findIndex((candidate) => candidate.name === match.name)
2826
+ )
2827
+ } : {}
2282
2828
  };
2283
2829
  }),
2284
- () => (
2285
- // An unreadable list falls back to the default base; the base step
2286
- // then shows only the default and enter moves on.
2287
- setCreating((current) => current === null ? current : { ...current, branches: [] })
2288
- )
2830
+ (error) => {
2831
+ if (creatingRef.current?.projectId !== projectId || request !== branchRequest.current)
2832
+ return;
2833
+ const message = errorText(error);
2834
+ say(`could not read branches \u2014 ${message}`);
2835
+ setCreating(
2836
+ (current) => current?.projectId === projectId ? { ...current, branches: [], branchError: message } : current
2837
+ );
2838
+ }
2289
2839
  );
2290
2840
  };
2291
2841
  const advanceFromName = (current, raw) => {
@@ -2313,6 +2863,12 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2313
2863
  if (current === null) return;
2314
2864
  const choice = deriveHarnesses(null)[current.harnessIndex];
2315
2865
  if (choice?.harness == null) return;
2866
+ const gateKey = `launch:${current.projectId}:${current.name}`;
2867
+ if (!gate.take(gateKey)) {
2868
+ setCreating(null);
2869
+ say("that worktree is already starting \u2014 wait for it to finish before starting another");
2870
+ return;
2871
+ }
2316
2872
  setCreating(null);
2317
2873
  setFocus("sessions");
2318
2874
  launchMutation.mutate({
@@ -2320,13 +2876,10 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2320
2876
  harness: choice.harness,
2321
2877
  name: current.name === "" ? null : current.name,
2322
2878
  base: current.base,
2323
- pendingKey: pendingId()
2879
+ pendingKey: pendingId(),
2880
+ gateKey
2324
2881
  });
2325
2882
  };
2326
- const resumeSession = (projectId, session, harness) => {
2327
- setFocus("sessions");
2328
- resumeMutation.mutate({ projectId, session, harness });
2329
- };
2330
2883
  const submitRename = (session, value) => {
2331
2884
  setEditing(null);
2332
2885
  const label = value.trim() === "" ? null : value.trim();
@@ -2336,86 +2889,142 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2336
2889
  setPicker(worktree === void 0 ? { session } : { session, worktree });
2337
2890
  setPickerIndex(0);
2338
2891
  };
2339
- const moveSelection = (delta) => {
2340
- if (picker !== null) {
2341
- setPickerIndex((current) => Math.max(0, Math.min(pickerItems.length - 1, current + delta)));
2342
- return;
2892
+ const attachSelected = () => {
2893
+ const plan = planAttach(selectedSession);
2894
+ switch (plan.kind) {
2895
+ case "attach":
2896
+ void attachFlow(plan.session);
2897
+ return;
2898
+ case "starting":
2899
+ say(
2900
+ `still starting \xB7 ${sessionDisplayName(plan.session)} \u2014 a attaches once the row reads running`
2901
+ );
2902
+ return;
2903
+ case "settled":
2904
+ say(`settled \xB7 ${sessionDisplayName(plan.session)} \u2014 r resumes it, \u21E7D removes it`);
2905
+ return;
2906
+ case "pending":
2907
+ say("still provisioning \u2014 the row fills in when the workspace answers");
2908
+ return;
2909
+ case "none":
2910
+ say("no session selected \u2014 n starts one");
2911
+ return;
2343
2912
  }
2344
- if (focus === "projects") {
2345
- if (projectItems.length === 0) return;
2346
- const next2 = Math.max(0, Math.min(projectItems.length - 1, projectIndex + delta));
2347
- const item = projectItems[next2];
2348
- if (item !== void 0) {
2349
- setProjectKey(item.project.id);
2350
- setSessionKey(null);
2351
- }
2352
- return;
2913
+ };
2914
+ const resumeSelected = () => {
2915
+ const plan = planResume(selectedSession);
2916
+ switch (plan.kind) {
2917
+ case "resume":
2918
+ if (gate.held(`resume:${plan.session.id}`)) {
2919
+ say(`already resuming \xB7 ${sessionDisplayName(plan.session)}`);
2920
+ return;
2921
+ }
2922
+ openPicker(plan.session);
2923
+ return;
2924
+ case "live":
2925
+ say(`already live \xB7 ${sessionDisplayName(plan.session)} \u2014 a attaches`);
2926
+ return;
2927
+ case "pending":
2928
+ say("still provisioning \u2014 nothing to resume yet");
2929
+ return;
2930
+ case "none":
2931
+ say("no session selected \u2014 n starts one");
2932
+ return;
2353
2933
  }
2354
- if (rows.length === 0) return;
2355
- const next = Math.max(0, Math.min(rows.length - 1, rowIndex + delta));
2356
- const row = rows[next];
2357
- if (row !== void 0) setSessionKey(rowKeyOf(row));
2358
2934
  };
2359
- const sayStillStarting = (session) => {
2360
- say(
2361
- busy !== null ? `still starting \xB7 ${worktreeDisplayName(session)} \u2014 attaches when the workspace answers` : `still starting \xB7 ${worktreeDisplayName(session)} \u2014 enter attaches once the row reads running`
2362
- );
2935
+ const resumeSession = (projectId, session, harness) => {
2936
+ const gateKey = `resume:${session.id}`;
2937
+ if (!gate.take(gateKey)) {
2938
+ say(`already resuming \xB7 ${sessionDisplayName(session)}`);
2939
+ return;
2940
+ }
2941
+ setFocus("sessions");
2942
+ resumeMutation.mutate({ projectId, session, harness, gateKey });
2363
2943
  };
2364
- const activate = () => {
2365
- if (picker !== null) {
2366
- const choice = pickerItems[pickerIndex];
2367
- const projectId = selectedProject?.project.id;
2368
- if (choice === void 0 || projectId === void 0) return;
2369
- setPicker(null);
2370
- if (picker.worktree !== void 0 && picker.worktree.id !== null) {
2371
- if (choice.harness !== null) {
2372
- setFocus("sessions");
2373
- launchInWorktreeMutation.mutate({
2374
- projectId,
2375
- worktreeId: picker.worktree.id,
2376
- harness: choice.harness,
2377
- pendingKey: pendingId()
2378
- });
2379
- }
2380
- } else if (picker.session !== null) {
2381
- resumeSession(projectId, picker.session, choice.harness);
2382
- }
2944
+ const newSession = () => {
2945
+ const projectId = selectedProject?.project.id;
2946
+ if (projectId === void 0) {
2947
+ say("no project selected");
2383
2948
  return;
2384
2949
  }
2950
+ const group = selectedGroup;
2385
2951
  if (focus === "projects") {
2386
- setFocus("sessions");
2952
+ openCreateModal(projectId);
2387
2953
  return;
2388
2954
  }
2389
- if (selectedRow?.kind === "worktree") {
2390
- const target2 = enterTargetOf(selectedRow.group.sessions.map((item2) => item2.session));
2391
- if (target2.kind === "attach") {
2392
- void attachFlow(target2.session);
2393
- } else if (target2.kind === "wait") {
2394
- sayStillStarting(target2.session);
2395
- } else if (selectedRow.group.id !== null) {
2396
- openPicker(null, selectedRow.group);
2397
- }
2955
+ if (group === null || group.id === null) {
2956
+ openCreateModal(projectId);
2398
2957
  return;
2399
2958
  }
2400
- const item = selectedSession;
2401
- if (item === null) return;
2402
- if (isPendingId(item.session.id)) {
2403
- say("still provisioning \u2014 the row fills in when the workspace answers");
2959
+ if (gate.held(`worktree-session:${group.id}`)) {
2960
+ say(`already starting a session in ${group.name}`);
2404
2961
  return;
2405
2962
  }
2406
- const target = enterTargetOf([item.session]);
2407
- if (target.kind === "attach") {
2408
- void attachFlow(target.session);
2409
- } else if (target.kind === "wait") {
2410
- sayStillStarting(target.session);
2411
- } else {
2412
- openPicker(item.session);
2963
+ openPicker(null, group);
2964
+ };
2965
+ const launchInWorktree = (projectId, group, harness) => {
2966
+ if (group.id === null) return;
2967
+ const gateKey = `worktree-session:${group.id}`;
2968
+ if (!gate.take(gateKey)) {
2969
+ say(`already starting a session in ${group.name}`);
2970
+ return;
2413
2971
  }
2972
+ setFocus("sessions");
2973
+ launchInWorktreeMutation.mutate({
2974
+ projectId,
2975
+ worktreeId: group.id,
2976
+ harness,
2977
+ pendingKey: pendingId(),
2978
+ gateKey
2979
+ });
2414
2980
  };
2981
+ const moveSelection = (delta) => {
2982
+ if (picker !== null) {
2983
+ setPickerIndex((current) => clampIndex(pickerItems.length, current + delta));
2984
+ return;
2985
+ }
2986
+ switch (focus) {
2987
+ case "projects": {
2988
+ const item = projectItems[clampIndex(projectItems.length, projectIndex + delta)];
2989
+ if (item === void 0) return;
2990
+ setProjectKey(item.project.id);
2991
+ setWorktreeKey(null);
2992
+ setSessionKey(null);
2993
+ setPreviewOffset(0);
2994
+ return;
2995
+ }
2996
+ case "worktrees": {
2997
+ const group = worktreeGroups[clampIndex(worktreeGroups.length, worktreeIndex + delta)];
2998
+ if (group === void 0) return;
2999
+ setWorktreeKey(group.key);
3000
+ setSessionKey(null);
3001
+ setPreviewOffset(0);
3002
+ return;
3003
+ }
3004
+ case "sessions": {
3005
+ const item = sessionItems[clampIndex(sessionItems.length, sessionIndex + delta)];
3006
+ if (item === void 0) return;
3007
+ selectSession(item.session.id);
3008
+ return;
3009
+ }
3010
+ case "detail":
3011
+ setPreviewOffset(
3012
+ Math.max(
3013
+ 0,
3014
+ Math.min(Math.max(0, preview.length - previewRows), previewView.offset - delta)
3015
+ )
3016
+ );
3017
+ return;
3018
+ }
3019
+ };
3020
+ const moveColumn = (delta) => setFocus(stepColumn(focus, delta));
2415
3021
  useKeyboard2((key) => {
2416
3022
  if (reviewing !== null) return;
2417
3023
  if (lockRef.current) return;
2418
3024
  if (key.ctrl && key.name === "c") return onQuit();
3025
+ const verb = verbForKey(key.name ?? "", key.shift === true);
3026
+ if (verb !== "stop") setStopArmed(null);
3027
+ if (verb !== "remove") setRemoveArmed(null);
2419
3028
  if (adoptOffer !== null) {
2420
3029
  if (key.name === "return" || key.name === "linefeed" || key.name === "y") {
2421
3030
  adoptMutation.mutate(adoptOffer);
@@ -2473,17 +3082,11 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2473
3082
  if (current === null) return current;
2474
3083
  if (current.step === "base") {
2475
3084
  const matches = filterBranches(current.branches ?? [], current.query).length;
2476
- return {
2477
- ...current,
2478
- baseIndex: Math.max(0, Math.min(matches - 1, current.baseIndex + delta))
2479
- };
3085
+ return { ...current, baseIndex: clampIndex(matches, current.baseIndex + delta) };
2480
3086
  }
2481
3087
  if (current.step === "harness") {
2482
3088
  const count = deriveHarnesses(null).length;
2483
- return {
2484
- ...current,
2485
- harnessIndex: Math.max(0, Math.min(count - 1, current.harnessIndex + delta))
2486
- };
3089
+ return { ...current, harnessIndex: clampIndex(count, current.harnessIndex + delta) };
2487
3090
  }
2488
3091
  return current;
2489
3092
  });
@@ -2504,8 +3107,20 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2504
3107
  return moveSelection(-1);
2505
3108
  case "return":
2506
3109
  case "linefeed":
2507
- case "l":
2508
- return activate();
3110
+ case "l": {
3111
+ const choice = pickerItems[pickerIndex];
3112
+ const projectId = selectedProject?.project.id;
3113
+ const target = picker;
3114
+ if (choice === void 0 || projectId === void 0) return;
3115
+ setPicker(null);
3116
+ if (target.worktree !== void 0) {
3117
+ if (choice.harness !== null)
3118
+ launchInWorktree(projectId, target.worktree, choice.harness);
3119
+ } else if (target.session !== null) {
3120
+ resumeSession(projectId, target.session, choice.harness);
3121
+ }
3122
+ return;
3123
+ }
2509
3124
  case "escape":
2510
3125
  case "q":
2511
3126
  case "h":
@@ -2514,71 +3129,67 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2514
3129
  return;
2515
3130
  }
2516
3131
  }
2517
- if (key.shift === true && key.name === "k") return armStop();
2518
- if (key.shift === true && key.name === "d") return armRemove();
2519
- switch (key.name) {
2520
- case "q":
3132
+ if (key.ctrl === true) return;
3133
+ if (focus === "projects" && ["attach", "resume", "stop", "remove", "rename", "openWeb", "review"].includes(verb ?? "")) {
3134
+ say("select a session first \u2014 \u2192 opens worktrees");
3135
+ return;
3136
+ }
3137
+ switch (verb) {
3138
+ case "quit":
2521
3139
  return onQuit();
2522
- case "down":
2523
- case "j":
3140
+ case "moveDown":
2524
3141
  return moveSelection(1);
2525
- case "up":
2526
- case "k":
3142
+ case "moveUp":
2527
3143
  return moveSelection(-1);
2528
- case "x":
2529
- return armStop();
2530
- case "return":
2531
- case "linefeed":
2532
- return activate();
2533
- case "l":
2534
- case "right":
2535
- if (focus === "projects") setFocus("sessions");
2536
- return;
2537
- case "h":
2538
- case "left":
2539
- case "-":
2540
- case "backspace":
2541
- if (showProjectsPane) setFocus("projects");
2542
- return;
2543
- case "tab":
2544
- if (showProjectsPane) setFocus(focus === "projects" ? "sessions" : "projects");
2545
- return;
2546
- case "n":
3144
+ case "pageDown":
3145
+ return moveSelection(10);
3146
+ case "pageUp":
3147
+ return moveSelection(-10);
3148
+ case "columnRight":
3149
+ return moveColumn(1);
3150
+ case "columnLeft":
3151
+ return moveColumn(-1);
3152
+ case "attach":
3153
+ return attachSelected();
3154
+ case "resume":
3155
+ return resumeSelected();
3156
+ case "newSession":
3157
+ return newSession();
3158
+ case "newWorktree":
2547
3159
  if (selectedProject !== null) openCreateModal(selectedProject.project.id);
2548
3160
  return;
2549
- case "s": {
2550
- const group = selectedGroup;
2551
- if (group === null) return;
2552
- if (group.id === null) {
2553
- say("this server predates shared worktrees \u2014 n starts a new one");
2554
- return;
2555
- }
2556
- openPicker(null, group);
3161
+ case "stop":
3162
+ return armStop();
3163
+ case "remove":
3164
+ return armRemove();
3165
+ case "refresh":
3166
+ say("refreshing\u2026");
3167
+ refetch();
3168
+ void queryClient.invalidateQueries({ queryKey: ["transcript"] });
2557
3169
  return;
2558
- }
2559
- case "e": {
2560
- const item = selectedSession;
2561
- if (item !== null && !isPendingId(item.session.id)) setEditing(item.session);
3170
+ case "rename": {
3171
+ const session = selectedSession;
3172
+ if (session !== null && !isPendingId(session.id)) setEditing(session);
2562
3173
  return;
2563
3174
  }
2564
- case "o": {
2565
- const item = selectedSession;
2566
- if (item !== null && !isPendingId(item.session.id)) {
2567
- openUrl(`${ctx.config.url}/sessions/${item.session.id}`);
2568
- say(`opened \xB7 ${ctx.config.url}/sessions/${item.session.id.slice(0, 8)}\u2026`);
3175
+ case "openWeb": {
3176
+ const session = selectedSession;
3177
+ if (session !== null && !isPendingId(session.id)) {
3178
+ openUrl(`${ctx.config.url}/sessions/${session.id}`);
3179
+ say(`opened \xB7 ${ctx.config.url}/sessions/${session.id.slice(0, 8)}\u2026`);
2569
3180
  }
2570
3181
  return;
2571
3182
  }
2572
- case "v": {
2573
- const item = selectedSession;
2574
- if (item === null) return;
2575
- if (isPendingId(item.session.id)) {
3183
+ case "review": {
3184
+ const session = selectedSession;
3185
+ if (session === null) return;
3186
+ if (isPendingId(session.id)) {
2576
3187
  say("still provisioning \u2014 nothing to review yet");
2577
3188
  return;
2578
3189
  }
2579
3190
  const target = reviewTargetForSession(
2580
- item.session,
2581
- selectedGroup?.annotation ?? item.annotation,
3191
+ session,
3192
+ selectedItem?.annotation ?? selectedGroup?.annotation,
2582
3193
  selectedProject?.project.name ?? "project"
2583
3194
  );
2584
3195
  if (target === null) {
@@ -2588,22 +3199,28 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2588
3199
  setReviewing(target);
2589
3200
  return;
2590
3201
  }
2591
- case "r":
2592
- say("refreshing\u2026");
2593
- refetch();
2594
- return;
2595
- default:
3202
+ case null:
2596
3203
  return;
2597
3204
  }
2598
3205
  });
2599
3206
  const liveTotal = projectItems.reduce((sum, item) => sum + item.live, 0);
2600
- const nameWidth = Math.min(16, Math.max(...projectItems.map((p) => p.project.name.length), 4));
2601
- const sessionsTitle = selectedProject === null ? "sessions" : `sessions \u2014 ${selectedProject.project.name}${selectedProject.live > 0 ? ` \xB7 ${selectedProject.live} live` : ""}`;
2602
- const detailTitle = selectedSession === null || selectedGroup === null ? "session" : `worktree \u2014 ${selectedGroup.name} \xB7 ${selectedSession.session.harness} ${selectedSession.session.id.slice(0, 8)}`;
2603
- const pickerTitle = picker === null ? "" : picker.worktree !== void 0 ? `new session in ${picker.worktree.name} \u2014 pick a harness` : picker.session === null ? "new session \u2014 pick a harness" : `resume ${picker.session.id.slice(0, 8)} \u2014 pick a harness`;
3207
+ const columnTitle = (column) => {
3208
+ if (column === "worktrees") {
3209
+ return selectedProject === null ? "worktrees" : `worktrees \xB7 ${worktreeGroups.length}${selectedProject.live > 0 ? ` \xB7 ${selectedProject.live} live` : ""}`;
3210
+ }
3211
+ if (column === "sessions") {
3212
+ return selectedGroup === null ? "sessions" : `sessions \xB7 ${sessionItems.length}`;
3213
+ }
3214
+ if (column === "detail") {
3215
+ if (selectedSession === null) return "session";
3216
+ return previewView.offset === 0 ? "session \xB7 read-only" : `session \xB7 read-only \xB7 ${previewView.later} newer below`;
3217
+ }
3218
+ return COLUMN_TITLE[column];
3219
+ };
3220
+ const pickerTitle = picker === null ? "" : picker.worktree === void 0 ? picker.session === null ? "new session \u2014 pick a harness" : `resume ${sessionDisplayName(picker.session)} \u2014 pick a harness` : `new session in ${picker.worktree.name} \u2014 pick a harness`;
2604
3221
  const creatingHeight = 2 + 1 + 1 + 6 + 1 + deriveHarnesses(null).length;
2605
- const footerText = adoptOffer !== null ? " enter adopt \xB7 \u2190\u2192 auth mode \xB7 esc not now" : editing !== null ? " enter save \xB7 esc cancel" : creating !== null ? creating.step === "name" ? " enter continue \xB7 esc cancel" : creating.step === "base" ? " type to filter \xB7 \u2191\u2193 move \xB7 enter choose base \xB7 esc back" : " \u2191\u2193 move \xB7 enter launch \xB7 esc back" : picker !== null ? " \u2191\u2193 move \xB7 enter start \xB7 esc cancel" : focus === "projects" ? " \u2191\u2193 move \xB7 enter/l sessions \xB7 \u21E5 panes \xB7 r refresh \xB7 q quit" : " \u2191\u2193 move \xB7 enter attach/resume \xB7 n new worktree \xB7 s session here \xB7 \u21E7K kill \xB7 \u21E7D remove \xB7 v review \xB7 e rename \xB7 o web \xB7 q quit";
2606
- const loadFailure = data === void 0 && failureReason !== null ? failureReason instanceof Error ? failureReason.message : String(failureReason) : null;
3222
+ const footerText = adoptOffer === null ? editing === null ? creating === null ? picker === null ? ` ${fitHints(verbHints(focus), Math.max(12, terminalCols - 2))}` : " \u2191\u2193 move \xB7 enter start \xB7 esc cancel" : creating.step === "name" ? " enter continue \xB7 esc cancel" : creating.step === "base" ? " type to filter \xB7 \u2191\u2193 move \xB7 enter choose base \xB7 esc back" : " \u2191\u2193 move \xB7 enter launch \xB7 esc back" : " enter save \xB7 esc cancel" : " enter adopt \xB7 \u2190\u2192 auth mode \xB7 esc not now";
3223
+ const loadFailure = data === void 0 && failureReason !== null ? errorText(failureReason) : null;
2607
3224
  if (reviewing !== null) {
2608
3225
  return /* @__PURE__ */ jsx2(
2609
3226
  ReviewScreen,
@@ -2620,7 +3237,160 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2620
3237
  }
2621
3238
  );
2622
3239
  }
2623
- return /* @__PURE__ */ jsxs2("box", { flexGrow: 1, flexDirection: "column", backgroundColor: "transparent", children: [
3240
+ const breadcrumbValue = (column) => {
3241
+ if (column === "projects") return selectedProject?.project.name ?? "no project";
3242
+ if (column === "worktrees") return selectedGroup?.name ?? "no worktree";
3243
+ return selectedSession === null ? "no session" : sessionDisplayName(selectedSession);
3244
+ };
3245
+ const sectionFact = (section) => {
3246
+ if (section === "projects") {
3247
+ if (selectedProject === null) return "";
3248
+ return selectedProject.live > 0 ? `${selectedProject.live} live` : "settled";
3249
+ }
3250
+ if (section === "worktrees") {
3251
+ if (selectedGroup === null) return "";
3252
+ return selectedGroup.live > 0 ? foldGroupStatus(selectedGroup) : "settled";
3253
+ }
3254
+ return selectedSession?.status ?? "";
3255
+ };
3256
+ const sectionEmpty = (section) => section === "projects" ? selectedProject === null : section === "worktrees" ? selectedGroup === null : selectedSession === null;
3257
+ const paneTitle = (column, width) => fit(columnTitle(column), Math.max(6, width - 6));
3258
+ const renderSection = (entry) => {
3259
+ const inner = Math.max(4, layout.sidebarWidth - 2);
3260
+ if (!entry.expanded) {
3261
+ return /* @__PURE__ */ jsx2(
3262
+ Pane,
3263
+ {
3264
+ title: paneTitle(entry.section, layout.sidebarWidth),
3265
+ focused: false,
3266
+ height: entry.height,
3267
+ children: /* @__PURE__ */ jsx2(
3268
+ SummaryRow,
3269
+ {
3270
+ name: breadcrumbValue(entry.section),
3271
+ fact: sectionFact(entry.section),
3272
+ width: inner,
3273
+ empty: sectionEmpty(entry.section)
3274
+ }
3275
+ )
3276
+ },
3277
+ entry.section
3278
+ );
3279
+ }
3280
+ const focused = focus === entry.section;
3281
+ switch (entry.section) {
3282
+ case "projects":
3283
+ return /* @__PURE__ */ jsx2(
3284
+ Pane,
3285
+ {
3286
+ title: paneTitle("projects", layout.sidebarWidth),
3287
+ focused,
3288
+ grow: true,
3289
+ children: /* @__PURE__ */ jsxs2(
3290
+ "scrollbox",
3291
+ {
3292
+ ref: projectScrollRef,
3293
+ flexGrow: 1,
3294
+ flexShrink: 1,
3295
+ minHeight: 0,
3296
+ style: paneScrollStyle,
3297
+ children: [
3298
+ projectItems.map((item, index) => /* @__PURE__ */ jsx2(
3299
+ ProjectRow,
3300
+ {
3301
+ item,
3302
+ selected: index === projectIndex,
3303
+ width: inner
3304
+ },
3305
+ item.project.id
3306
+ )),
3307
+ data !== void 0 && projectItems.length === 0 ? /* @__PURE__ */ jsx2(EmptyNote, { text: "none adopted yet" }) : null
3308
+ ]
3309
+ }
3310
+ )
3311
+ },
3312
+ entry.section
3313
+ );
3314
+ case "worktrees":
3315
+ return /* @__PURE__ */ jsx2(
3316
+ Pane,
3317
+ {
3318
+ title: paneTitle("worktrees", layout.sidebarWidth),
3319
+ focused,
3320
+ grow: true,
3321
+ children: /* @__PURE__ */ jsxs2(
3322
+ "scrollbox",
3323
+ {
3324
+ ref: worktreeScrollRef,
3325
+ flexGrow: 1,
3326
+ flexShrink: 1,
3327
+ minHeight: 0,
3328
+ style: paneScrollStyle,
3329
+ children: [
3330
+ worktreeGroups.map((group, index) => /* @__PURE__ */ jsx2(
3331
+ WorktreeRow,
3332
+ {
3333
+ group,
3334
+ selected: index === worktreeIndex,
3335
+ width: inner
3336
+ },
3337
+ group.key
3338
+ )),
3339
+ data !== void 0 && worktreeGroups.length === 0 ? /* @__PURE__ */ jsx2(EmptyNote, { text: "no worktrees \u2014 w starts one" }) : null
3340
+ ]
3341
+ }
3342
+ )
3343
+ },
3344
+ entry.section
3345
+ );
3346
+ case "sessions":
3347
+ return /* @__PURE__ */ jsx2(
3348
+ Pane,
3349
+ {
3350
+ title: paneTitle("sessions", layout.sidebarWidth),
3351
+ focused,
3352
+ grow: true,
3353
+ children: /* @__PURE__ */ jsxs2(
3354
+ "scrollbox",
3355
+ {
3356
+ ref: sessionScrollRef,
3357
+ flexGrow: 1,
3358
+ flexShrink: 1,
3359
+ minHeight: 0,
3360
+ style: paneScrollStyle,
3361
+ children: [
3362
+ sessionItems.map((item, index) => /* @__PURE__ */ jsx2(
3363
+ SessionRow,
3364
+ {
3365
+ item,
3366
+ selected: index === sessionIndex,
3367
+ width: inner
3368
+ },
3369
+ item.session.id
3370
+ )),
3371
+ data !== void 0 && sessionItems.length === 0 ? /* @__PURE__ */ jsx2(
3372
+ EmptyNote,
3373
+ {
3374
+ text: selectedGroup === null ? "no worktree selected" : "no sessions \u2014 n starts one"
3375
+ }
3376
+ ) : null,
3377
+ loadFailure === null ? null : /* @__PURE__ */ jsx2("text", { height: 1, fg: INK_2, bg: "transparent", children: ` ${loadFailure} \u2014 retrying` })
3378
+ ]
3379
+ }
3380
+ )
3381
+ },
3382
+ entry.section
3383
+ );
3384
+ }
3385
+ };
3386
+ const renderDetail = () => /* @__PURE__ */ jsx2(Pane, { title: paneTitle("detail", detailWidth + 2), focused: focus === "detail", grow: true, children: selectedItem === null ? /* @__PURE__ */ jsx2(EmptyNote, { text: "no session selected \u2014 n starts one" }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
3387
+ /* @__PURE__ */ jsx2(SessionFacts, { group: selectedGroup, item: selectedItem, rows: factRows }),
3388
+ showFactRule ? /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", fg: FAINT, children: ` ${"\u2500".repeat(Math.max(4, detailWidth - 4))}` }) : null,
3389
+ /* @__PURE__ */ jsx2("box", { flexGrow: 1, flexShrink: 1, minHeight: 0, flexDirection: "column", children: previewSessionId === null ? /* @__PURE__ */ jsx2(EmptyNote, { text: "provisioning \u2014 no record yet" }) : selectedSession?.harness === "shell" ? /* @__PURE__ */ jsx2(EmptyNote, { text: "shell \u2014 no conversation record; a attaches if live" }) : transcript.isPending ? /* @__PURE__ */ jsx2(EmptyNote, { text: "reading the record\u2026" }) : transcript.error === null ? previewView.lines.length === 0 ? /* @__PURE__ */ jsx2(EmptyNote, { text: "no conversation recorded yet" }) : previewView.lines.map((line, index) => /* @__PURE__ */ jsx2(PreviewRow, { line }, `${index}-${line.kind}`)) : /* @__PURE__ */ jsx2("text", { height: 1, fg: INK_2, bg: "transparent", children: ` could not read the record \u2014 ${errorText(transcript.error)}` }) })
3390
+ ] }) });
3391
+ const offNav = layout.offscreen.filter(isNavSection);
3392
+ const detailOffscreen = layout.offscreen.includes("detail");
3393
+ return /* @__PURE__ */ jsxs2("box", { flexGrow: 1, flexDirection: "column", backgroundColor: CANVAS, children: [
2624
3394
  /* @__PURE__ */ jsxs2("box", { height: 1, flexDirection: "row", justifyContent: "space-between", children: [
2625
3395
  /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
2626
3396
  /* @__PURE__ */ jsx2("span", { fg: INK, children: " mend" }),
@@ -2635,6 +3405,13 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2635
3405
  liveTotal,
2636
3406
  " live"
2637
3407
  ] }),
3408
+ gate.count() > 0 ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
3409
+ /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \xB7 " }),
3410
+ /* @__PURE__ */ jsxs2("span", { fg: INK_2, children: [
3411
+ gate.count(),
3412
+ " starting"
3413
+ ] })
3414
+ ] }) : null,
2638
3415
  shareState === "off" ? null : /* @__PURE__ */ jsxs2(Fragment2, { children: [
2639
3416
  /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \xB7 " }),
2640
3417
  /* @__PURE__ */ jsx2("span", { fg: shareState === "connected" ? MUTED : FAINT, children: shareState === "connected" ? "agent shared" : "agent share reconnecting" })
@@ -2642,70 +3419,69 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2642
3419
  ] }),
2643
3420
  /* @__PURE__ */ jsx2("text", { height: 1, fg: FAINT, bg: "transparent", children: `${ctx.config.url} ` })
2644
3421
  ] }),
3422
+ !layout.breadcrumb ? null : /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
3423
+ /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " " }),
3424
+ offNav.map((column, index) => /* @__PURE__ */ jsxs2("span", { children: [
3425
+ index > 0 ? /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \u25B8 " }) : null,
3426
+ /* @__PURE__ */ jsx2("span", { fg: index === offNav.length - 1 ? INK_2 : MUTED, children: fit(breadcrumbValue(column), 28) })
3427
+ ] }, column)),
3428
+ /* @__PURE__ */ jsx2("span", { fg: FAINT, children: detailOffscreen ? `${offNav.length > 0 ? " \xB7 " : ""}record hidden \xB7 \u2192 opens it` : " \u25B8" })
3429
+ ] }),
2645
3430
  /* @__PURE__ */ jsxs2("box", { flexGrow: 1, flexShrink: 1, minHeight: 0, flexDirection: "row", children: [
2646
- showProjectsPane ? /* @__PURE__ */ jsx2(Pane, { title: "projects", focused: focus === "projects", width: PROJECTS_PANE_WIDTH, children: /* @__PURE__ */ jsxs2(
2647
- "scrollbox",
2648
- {
2649
- ref: projectScrollRef,
2650
- flexGrow: 1,
2651
- flexShrink: 1,
2652
- minHeight: 0,
2653
- style: paneScrollStyle,
2654
- children: [
2655
- projectItems.map((item, index) => /* @__PURE__ */ jsx2(
2656
- ProjectRow,
2657
- {
2658
- item,
2659
- selected: index === projectIndex,
2660
- nameWidth
2661
- },
2662
- item.project.id
2663
- )),
2664
- data !== void 0 && projectItems.length === 0 ? /* @__PURE__ */ jsx2("text", { height: 1, fg: FAINT, bg: "transparent", children: " none adopted yet" }) : null
2665
- ]
2666
- }
2667
- ) }) : null,
2668
- /* @__PURE__ */ jsx2(Pane, { title: sessionsTitle, focused: focus === "sessions", grow: true, children: /* @__PURE__ */ jsxs2(
2669
- "scrollbox",
3431
+ layout.sidebarWidth === 0 ? null : /* @__PURE__ */ jsx2(
3432
+ "box",
2670
3433
  {
2671
- ref: sessionScrollRef,
2672
- flexGrow: 1,
2673
- flexShrink: 1,
3434
+ width: layout.sidebarWidth,
3435
+ flexShrink: 0,
2674
3436
  minHeight: 0,
2675
- style: paneScrollStyle,
2676
- children: [
2677
- rows.map(
2678
- (row, index) => row.kind === "worktree" ? /* @__PURE__ */ jsx2(
2679
- WorktreeHeaderRow,
2680
- {
2681
- group: row.group,
2682
- selected: index === rowIndex
2683
- },
2684
- rowKeyOf(row)
2685
- ) : /* @__PURE__ */ jsx2(
2686
- SessionChildRow,
2687
- {
2688
- item: row.item,
2689
- selected: index === rowIndex
2690
- },
2691
- rowKeyOf(row)
2692
- )
2693
- ),
2694
- data !== void 0 && rows.length === 0 ? /* @__PURE__ */ jsx2("text", { height: 1, fg: FAINT, bg: "transparent", children: " no sessions yet \u2014 n starts one" }) : null,
2695
- loadFailure !== null ? /* @__PURE__ */ jsx2("text", { height: 1, fg: INK_2, bg: "transparent", children: ` ${loadFailure} \u2014 retrying` }) : null
2696
- ]
3437
+ flexDirection: "column",
3438
+ backgroundColor: "transparent",
3439
+ children: layout.sections.map(renderSection)
2697
3440
  }
2698
- ) })
3441
+ ),
3442
+ detailWidth === 0 ? null : renderDetail()
2699
3443
  ] }),
2700
- picker !== null ? /* @__PURE__ */ jsx2(Pane, { title: pickerTitle, focused: true, height: 2 + pickerItems.length, children: pickerItems.map((item, index) => /* @__PURE__ */ jsx2(HarnessRow, { item, selected: index === pickerIndex }, String(item.harness))) }) : editing !== null ? /* @__PURE__ */ jsx2(
3444
+ picker === null ? null : /* @__PURE__ */ jsx2(
2701
3445
  "box",
2702
3446
  {
3447
+ position: "absolute",
3448
+ zIndex: 12,
3449
+ left: Math.max(1, Math.floor((terminalCols - Math.min(74, terminalCols - 4)) / 2)),
3450
+ top: Math.max(1, Math.floor((terminalRows - (2 + pickerItems.length)) / 2)),
3451
+ width: Math.min(74, terminalCols - 4),
3452
+ height: 2 + pickerItems.length,
2703
3453
  border: true,
2704
3454
  borderStyle: "rounded",
2705
- borderColor: COBALT,
3455
+ borderColor: ACCENT,
3456
+ title: ` ${pickerTitle} `,
3457
+ titleAlignment: "left",
3458
+ backgroundColor: SURFACE,
3459
+ flexDirection: "column",
3460
+ children: pickerItems.map((item, index) => /* @__PURE__ */ jsx2(
3461
+ HarnessRow,
3462
+ {
3463
+ item,
3464
+ selected: index === pickerIndex,
3465
+ background: SURFACE
3466
+ },
3467
+ String(item.harness)
3468
+ ))
3469
+ }
3470
+ ),
3471
+ editing === null ? null : /* @__PURE__ */ jsx2(
3472
+ "box",
3473
+ {
3474
+ position: "absolute",
3475
+ zIndex: 12,
3476
+ left: Math.max(1, Math.floor((terminalCols - Math.min(74, terminalCols - 4)) / 2)),
3477
+ top: Math.max(1, Math.floor((terminalRows - 3) / 2)),
3478
+ width: Math.min(74, terminalCols - 4),
3479
+ border: true,
3480
+ borderStyle: "rounded",
3481
+ borderColor: ACCENT,
2706
3482
  title: ` label \u2014 ${editing.harness} ${editing.id.slice(0, 8)} `,
2707
3483
  titleAlignment: "left",
2708
- backgroundColor: "transparent",
3484
+ backgroundColor: SURFACE,
2709
3485
  height: 3,
2710
3486
  flexShrink: 0,
2711
3487
  children: /* @__PURE__ */ jsx2(
@@ -2714,8 +3490,8 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2714
3490
  focused: true,
2715
3491
  value: editing.label ?? "",
2716
3492
  placeholder: "a few words for what this session is doing (empty clears)",
2717
- backgroundColor: "transparent",
2718
- focusedBackgroundColor: "transparent",
3493
+ backgroundColor: SURFACE,
3494
+ focusedBackgroundColor: SURFACE,
2719
3495
  textColor: INK,
2720
3496
  focusedTextColor: INK,
2721
3497
  placeholderColor: FAINT,
@@ -2727,8 +3503,8 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2727
3503
  }
2728
3504
  )
2729
3505
  }
2730
- ) : showDetail ? /* @__PURE__ */ jsx2(Pane, { title: detailTitle, focused: false, height: 6, children: /* @__PURE__ */ jsx2(SessionDetail, { item: selectedSession }) }) : null,
2731
- adoptOffer !== null ? /* @__PURE__ */ jsxs2(
3506
+ ),
3507
+ adoptOffer === null ? null : /* @__PURE__ */ jsxs2(
2732
3508
  "box",
2733
3509
  {
2734
3510
  position: "absolute",
@@ -2739,7 +3515,7 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2739
3515
  height: 7,
2740
3516
  border: true,
2741
3517
  borderStyle: "rounded",
2742
- borderColor: COBALT,
3518
+ borderColor: ACCENT,
2743
3519
  title: " adopt this repository URL? ",
2744
3520
  titleAlignment: "left",
2745
3521
  backgroundColor: SURFACE,
@@ -2755,15 +3531,15 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2755
3531
  /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " auth " }),
2756
3532
  ADOPT_AUTH_MODES.map((candidate, index) => /* @__PURE__ */ jsxs2("span", { children: [
2757
3533
  index > 0 ? /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \xB7 " }) : null,
2758
- /* @__PURE__ */ jsx2("span", { fg: index === adoptOffer.modeIndex ? COBALT : FAINT, children: index === adoptOffer.modeIndex ? `\u25B8 ${candidate.label}` : candidate.label })
3534
+ /* @__PURE__ */ jsx2("span", { fg: index === adoptOffer.modeIndex ? ACCENT : FAINT, children: index === adoptOffer.modeIndex ? `\u25B8 ${candidate.label}` : candidate.label })
2759
3535
  ] }, candidate.mode))
2760
3536
  ] }),
2761
3537
  /* @__PURE__ */ jsx2("text", { height: 1, bg: SURFACE, fg: FAINT, children: ` ${ADOPT_AUTH_MODES[adoptOffer.modeIndex]?.hint ?? ""}` }),
2762
3538
  /* @__PURE__ */ jsx2("text", { height: 1, bg: SURFACE, fg: FAINT, children: " enter adopt \xB7 \u2190\u2192 auth mode \xB7 esc not now" })
2763
3539
  ]
2764
3540
  }
2765
- ) : null,
2766
- creating !== null ? /* @__PURE__ */ jsxs2(
3541
+ ),
3542
+ creating === null ? null : /* @__PURE__ */ jsxs2(
2767
3543
  "box",
2768
3544
  {
2769
3545
  position: "absolute",
@@ -2774,7 +3550,7 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2774
3550
  height: creatingHeight,
2775
3551
  border: true,
2776
3552
  borderStyle: "rounded",
2777
- borderColor: COBALT,
3553
+ borderColor: ACCENT,
2778
3554
  title: ` new worktree \u2014 ${selectedProject?.project.name ?? "project"} `,
2779
3555
  titleAlignment: "left",
2780
3556
  backgroundColor: SURFACE,
@@ -2784,7 +3560,7 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2784
3560
  /* @__PURE__ */ jsxs2("text", { height: 1, bg: SURFACE, children: [
2785
3561
  /* @__PURE__ */ jsx2(Gutter, { selected: creating.step === "name" }),
2786
3562
  /* @__PURE__ */ jsx2("span", { fg: FAINT, children: "name " }),
2787
- creating.step !== "name" ? /* @__PURE__ */ jsx2("span", { fg: INK, children: creating.name === "" ? "auto" : creating.name }) : null,
3563
+ creating.step === "name" ? null : /* @__PURE__ */ jsx2("span", { fg: INK, children: creating.name === "" ? "auto" : creating.name }),
2788
3564
  creating.joins ? /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \xB7 joins the existing worktree" }) : null
2789
3565
  ] }),
2790
3566
  creating.step === "name" ? /* @__PURE__ */ jsx2(
@@ -2816,9 +3592,9 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2816
3592
  /* @__PURE__ */ jsxs2("text", { height: 1, bg: SURFACE, children: [
2817
3593
  /* @__PURE__ */ jsx2(Gutter, { selected: creating.step === "base" }),
2818
3594
  /* @__PURE__ */ jsx2("span", { fg: FAINT, children: "base " }),
2819
- creating.step !== "base" ? /* @__PURE__ */ jsx2("span", { fg: creating.step === "name" && !creating.joins ? FAINT : INK, children: creating.joins ? "fixed by the existing worktree" : creating.base ?? "default branch" }) : null
3595
+ creating.step === "base" ? null : /* @__PURE__ */ jsx2("span", { fg: creating.step === "name" && !creating.joins ? FAINT : INK, children: creating.joins ? "fixed by the existing worktree" : creating.base ?? "default branch" })
2820
3596
  ] }),
2821
- creating.step === "base" ? creating.branches === null ? /* @__PURE__ */ jsx2("text", { height: 1, bg: SURFACE, fg: FAINT, children: "reading branches\u2026" }) : /* @__PURE__ */ jsx2(
3597
+ creating.step === "base" && creating.branches !== null ? /* @__PURE__ */ jsx2(
2822
3598
  "input",
2823
3599
  {
2824
3600
  focused: true,
@@ -2845,7 +3621,17 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2845
3621
  const branch = creating.joins || creating.branches === null ? void 0 : filterBranches(creating.branches, creating.query)[index];
2846
3622
  const active = creating.step === "base";
2847
3623
  if (branch === void 0) {
2848
- return /* @__PURE__ */ jsx2("text", { height: 1, bg: SURFACE, fg: FAINT, children: index === 0 && active && creating.branches !== null ? " no branch matches \u2014 enter uses the default" : " " }, `slot-${index}`);
3624
+ const notice = creating.joins ? null : baseStepNotice(creating);
3625
+ return /* @__PURE__ */ jsx2(
3626
+ "text",
3627
+ {
3628
+ height: 1,
3629
+ bg: SURFACE,
3630
+ fg: creating.branchError === null ? FAINT : ERROR,
3631
+ children: index === 0 && notice !== null ? ` ${fit(notice, 66)}` : " "
3632
+ },
3633
+ `slot-${index}`
3634
+ );
2849
3635
  }
2850
3636
  const highlighted = active && index === creating.baseIndex;
2851
3637
  return /* @__PURE__ */ jsx2(
@@ -2859,7 +3645,7 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2859
3645
  /* @__PURE__ */ jsx2(Gutter, { selected: highlighted }),
2860
3646
  /* @__PURE__ */ jsx2("span", { fg: active ? INK : FAINT, children: branch.name.slice(0, 40).padEnd(41) }),
2861
3647
  /* @__PURE__ */ jsx2("span", { fg: FAINT, children: branch.sha.slice(0, 8).padEnd(10) }),
2862
- /* @__PURE__ */ jsx2("span", { fg: active ? MUTED : FAINT, children: timeAgo(branch.committedAt).replace(" ago", "").padEnd(6) }),
3648
+ /* @__PURE__ */ jsx2("span", { fg: active ? MUTED : FAINT, children: shortAge(branch.committedAt).padEnd(6) }),
2863
3649
  branch.isDefault ? /* @__PURE__ */ jsx2("span", { fg: FAINT, children: "default" }) : null
2864
3650
  ] })
2865
3651
  },
@@ -2891,13 +3677,13 @@ attached \xB7 ${session.harness} \xB7 ${short} \xB7 detach: Ctrl+]
2891
3677
  })
2892
3678
  ]
2893
3679
  }
2894
- ) : null,
2895
- /* @__PURE__ */ jsx2(StatusLine, { busy, busyStarted, status }),
3680
+ ),
3681
+ loadFailure === null ? /* @__PURE__ */ jsx2(StatusLine, { busy, busyStarted, status }) : /* @__PURE__ */ jsx2("text", { height: 1, fg: ERROR, bg: "transparent", children: ` could not load workbench \u2014 ${loadFailure} \xB7 retrying` }),
2896
3682
  /* @__PURE__ */ jsx2("text", { height: 1, fg: FAINT, bg: "transparent", children: footerText })
2897
3683
  ] });
2898
3684
  };
2899
3685
  var runDashboard = async (ctx) => {
2900
- const renderer = await createCliRenderer({ exitOnCtrlC: false });
3686
+ const renderer = await createCliRenderer({ exitOnCtrlC: false, backgroundColor: CANVAS });
2901
3687
  const queryClient = new QueryClient();
2902
3688
  const controller = new AbortController();
2903
3689
  let eventTimer = null;
@@ -2948,7 +3734,7 @@ var runDashboard = async (ctx) => {
2948
3734
  } catch {
2949
3735
  if (controller.signal.aborted) return;
2950
3736
  }
2951
- pendingFamilies.add("workbench").add("review");
3737
+ pendingFamilies.add("workbench").add("review").add("transcript");
2952
3738
  if (eventTimer === null && !controller.signal.aborted) {
2953
3739
  eventTimer = setTimeout(flushInvalidations, 250);
2954
3740
  }