@sealant/mend 0.36.0-next.684 → 0.36.0-next.721

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 (94) hide show
  1. package/dist/{chunk-DHM7VXTR.js → chunk-GF5A6W2Y.js} +12 -1
  2. package/dist/{dashboard-OBFWHSPD.js → dashboard-MPO6H5YF.js} +823 -176
  3. package/dist/main.js +1221 -84
  4. package/man/mend-accounts.1 +1 -1
  5. package/man/mend-adopt.1 +1 -1
  6. package/man/mend-attach.1 +1 -1
  7. package/man/mend-codex.1 +1 -1
  8. package/man/mend-completions.1 +1 -1
  9. package/man/mend-connect-pi.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-repo.1 +1 -1
  14. package/man/mend-dotfiles-sync.1 +1 -1
  15. package/man/mend-dotfiles.1 +1 -1
  16. package/man/mend-env-cluster.1 +1 -1
  17. package/man/mend-env-load.1 +1 -1
  18. package/man/mend-env-show.1 +1 -1
  19. package/man/mend-folder-create.1 +1 -1
  20. package/man/mend-folder-list.1 +1 -1
  21. package/man/mend-folder-push.1 +1 -1
  22. package/man/mend-folder-rm.1 +1 -1
  23. package/man/mend-git-author.1 +1 -1
  24. package/man/mend-help.1 +1 -1
  25. package/man/mend-invite.1 +1 -1
  26. package/man/mend-keys-init.1 +1 -1
  27. package/man/mend-keys-mode.1 +1 -1
  28. package/man/mend-keys-share.1 +1 -1
  29. package/man/mend-keys-show.1 +1 -1
  30. package/man/mend-land.1 +1 -1
  31. package/man/mend-login.1 +1 -1
  32. package/man/mend-logout.1 +1 -1
  33. package/man/mend-logs.1 +1 -1
  34. package/man/mend-man.1 +1 -1
  35. package/man/mend-members.1 +1 -1
  36. package/man/mend-memory-import.1 +1 -1
  37. package/man/mend-memory-rm.1 +1 -1
  38. package/man/mend-memory-show.1 +1 -1
  39. package/man/mend-memory.1 +1 -1
  40. package/man/mend-models.1 +1 -1
  41. package/man/mend-operator-exposure.1 +2 -2
  42. package/man/mend-operator-gate.1 +1 -1
  43. package/man/mend-operator-grant-owner.1 +1 -1
  44. package/man/mend-operator-org-create.1 +1 -1
  45. package/man/mend-operator-org-invite-owner.1 +1 -1
  46. package/man/mend-operator-org-list.1 +1 -1
  47. package/man/mend-operator-org-rename.1 +1 -1
  48. package/man/mend-operator-reset-link.1 +1 -1
  49. package/man/mend-pair.1 +1 -1
  50. package/man/mend-projects.1 +1 -1
  51. package/man/mend-pull.1 +1 -1
  52. package/man/mend-refresh.1 +1 -1
  53. package/man/mend-rejoin.1 +1 -1
  54. package/man/mend-resume.1 +1 -1
  55. package/man/mend-run.1 +1 -1
  56. package/man/mend-secrets-add.1 +1 -1
  57. package/man/mend-secrets-rm.1 +1 -1
  58. package/man/mend-secrets.1 +1 -1
  59. package/man/mend-server-logs.1 +1 -1
  60. package/man/mend-server-restart.1 +1 -1
  61. package/man/mend-server-setup.1 +37 -6
  62. package/man/mend-server-start.1 +1 -1
  63. package/man/mend-server-status.1 +1 -1
  64. package/man/mend-server-stop.1 +1 -1
  65. package/man/mend-server-upgrade.1 +1 -1
  66. package/man/mend-server.1 +1 -1
  67. package/man/mend-service-add.1 +1 -1
  68. package/man/mend-service-connect.1 +1 -1
  69. package/man/mend-service-init.1 +1 -1
  70. package/man/mend-service-list.1 +1 -1
  71. package/man/mend-service-logs.1 +1 -1
  72. package/man/mend-service-restart.1 +1 -1
  73. package/man/mend-service-run.1 +1 -1
  74. package/man/mend-service-stop.1 +1 -1
  75. package/man/mend-session-share.1 +1 -1
  76. package/man/mend-sessions.1 +1 -1
  77. package/man/mend-shell.1 +1 -1
  78. package/man/mend-skills-push.1 +1 -1
  79. package/man/mend-skills.1 +1 -1
  80. package/man/mend-snake.1 +2 -2
  81. package/man/mend-ssh-keys-remove.1 +1 -1
  82. package/man/mend-ssh-keys.1 +1 -1
  83. package/man/mend-ssh-setup.1 +1 -1
  84. package/man/mend-ssh.1 +1 -1
  85. package/man/mend-stop.1 +1 -1
  86. package/man/mend-ui.1 +3 -1
  87. package/man/mend-uninstall.1 +1 -1
  88. package/man/mend-version.1 +1 -1
  89. package/man/mend-wait.1 +1 -1
  90. package/man/mend-workspace-replace.1 +1 -1
  91. package/man/mend-worktrees-rm.1 +1 -1
  92. package/man/mend-worktrees.1 +1 -1
  93. package/man/mend.1 +2 -2
  94. package/package.json +2 -2
@@ -17,6 +17,8 @@ import {
17
17
  normalizeProjectName,
18
18
  pendingId,
19
19
  repositoryCloneUrlIssue,
20
+ serviceViewHostPort,
21
+ serviceViewStatus,
20
22
  servicesHoldLine,
21
23
  sessionWorkspaceLines,
22
24
  withoutLaunchPhase,
@@ -24,7 +26,7 @@ import {
24
26
  workspaceReadsOf,
25
27
  worktreeJoinLine,
26
28
  wrapWords
27
- } from "./chunk-DHM7VXTR.js";
29
+ } from "./chunk-GF5A6W2Y.js";
28
30
 
29
31
  // src/dashboard.tsx
30
32
  import { CliRenderEvents, createCliRenderer } from "@opentui/core";
@@ -60,6 +62,15 @@ var submitDashboardAdoption = async (api, input) => {
60
62
  };
61
63
 
62
64
  // src/dashboard-model.ts
65
+ var serviceRowOf = (view) => ({
66
+ id: view.service.id,
67
+ sessionId: view.service.sessionId,
68
+ label: view.service.name,
69
+ status: serviceViewStatus(view),
70
+ workspacePort: view.service.workspacePort,
71
+ protocol: view.service.transport,
72
+ hostPort: serviceViewHostPort(view)
73
+ });
63
74
  var worktreeName = (branch) => branch.startsWith("mend/") ? branch.slice("mend/".length) : branch;
64
75
  var worktreeDisplayName = (session) => {
65
76
  const name = worktreeName(session.branch);
@@ -76,7 +87,7 @@ var fetchWorkbench = async (ctx) => {
76
87
  ctx.api("GET", "/services")
77
88
  ]);
78
89
  const servicesBySession = /* @__PURE__ */ new Map();
79
- for (const service of services) {
90
+ for (const service of services.map(serviceRowOf)) {
80
91
  const bucket = servicesBySession.get(service.sessionId) ?? [];
81
92
  bucket.push(service);
82
93
  servicesBySession.set(service.sessionId, bucket);
@@ -205,7 +216,7 @@ var pseudoGroup = (session, item) => ({
205
216
  live: LIVE_STATUSES.has(session.status) ? 1 : 0,
206
217
  annotation: item(session).annotation
207
218
  });
208
- var deriveHarnesses = (resuming) => {
219
+ var deriveHarnesses = (resuming, joining = null) => {
209
220
  if (resuming !== null) {
210
221
  const others = Object.keys(HARNESS_COMMANDS).filter((h) => h !== resuming.harness);
211
222
  return [
@@ -223,11 +234,12 @@ var deriveHarnesses = (resuming) => {
223
234
  )
224
235
  ];
225
236
  }
237
+ const where = joining === null ? "new worktree" : `joins ${joining}`;
226
238
  return Object.keys(HARNESS_COMMANDS).map(
227
239
  (harness) => ({
228
240
  harness,
229
241
  label: harness,
230
- hint: harness === "shell" ? "a plain bash session \xB7 new worktree, recorded" : `mend ${harness} \xB7 new worktree, recorded session`
242
+ hint: harness === "shell" ? `a plain bash session \xB7 ${where}, recorded` : `mend ${harness} \xB7 ${where}, recorded session`
231
243
  })
232
244
  );
233
245
  };
@@ -487,8 +499,35 @@ var advanceFromBase = (current) => {
487
499
  var COLUMNS = ["projects", "worktrees", "sessions", "detail"];
488
500
  var NAV_SECTIONS = ["projects", "worktrees", "sessions"];
489
501
  var isNavSection = (column) => column !== "detail";
502
+ var PANE_NUMBER = {
503
+ projects: "1",
504
+ worktrees: "2",
505
+ sessions: "3",
506
+ detail: "0"
507
+ };
508
+ var paneForDigit = (digit) => COLUMNS.find((column) => PANE_NUMBER[column] === digit) ?? null;
509
+ var numberedTitle = (column, title) => `[${PANE_NUMBER[column]}] ${title}`;
510
+ var cyclePane = (focus, delta) => {
511
+ const at = COLUMNS.indexOf(focus);
512
+ if (at === -1) return "sessions";
513
+ const next = ((at + delta) % COLUMNS.length + COLUMNS.length) % COLUMNS.length;
514
+ return COLUMNS[next] ?? focus;
515
+ };
516
+ var backFrom = (focus, lastNav) => focus === "detail" ? lastNav : stepColumn(focus, -1);
517
+ var SCREEN_MODES = ["normal", "half", "full"];
518
+ var stepScreenMode = (mode, delta) => {
519
+ const at = SCREEN_MODES.indexOf(mode);
520
+ const next = ((at + delta) % SCREEN_MODES.length + SCREEN_MODES.length) % SCREEN_MODES.length;
521
+ return SCREEN_MODES[next] ?? mode;
522
+ };
523
+ var matchesFilter = (query, ...fields) => {
524
+ const needle = query.trim().toLowerCase();
525
+ if (needle === "") return true;
526
+ return fields.some((field) => field !== null && field.toLowerCase().includes(needle));
527
+ };
490
528
  var expandedSection = (focus, lastNav) => isNavSection(focus) ? focus : lastNav;
491
529
  var SIDEBAR_SHARE = 0.25;
530
+ var HALF_SIDEBAR_SHARE = 0.5;
492
531
  var MIN_SIDEBAR_WIDTH = 24;
493
532
  var MIN_DETAIL_WIDTH = 48;
494
533
  var SPLIT_MIN_WIDTH = MIN_SIDEBAR_WIDTH + MIN_DETAIL_WIDTH;
@@ -504,13 +543,11 @@ var stackSections = (body, open) => {
504
543
  (section) => section === open ? { section, expanded: true, height: room, rows: room - 2 } : { section, expanded: false, height: MIN_SECTION_HEIGHT, rows: 1 }
505
544
  );
506
545
  };
507
- var planLayout = (width, height, focus, lastNav) => {
508
- const split = width >= SPLIT_MIN_WIDTH;
546
+ var planLayout = (width, height, focus, lastNav, mode = "normal") => {
547
+ const split = mode !== "full" && width >= SPLIT_MIN_WIDTH;
509
548
  const navVisible = split || isNavSection(focus);
510
- const sidebarOuter = split ? Math.max(
511
- MIN_SIDEBAR_WIDTH,
512
- Math.min(width - MIN_DETAIL_WIDTH, Math.round(width * SIDEBAR_SHARE))
513
- ) : navVisible ? Math.max(4, width) : 0;
549
+ const share = mode === "half" ? HALF_SIDEBAR_SHARE : SIDEBAR_SHARE;
550
+ const sidebarOuter = split ? Math.max(MIN_SIDEBAR_WIDTH, Math.min(width - MIN_DETAIL_WIDTH, Math.round(width * share))) : navVisible ? Math.max(4, width) : 0;
514
551
  const detailOuter = split ? width - sidebarOuter : navVisible ? 0 : Math.max(4, width);
515
552
  const build = (body2, breadcrumb) => {
516
553
  const sections = navVisible ? stackSections(body2, expandedSection(focus, lastNav)) : [];
@@ -593,6 +630,11 @@ var worktreeJoinRows = (data, projectId, worktree, viewerId, width) => {
593
630
  const line = worktreeJoinLine(detail.sessions, id, viewerId);
594
631
  return line === null ? [] : wrapWords(line, width);
595
632
  };
633
+ var KEY_GROUP_TITLE = {
634
+ navigate: "move around",
635
+ session: "act on the selection",
636
+ dashboard: "the dashboard"
637
+ };
596
638
  var KEY_BINDINGS = [
597
639
  {
598
640
  verb: "moveUp",
@@ -603,36 +645,123 @@ var KEY_BINDINGS = [
603
645
  worktrees: "\u2191\u2193 move",
604
646
  sessions: "\u2191\u2193 move",
605
647
  detail: "\u2191\u2193 scroll"
606
- }
648
+ },
649
+ help: "up the list, or back through the record",
650
+ group: "navigate"
651
+ },
652
+ {
653
+ verb: "moveDown",
654
+ keys: ["down", "j"],
655
+ shift: false,
656
+ hints: {},
657
+ help: "down the list, or toward the newest line",
658
+ group: "navigate"
659
+ },
660
+ { verb: "pageUp", keys: ["pageup"], shift: false, hints: {}, help: "up ten", group: "navigate" },
661
+ {
662
+ verb: "pageDown",
663
+ keys: ["pagedown"],
664
+ shift: false,
665
+ hints: {},
666
+ help: "down ten",
667
+ group: "navigate"
668
+ },
669
+ {
670
+ verb: "jumpProjects",
671
+ keys: ["1"],
672
+ shift: false,
673
+ hints: {},
674
+ help: "jump to [1] projects",
675
+ group: "navigate"
676
+ },
677
+ {
678
+ verb: "jumpWorktrees",
679
+ keys: ["2"],
680
+ shift: false,
681
+ hints: {},
682
+ help: "jump to [2] worktrees",
683
+ group: "navigate"
684
+ },
685
+ {
686
+ verb: "jumpSessions",
687
+ keys: ["3"],
688
+ shift: false,
689
+ hints: {},
690
+ help: "jump to [3] sessions",
691
+ group: "navigate"
692
+ },
693
+ {
694
+ verb: "jumpDetail",
695
+ keys: ["0"],
696
+ shift: false,
697
+ hints: {},
698
+ help: "jump to [0], the session pane",
699
+ group: "navigate"
607
700
  },
608
- { verb: "moveDown", keys: ["down", "j"], shift: false, hints: {} },
609
- { verb: "pageUp", keys: ["pageup"], shift: false, hints: {} },
610
- { verb: "pageDown", keys: ["pagedown"], shift: false, hints: {} },
611
701
  {
612
702
  verb: "columnRight",
613
- keys: ["return", "linefeed", "l", "right", "tab"],
703
+ keys: ["return", "linefeed", "l", "right"],
614
704
  shift: false,
615
- hints: { projects: "\u2192 worktrees", worktrees: "\u2190\u2192 panes", sessions: "\u2190\u2192 panes" }
705
+ hints: { projects: "\u2192 worktrees", worktrees: "\u2190\u2192 panes", sessions: "\u2190\u2192 panes" },
706
+ help: "drill in: projects, worktrees, sessions, then [0]",
707
+ group: "navigate"
616
708
  },
617
709
  {
618
710
  verb: "columnLeft",
619
711
  keys: ["left", "h", "-", "backspace"],
620
712
  shift: false,
621
- hints: { detail: "\u2190 sessions" }
713
+ hints: { detail: "\u2190 sessions" },
714
+ help: "one pane back up the hierarchy",
715
+ group: "navigate"
716
+ },
717
+ {
718
+ verb: "nextPane",
719
+ keys: ["tab"],
720
+ shift: false,
721
+ hints: {},
722
+ help: "next pane, round to the first after the last",
723
+ group: "navigate"
724
+ },
725
+ { verb: "prevPane", keys: ["backtab"], shift: "any", hints: {} },
726
+ {
727
+ verb: "prevPane",
728
+ keys: ["tab"],
729
+ shift: true,
730
+ hints: {},
731
+ help: "previous pane, round to the last before the first",
732
+ group: "navigate"
733
+ },
734
+ {
735
+ verb: "back",
736
+ keys: ["escape"],
737
+ shift: false,
738
+ hints: {},
739
+ help: "clear the filter, else back to where you came from",
740
+ group: "navigate"
741
+ },
742
+ {
743
+ verb: "filter",
744
+ keys: ["/"],
745
+ shift: false,
746
+ hints: {},
747
+ help: "filter this list; enter keeps the filter, esc clears it",
748
+ group: "navigate"
622
749
  },
623
- { verb: "columnLeft", keys: ["backtab"], shift: "any", hints: {} },
624
- { verb: "columnLeft", keys: ["tab"], shift: true, hints: {} },
625
750
  {
626
751
  verb: "attach",
627
752
  keys: ["a"],
628
753
  shift: false,
629
- hints: { worktrees: "a attach", sessions: "a attach", detail: "a attach" }
754
+ hints: { worktrees: "a attach", sessions: "a attach", detail: "a attach" },
755
+ help: "attach this terminal to the live session",
756
+ group: "session"
630
757
  },
631
758
  {
632
759
  verb: "resume",
633
760
  keys: ["r"],
634
761
  shift: false,
635
- hints: { worktrees: "r resume", sessions: "r resume", detail: "r resume" }
762
+ hints: { worktrees: "r resume", sessions: "r resume", detail: "r resume" },
763
+ help: "resume the settled session, on a harness you pick",
764
+ group: "session"
636
765
  },
637
766
  {
638
767
  verb: "newSession",
@@ -642,60 +771,166 @@ var KEY_BINDINGS = [
642
771
  projects: "n/w new worktree",
643
772
  worktrees: "n new session",
644
773
  sessions: "n new session"
645
- }
774
+ },
775
+ help: "another session in this worktree, or a new worktree",
776
+ group: "session"
646
777
  },
647
778
  {
648
779
  verb: "newWorktree",
649
780
  keys: ["w"],
650
781
  shift: false,
651
- hints: { worktrees: "w new worktree", sessions: "w new worktree" }
782
+ hints: { worktrees: "w new worktree", sessions: "w new worktree" },
783
+ help: "a new worktree in this project",
784
+ group: "session"
652
785
  },
653
786
  {
654
787
  verb: "stop",
655
788
  keys: ["k"],
656
789
  shift: true,
657
- hints: { worktrees: "\u21E7K stop all", sessions: "\u21E7K stop" }
790
+ hints: { worktrees: "\u21E7K stop all", sessions: "\u21E7K stop" },
791
+ help: "stop it, or every live session in [2] (press twice)",
792
+ group: "session"
658
793
  },
659
794
  { verb: "stop", keys: ["x"], shift: false, hints: {} },
660
795
  {
661
796
  verb: "remove",
662
797
  keys: ["d"],
663
798
  shift: true,
664
- hints: { worktrees: "\u21E7D remove worktree", sessions: "\u21E7D remove" }
799
+ hints: { worktrees: "\u21E7D remove worktree", sessions: "\u21E7D remove" },
800
+ help: "remove it, or the worktree from [2] (press twice)",
801
+ group: "session"
665
802
  },
666
803
  {
667
804
  verb: "review",
668
805
  keys: ["v"],
669
806
  shift: false,
670
- hints: { worktrees: "v review", sessions: "v review", detail: "v review" }
807
+ hints: { worktrees: "v review", sessions: "v review", detail: "v review" },
808
+ help: "review the change in this terminal",
809
+ group: "session"
671
810
  },
672
811
  {
673
812
  verb: "rename",
674
813
  keys: ["e"],
675
814
  shift: false,
676
- hints: { sessions: "e rename", detail: "e rename" }
815
+ hints: { sessions: "e rename", detail: "e rename" },
816
+ help: "label the session; empty clears it",
817
+ group: "session"
818
+ },
819
+ {
820
+ verb: "openWeb",
821
+ keys: ["o"],
822
+ shift: false,
823
+ hints: { sessions: "o web", detail: "o web" },
824
+ help: "open the session in the browser",
825
+ group: "session"
677
826
  },
678
- { verb: "openWeb", keys: ["o"], shift: false, hints: { sessions: "o web", detail: "o web" } },
679
827
  {
680
828
  verb: "refresh",
681
829
  keys: ["r"],
682
830
  shift: true,
683
- hints: { projects: "\u21E7R refresh", worktrees: "\u21E7R refresh", detail: "\u21E7R refresh" }
831
+ hints: { projects: "\u21E7R refresh", worktrees: "\u21E7R refresh", detail: "\u21E7R refresh" },
832
+ help: "read everything from the server again",
833
+ group: "dashboard"
834
+ },
835
+ {
836
+ verb: "nextScreenMode",
837
+ keys: ["+"],
838
+ shift: "any",
839
+ hints: {},
840
+ help: "screen mode: normal, half, full",
841
+ group: "dashboard"
842
+ },
843
+ {
844
+ verb: "prevScreenMode",
845
+ keys: ["_"],
846
+ shift: "any",
847
+ hints: {},
848
+ help: "screen mode, the other way",
849
+ group: "dashboard"
850
+ },
851
+ {
852
+ verb: "help",
853
+ keys: ["?"],
854
+ shift: "any",
855
+ hints: {},
856
+ help: "this list",
857
+ group: "dashboard",
858
+ footer: "? keys"
684
859
  },
685
860
  {
686
861
  verb: "quit",
687
862
  keys: ["q"],
688
863
  shift: false,
689
- hints: { projects: "q quit", worktrees: "q quit", sessions: "q quit", detail: "q quit" }
864
+ hints: { projects: "q quit", worktrees: "q quit", sessions: "q quit", detail: "q quit" },
865
+ help: "quit; sessions keep running",
866
+ group: "dashboard"
690
867
  }
691
868
  ];
869
+ var SYMBOL_STROKES = /* @__PURE__ */ new Set(["?", "+", "_"]);
870
+ var strokeName = (key) => {
871
+ const sequence = key.sequence ?? "";
872
+ return SYMBOL_STROKES.has(sequence) ? sequence : key.name ?? "";
873
+ };
692
874
  var verbForKey = (name, shift) => KEY_BINDINGS.find(
693
875
  (binding) => binding.keys.includes(name) && (binding.shift === "any" || binding.shift === shift)
694
876
  )?.verb ?? null;
877
+ var gameSwallows = (verb) => verb !== null;
695
878
  var verbHints = (focus) => KEY_BINDINGS.flatMap((binding) => {
696
879
  const hint = binding.hints[focus];
697
880
  return hint === void 0 ? [] : [hint];
698
881
  });
882
+ var FOOTER_TAIL = KEY_BINDINGS.flatMap(
883
+ (binding) => binding.footer === void 0 ? [] : [binding.footer]
884
+ );
885
+ var footerHints = (focus, width) => {
886
+ const tail = FOOTER_TAIL.map((hint) => ` \xB7 ${hint}`).join("");
887
+ return `${fitHints(verbHints(focus), Math.max(1, width - tail.length))}${tail}`;
888
+ };
889
+ var KEY_LABEL = {
890
+ up: "\u2191",
891
+ down: "\u2193",
892
+ left: "\u2190",
893
+ right: "\u2192",
894
+ return: "enter",
895
+ tab: "tab",
896
+ backtab: "\u21E7tab",
897
+ escape: "esc",
898
+ backspace: "\u232B",
899
+ pageup: "PgUp",
900
+ pagedown: "PgDn"
901
+ };
902
+ var keyLabel = (key, shift) => {
903
+ const base = KEY_LABEL[key] ?? key;
904
+ return shift === true ? `\u21E7${base.length === 1 ? base.toUpperCase() : base}` : base;
905
+ };
906
+ var helpSections = (focus) => {
907
+ const rows = /* @__PURE__ */ new Map();
908
+ for (const binding of KEY_BINDINGS) {
909
+ const entry = rows.get(binding.verb) ?? { keys: [], help: "", group: null };
910
+ for (const key of binding.keys) {
911
+ if (key === "linefeed") continue;
912
+ const label = keyLabel(key, binding.shift);
913
+ if (!entry.keys.includes(label)) entry.keys.push(label);
914
+ }
915
+ if (entry.help === "" && binding.help !== void 0) entry.help = binding.help;
916
+ if (entry.group === null && binding.group !== void 0) entry.group = binding.group;
917
+ rows.set(binding.verb, entry);
918
+ }
919
+ const all = [...rows].map(([verb, entry]) => ({
920
+ verb,
921
+ keys: entry.keys.join(" "),
922
+ help: entry.help,
923
+ group: entry.group ?? "dashboard",
924
+ here: KEY_BINDINGS.some(
925
+ (binding) => binding.verb === verb && binding.hints[focus] !== void 0
926
+ )
927
+ }));
928
+ const groups = ["navigate", "session", "dashboard"];
929
+ return groups.flatMap((group) => {
930
+ const ordered = all.filter((row) => row.group === group);
931
+ return ordered.length === 0 ? [] : [{ title: KEY_GROUP_TITLE[group], rows: ordered.map(({ group: _group, ...row }) => row) }];
932
+ });
933
+ };
699
934
 
700
935
  // src/dashboard-preview.ts
701
936
  import { stripVTControlCharacters } from "node:util";
@@ -2146,9 +2381,6 @@ function ReviewScreen({
2146
2381
  );
2147
2382
  }
2148
2383
 
2149
- // src/snake.tsx
2150
- import { useEffect as useEffect2, useState as useState2 } from "react";
2151
-
2152
2384
  // src/snake-game.ts
2153
2385
  var OPPOSITE = {
2154
2386
  up: "down",
@@ -2230,94 +2462,294 @@ var render = (game) => {
2230
2462
  return rows.map((row) => row.join(""));
2231
2463
  };
2232
2464
 
2465
+ // src/snake-play.ts
2466
+ var TICK_MS = 140;
2467
+ var COUNTDOWN_STEP_MS = 1e3;
2468
+ var COUNTDOWN = { kind: "countdown", step: 3 };
2469
+ var RUNNING = { kind: "running" };
2470
+ var PAUSED = { kind: "paused" };
2471
+ var READY = { kind: "ready" };
2472
+ var newSnakePlay = (width, height, random) => ({
2473
+ game: newSnakeGame(width, height, random),
2474
+ clock: READY,
2475
+ focused: false,
2476
+ width,
2477
+ height
2478
+ });
2479
+ var countDown = (play, random) => ({
2480
+ ...play,
2481
+ game: play.game.over ? newSnakeGame(play.width, play.height, random) : play.game,
2482
+ clock: COUNTDOWN
2483
+ });
2484
+ var reduceSnake = (play, event, random) => {
2485
+ switch (event.type) {
2486
+ case "focus":
2487
+ return play.focused ? play : { ...countDown(play, random), focused: true };
2488
+ case "blur": {
2489
+ if (!play.focused) return play;
2490
+ const clock = play.clock.kind === "running" ? PAUSED : play.clock.kind === "countdown" ? READY : play.clock;
2491
+ return { ...play, focused: false, clock };
2492
+ }
2493
+ case "resize": {
2494
+ if (event.width === play.width && event.height === play.height) return play;
2495
+ return {
2496
+ ...play,
2497
+ width: event.width,
2498
+ height: event.height,
2499
+ game: newSnakeGame(event.width, event.height, random),
2500
+ clock: play.clock.kind === "running" ? COUNTDOWN : play.clock
2501
+ };
2502
+ }
2503
+ case "count": {
2504
+ if (!play.focused || play.clock.kind !== "countdown") return play;
2505
+ const { step } = play.clock;
2506
+ return {
2507
+ ...play,
2508
+ clock: step === 0 ? RUNNING : { kind: "countdown", step: step === 3 ? 2 : step === 2 ? 1 : 0 }
2509
+ };
2510
+ }
2511
+ case "tick": {
2512
+ if (!play.focused || play.clock.kind !== "running") return play;
2513
+ const game = tick(play.game, random);
2514
+ return { ...play, game, clock: game.over ? READY : play.clock };
2515
+ }
2516
+ case "steer": {
2517
+ if (!play.focused) return play;
2518
+ if (play.game.over) return countDown(play, random);
2519
+ const game = turn(play.game, event.direction);
2520
+ return play.clock.kind === "running" || play.clock.kind === "countdown" ? { ...play, game } : { ...play, game, clock: COUNTDOWN };
2521
+ }
2522
+ case "togglePause":
2523
+ if (!play.focused) return play;
2524
+ return play.clock.kind === "running" || play.clock.kind === "countdown" ? { ...play, clock: PAUSED } : countDown(play, random);
2525
+ case "play":
2526
+ if (!play.focused || play.clock.kind === "running" || play.clock.kind === "countdown") {
2527
+ return play;
2528
+ }
2529
+ return countDown(play, random);
2530
+ }
2531
+ };
2532
+ var KEY_DIRECTIONS = {
2533
+ up: "up",
2534
+ down: "down",
2535
+ left: "left",
2536
+ right: "right",
2537
+ k: "up",
2538
+ j: "down",
2539
+ h: "left",
2540
+ l: "right"
2541
+ };
2542
+ var snakeKey = (name) => {
2543
+ const direction = KEY_DIRECTIONS[name];
2544
+ if (direction !== void 0) return { type: "steer", direction };
2545
+ if (name === "space" || name === "p") return { type: "togglePause" };
2546
+ if (name === "return" || name === "linefeed") return { type: "play" };
2547
+ if (name === "escape" || name === "q") return { type: "leave" };
2548
+ return null;
2549
+ };
2550
+ var snakeHints = (play) => play.game.over ? "enter/arrows again \xB7 esc/q leave" : play.clock.kind === "paused" || play.clock.kind === "ready" ? "enter/space resume \xB7 esc/q leave" : "\u2190\u2191\u2193\u2192/hjkl steer \xB7 space/p pause \xB7 esc/q leave";
2551
+ var SEGMENTS = {
2552
+ "1": "bc",
2553
+ "2": "abdeg",
2554
+ "3": "abcdg",
2555
+ G: "acdef",
2556
+ O: "abcdef"
2557
+ };
2558
+ var glyphPixels = (char, length, thickness) => {
2559
+ const lit = new Set(SEGMENTS[char] ?? "");
2560
+ const on = (...segments) => segments.some((segment) => lit.has(segment));
2561
+ const width = length + 2 * thickness;
2562
+ const height = 2 * length + 3 * thickness;
2563
+ const column = (x) => x < thickness ? "left" : x < thickness + length ? "middle" : "right";
2564
+ const row = (y) => y < thickness ? "top" : y < thickness + length ? "upper" : y < 2 * thickness + length ? "centre" : y < 2 * thickness + 2 * length ? "lower" : "bottom";
2565
+ const pixel = (x, y) => {
2566
+ const where = `${column(x)} ${row(y)}`;
2567
+ switch (where) {
2568
+ case "middle top":
2569
+ return on("a");
2570
+ case "middle centre":
2571
+ return on("g");
2572
+ case "middle bottom":
2573
+ return on("d");
2574
+ case "left upper":
2575
+ return on("f");
2576
+ case "right upper":
2577
+ return on("b");
2578
+ case "left lower":
2579
+ return on("e");
2580
+ case "right lower":
2581
+ return on("c");
2582
+ case "left top":
2583
+ return on("a", "f");
2584
+ case "right top":
2585
+ return on("a", "b");
2586
+ case "left centre":
2587
+ return on("f", "e", "g");
2588
+ case "right centre":
2589
+ return on("b", "c", "g");
2590
+ case "left bottom":
2591
+ return on("e", "d");
2592
+ case "right bottom":
2593
+ return on("c", "d");
2594
+ default:
2595
+ return false;
2596
+ }
2597
+ };
2598
+ return Array.from(
2599
+ { length: height },
2600
+ (_row, y) => Array.from({ length: width }, (_cell, x) => pixel(x, y))
2601
+ );
2602
+ };
2603
+ var halfBlocks = (pixels) => {
2604
+ const rows = [];
2605
+ for (let y = 0; y < pixels.length; y += 2) {
2606
+ const top = pixels[y] ?? [];
2607
+ const bottom = pixels[y + 1] ?? [];
2608
+ rows.push(
2609
+ top.map((upper, x) => {
2610
+ const lower = bottom[x] === true;
2611
+ return upper && lower ? "\u2588" : upper ? "\u2580" : lower ? "\u2584" : " ";
2612
+ }).join("")
2613
+ );
2614
+ }
2615
+ return rows;
2616
+ };
2617
+ var drawLabel = (label, length, thickness) => {
2618
+ const glyphs = [...label].map((char) => glyphPixels(char, length, thickness));
2619
+ const gap = Array.from({ length: thickness }, () => false);
2620
+ const height = 2 * length + 3 * thickness;
2621
+ const pixels = Array.from(
2622
+ { length: height },
2623
+ (_, y) => glyphs.flatMap((glyph, index) => index === 0 ? glyph[y] : [...gap, ...glyph[y]])
2624
+ );
2625
+ const lit = (x) => pixels.some((line) => line[x] === true);
2626
+ const width = pixels[0]?.length ?? 0;
2627
+ let first = 0;
2628
+ while (first < width && !lit(first)) first += 1;
2629
+ let last = width - 1;
2630
+ while (last > first && !lit(last)) last -= 1;
2631
+ return halfBlocks(pixels.map((line) => line.slice(first, last + 1)));
2632
+ };
2633
+ var countdownLabel = (step) => step === 0 ? "GO" : String(step);
2634
+ var MAX_SEGMENT = 12;
2635
+ var countdownArt = (label, cols, rows) => {
2636
+ const marginRows = Math.floor(rows / 8);
2637
+ const marginCols = cols >= 12 ? 1 : 0;
2638
+ const fitRows = rows - 2 * marginRows;
2639
+ const fitCols = cols - 2 * marginCols;
2640
+ for (let length = MAX_SEGMENT; length >= 1; length -= 1) {
2641
+ const thickness = length >= 5 ? 2 : 1;
2642
+ const art = drawLabel(label, length, thickness);
2643
+ if (art.length <= fitRows && (art[0]?.length ?? 0) <= fitCols) return art;
2644
+ }
2645
+ return label.length <= cols ? [label] : [label.slice(0, Math.max(1, cols))];
2646
+ };
2647
+ var overlayArt = (board, art) => {
2648
+ const height = board.length;
2649
+ const width = [...board[0] ?? ""].length;
2650
+ const artWidth = Math.max(0, ...art.map((line) => [...line].length));
2651
+ const top = Math.floor((height - art.length) / 2);
2652
+ const left = Math.floor((width - artWidth) / 2);
2653
+ return board.map((line, y) => {
2654
+ const artLine = art[y - top];
2655
+ const artCells = artLine === void 0 ? [] : [...artLine];
2656
+ return [...line].map((cell, x) => {
2657
+ const char = artCells[x - left];
2658
+ return { board: cell, art: char === void 0 || char === " " ? null : char };
2659
+ });
2660
+ });
2661
+ };
2662
+
2233
2663
  // src/snake.tsx
2664
+ import { useEffect as useEffect2, useState as useState2 } from "react";
2234
2665
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "@opentui/react/jsx-runtime";
2235
- var TICK_MS = 140;
2236
2666
  var useSnake = (options) => {
2237
- const { width, height, enabled } = options;
2238
- const [game, setGame] = useState2(() => newSnakeGame(width, height, Math.random));
2239
- const [paused, setPaused] = useState2(false);
2667
+ const { width, height, focused } = options;
2668
+ const [stored, setPlay] = useState2(() => newSnakePlay(width, height, Math.random));
2669
+ let play = reduceSnake(stored, { type: "resize", width, height }, Math.random);
2670
+ if (play.focused !== focused) {
2671
+ play = reduceSnake(play, { type: focused ? "focus" : "blur" }, Math.random);
2672
+ }
2673
+ if (play !== stored) setPlay(play);
2674
+ const { clock } = play;
2675
+ const step = clock.kind === "countdown" ? clock.step : null;
2240
2676
  useEffect2(() => {
2241
- if (enabled) {
2242
- setGame(newSnakeGame(width, height, Math.random));
2243
- setPaused(false);
2677
+ if (clock.kind === "countdown") {
2678
+ const timer2 = setTimeout(
2679
+ () => setPlay((current) => reduceSnake(current, { type: "count" }, Math.random)),
2680
+ COUNTDOWN_STEP_MS
2681
+ );
2682
+ return () => clearTimeout(timer2);
2244
2683
  }
2245
- }, [width, height, enabled]);
2246
- useEffect2(() => {
2247
- if (!enabled || paused) return;
2248
- const timer = setInterval(() => setGame((current) => tick(current, Math.random)), TICK_MS);
2249
- return () => clearInterval(timer);
2250
- }, [enabled, paused]);
2251
- const steer = (direction) => {
2252
- setPaused(false);
2253
- setGame(
2254
- (current) => current.over ? newSnakeGame(current.width, current.height, Math.random) : turn(current, direction)
2684
+ if (clock.kind !== "running") return;
2685
+ const timer = setInterval(
2686
+ () => setPlay((current) => reduceSnake(current, { type: "tick" }, Math.random)),
2687
+ TICK_MS
2255
2688
  );
2256
- };
2257
- const togglePause = () => setPaused((current) => !current);
2258
- return { game, paused, steer, togglePause };
2689
+ return () => clearInterval(timer);
2690
+ }, [clock.kind, step]);
2691
+ const send = (event) => setPlay((current) => reduceSnake(current, event, Math.random));
2692
+ return { play, send };
2259
2693
  };
2260
- var SnakeHeading = ({
2261
- handle,
2262
- hint
2263
- }) => {
2264
- const { game, paused } = handle;
2265
- const state = game.over ? "over \xB7 any arrow starts again" : paused ? "paused" : null;
2266
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
2267
- /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
2268
- /* @__PURE__ */ jsx2("span", { children: " " }),
2269
- /* @__PURE__ */ jsx2("span", { fg: INK, children: "play snake while you wait" }),
2270
- /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \xB7 score " }),
2271
- /* @__PURE__ */ jsx2("span", { fg: game.over ? MUTED : ACCENT, children: String(game.score) }),
2272
- state === null ? null : /* @__PURE__ */ jsx2("span", { fg: FAINT, children: ` \xB7 ${state}` })
2273
- ] }),
2274
- /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", fg: FAINT, children: ` ${hint}` })
2694
+ var SnakeHeading = ({ play }) => {
2695
+ const { game, clock } = play;
2696
+ const state = game.over ? "over" : clock.kind === "paused" ? "paused" : null;
2697
+ return /* @__PURE__ */ jsxs2("text", { height: 1, bg: "transparent", children: [
2698
+ /* @__PURE__ */ jsx2("span", { children: " " }),
2699
+ /* @__PURE__ */ jsx2("span", { fg: INK, children: "play snake while you wait" }),
2700
+ /* @__PURE__ */ jsx2("span", { fg: FAINT, children: " \xB7 score " }),
2701
+ /* @__PURE__ */ jsx2("span", { fg: game.over ? MUTED : ACCENT, children: String(game.score) }),
2702
+ state === null ? null : /* @__PURE__ */ jsx2("span", { fg: FAINT, children: ` \xB7 ${state}` })
2275
2703
  ] });
2276
2704
  };
2705
+ var SNAKE_IDLE = "enter plays";
2706
+ var snakeTooSmall = (short) => `make the terminal ${short ? "taller" : "wider"} to play snake`;
2707
+ var SNAKE_BEHIND_DIALOG = "the countdown starts when this dialog closes";
2708
+ var SnakeFooter = ({
2709
+ play,
2710
+ idle
2711
+ }) => /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", fg: FAINT, children: ` ${play.focused ? snakeHints(play) : idle}` });
2712
+ var cellColor = (cell) => cell === "\u25C6" ? ACCENT : cell === "\u2588" ? GREEN : cell === "\u25CF" ? INK : FAINT;
2713
+ var SnakeRows = ({ play }) => {
2714
+ const { game, clock } = play;
2715
+ const board = render(game);
2716
+ if (clock.kind !== "countdown") {
2717
+ return /* @__PURE__ */ jsx2(Fragment2, { children: board.map((row, index) => /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", children: [...row].map((cell, x) => /* @__PURE__ */ jsx2("span", { fg: cellColor(cell), children: cell === "\xB7" ? " " : cell }, x)) }, index)) });
2718
+ }
2719
+ const cells = overlayArt(
2720
+ board,
2721
+ countdownArt(countdownLabel(clock.step), game.width, game.height)
2722
+ );
2723
+ return /* @__PURE__ */ jsx2(Fragment2, { children: cells.map((row, index) => /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", children: row.map(
2724
+ (cell, x) => cell.board !== "\xB7" || cell.art === null ? /* @__PURE__ */ jsx2("span", { fg: cellColor(cell.board), children: cell.board === "\xB7" ? " " : cell.board }, x) : /* @__PURE__ */ jsx2("span", { fg: ACCENT, children: cell.art }, x)
2725
+ ) }, index)) });
2726
+ };
2277
2727
  var SnakeBoard = ({
2278
2728
  handle,
2279
- focused
2729
+ idle
2280
2730
  }) => {
2281
- const { game } = handle;
2282
- const rows = render(game);
2731
+ const { play } = handle;
2283
2732
  return /* @__PURE__ */ jsxs2(Fragment2, { children: [
2284
- /* @__PURE__ */ jsx2(
2285
- SnakeHeading,
2286
- {
2287
- handle,
2288
- hint: focused ? "arrows steer \xB7 space pauses \xB7 esc puts it away" : "focus this pane to play \xB7 esc puts it away"
2289
- }
2290
- ),
2733
+ /* @__PURE__ */ jsx2(SnakeHeading, { play }),
2291
2734
  /* @__PURE__ */ jsx2(
2292
2735
  "box",
2293
2736
  {
2294
2737
  border: true,
2295
2738
  borderStyle: "rounded",
2296
- borderColor: focused ? ACCENT : RULE,
2297
- width: game.width + 2,
2298
- height: game.height + 2,
2739
+ borderColor: play.focused ? ACCENT : RULE,
2740
+ title: " snake ",
2741
+ titleAlignment: "left",
2742
+ width: play.game.width + 2,
2743
+ height: play.game.height + 2,
2299
2744
  flexShrink: 0,
2300
2745
  marginLeft: 2,
2301
- children: rows.map((row, index) => /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", children: [...row].map((cell, x) => /* @__PURE__ */ jsx2(
2302
- "span",
2303
- {
2304
- fg: cell === "\u25C6" ? ACCENT : cell === "\u2588" ? GREEN : cell === "\u25CF" ? INK : FAINT,
2305
- children: cell === "\xB7" ? " " : cell
2306
- },
2307
- x
2308
- )) }, index))
2746
+ flexDirection: "column",
2747
+ children: /* @__PURE__ */ jsx2(SnakeRows, { play })
2309
2748
  }
2310
- )
2749
+ ),
2750
+ /* @__PURE__ */ jsx2(SnakeFooter, { play, idle })
2311
2751
  ] });
2312
2752
  };
2313
- var SnakeRows = ({ game }) => /* @__PURE__ */ jsx2(Fragment2, { children: render(game).map((row, index) => /* @__PURE__ */ jsx2("text", { height: 1, bg: "transparent", children: [...row].map((cell, x) => /* @__PURE__ */ jsx2(
2314
- "span",
2315
- {
2316
- fg: cell === "\u25C6" ? ACCENT : cell === "\u2588" ? GREEN : cell === "\u25CF" ? INK : FAINT,
2317
- children: cell === "\xB7" ? " " : cell
2318
- },
2319
- x
2320
- )) }, index)) });
2321
2753
 
2322
2754
  // src/dashboard.tsx
2323
2755
  import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "@opentui/react/jsx-runtime";
@@ -2351,12 +2783,6 @@ var lastSessionWords = (group) => {
2351
2783
  return [group.sessions.length === 0 ? `created ${ago}` : `last session created ${ago}`];
2352
2784
  };
2353
2785
  var fit = (text, width) => text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
2354
- var COLUMN_TITLE = {
2355
- projects: "projects",
2356
- worktrees: "worktrees",
2357
- sessions: "sessions",
2358
- detail: "session"
2359
- };
2360
2786
  var WORKTREE_ROW_HEIGHT = 2;
2361
2787
  var SESSION_ROW_HEIGHT = 2;
2362
2788
  var Gutter = ({ selected }) => /* @__PURE__ */ jsx3("span", { fg: selected ? ACCENT : FAINT, children: selected ? "\u258C " : " " });
@@ -2470,11 +2896,12 @@ var SessionRow = ({
2470
2896
  var HarnessRow = ({
2471
2897
  item,
2472
2898
  selected,
2473
- background
2899
+ background,
2900
+ width
2474
2901
  }) => /* @__PURE__ */ jsx3("box", { height: 1, flexShrink: 0, backgroundColor: selected ? WASH : background, children: /* @__PURE__ */ jsxs3("text", { height: 1, bg: selected ? WASH : background, children: [
2475
2902
  /* @__PURE__ */ jsx3(Gutter, { selected }),
2476
2903
  /* @__PURE__ */ jsx3("span", { fg: INK, children: item.label.padEnd(12) }),
2477
- /* @__PURE__ */ jsx3("span", { fg: FAINT, children: item.hint })
2904
+ /* @__PURE__ */ jsx3("span", { fg: FAINT, children: fit(item.hint, Math.max(4, width - 2 - 12)) })
2478
2905
  ] }) });
2479
2906
  var PREVIEW_GLYPH = {
2480
2907
  user: "\u203A ",
@@ -2620,10 +3047,23 @@ var keepSpanVisible = (scroll, top, height, viewRows) => {
2620
3047
  };
2621
3048
  var PREVIEW_MAX_LINES = 300;
2622
3049
  var SESSION_FACT_ROWS = 6;
3050
+ var SNAKE_CHROME_ROWS = 5;
3051
+ var SNAKE_MIN_BOARD_ROWS = 4;
3052
+ var SNAKE_MIN_BOARD_COLS = 8;
3053
+ var SNAKE_MIN_PANE_ROWS = SNAKE_CHROME_ROWS + SNAKE_MIN_BOARD_ROWS;
3054
+ var SNAKE_MIN_PANE_COLS = SNAKE_MIN_BOARD_COLS + 4;
2623
3055
  var noShare = () => "off";
2624
3056
  var NO_TUNNELS = [];
2625
3057
  var noTunnels = () => NO_TUNNELS;
2626
- var App = ({ ctx, onQuit }) => {
3058
+ var NO_FILTERS = {
3059
+ projects: "",
3060
+ worktrees: "",
3061
+ sessions: ""
3062
+ };
3063
+ var App = ({
3064
+ ctx,
3065
+ onQuit
3066
+ }) => {
2627
3067
  const renderer = useRenderer();
2628
3068
  const queryClient = useQueryClient2();
2629
3069
  const { data, failureReason } = useQuery2({
@@ -2633,8 +3073,13 @@ var App = ({ ctx, onQuit }) => {
2633
3073
  retry: true,
2634
3074
  retryDelay: 1e3
2635
3075
  });
2636
- const [focus, setFocusState] = useState3("sessions");
3076
+ const [columnFocus, setFocusState] = useState3("sessions");
2637
3077
  const [lastNav, setLastNav] = useState3("sessions");
3078
+ const [screenMode, setScreenMode] = useState3("normal");
3079
+ const [filters, setFilters] = useState3(NO_FILTERS);
3080
+ const [filtering, setFiltering] = useState3(null);
3081
+ const [helpOpen, setHelpOpen] = useState3(false);
3082
+ const [helpOffset, setHelpOffset] = useState3(0);
2638
3083
  const [projectKey, setProjectKey] = useState3(null);
2639
3084
  const [worktreeKey, setWorktreeKey] = useState3(null);
2640
3085
  const [sessionKey, setSessionKey] = useState3(null);
@@ -2661,12 +3106,16 @@ var App = ({ ctx, onQuit }) => {
2661
3106
  const adoptOfferDecided = useRef2(false);
2662
3107
  const [reviewing, setReviewing] = useState3(null);
2663
3108
  const lockRef = useRef2(false);
3109
+ const [snakeFocus, setSnakeFocus] = useState3(null);
3110
+ const [snakeOverlay, setSnakeOverlay] = useState3(ctx.openSnake === true);
2664
3111
  const [gate] = useState3(createLaunchGate);
2665
3112
  const creatingRef = useRef2(creating);
2666
3113
  creatingRef.current = creating;
2667
3114
  const branchRequest = useRef2(0);
2668
3115
  const { width: terminalCols, height: terminalRows } = useTerminalDimensions2();
2669
- const projectItems = deriveProjects(data);
3116
+ const projectItems = deriveProjects(data).filter(
3117
+ (item) => matchesFilter(filters.projects, item.project.name)
3118
+ );
2670
3119
  const homeProject = data === void 0 ? void 0 : matchProjectByCwd(data.projects, cwdFacts(ctx.cwd));
2671
3120
  const projectIndexRaw = projectKey === null ? -1 : projectItems.findIndex((p) => p.project.id === projectKey);
2672
3121
  const projectIndex = projectIndexRaw === -1 ? Math.max(
@@ -2674,20 +3123,38 @@ var App = ({ ctx, onQuit }) => {
2674
3123
  homeProject === void 0 ? 0 : projectItems.findIndex((p) => p.project.id === homeProject.id)
2675
3124
  ) : projectIndexRaw;
2676
3125
  const selectedProject = projectItems[projectIndex] ?? null;
2677
- const worktreeGroups = deriveWorktrees(data, selectedProject?.project.id ?? null);
3126
+ const worktreeGroups = deriveWorktrees(data, selectedProject?.project.id ?? null).filter(
3127
+ (group) => matchesFilter(filters.worktrees, group.name, groupBaseLabel(group))
3128
+ );
2678
3129
  const worktreeIndexRaw = worktreeKey === null ? -1 : worktreeGroups.findIndex((group) => group.key === worktreeKey);
2679
3130
  const sessionWorktreeIndex = worktreeGroups.findIndex(
2680
3131
  (group) => group.sessions.some((item) => item.session.id === sessionKey)
2681
3132
  );
2682
3133
  const worktreeIndex = worktreeIndexRaw === -1 ? Math.max(0, sessionWorktreeIndex) : worktreeIndexRaw;
2683
3134
  const selectedGroup = worktreeGroups[worktreeIndex] ?? null;
2684
- const sessionItems = selectedGroup?.sessions ?? [];
3135
+ const sessionItems = (selectedGroup?.sessions ?? []).filter(
3136
+ (item) => matchesFilter(
3137
+ filters.sessions,
3138
+ sessionDisplayName(item.session),
3139
+ item.session.harness,
3140
+ item.session.status
3141
+ )
3142
+ );
2685
3143
  const sessionIndexRaw = sessionKey === null ? -1 : sessionItems.findIndex((item) => item.session.id === sessionKey);
2686
3144
  const sessionIndex = sessionIndexRaw === -1 ? 0 : sessionIndexRaw;
2687
3145
  const selectedItem = sessionItems[sessionIndex] ?? null;
2688
3146
  const selectedSession = selectedItem?.session ?? null;
2689
- const pickerItems = picker === null ? [] : deriveHarnesses(picker.session);
2690
- const layout = planLayout(terminalCols, terminalRows, focus, lastNav);
3147
+ const pickerItems = picker === null ? [] : deriveHarnesses(picker.session, picker.worktree?.name ?? null);
3148
+ const waiting = selectedSession !== null && (selectedSession.status === "starting" || isPendingId(selectedSession.id));
3149
+ const dialogOpen = picker !== null || editing !== null || creating !== null || adoptOffer !== null || reviewing !== null || helpOpen || filtering !== null;
3150
+ const gameLayout = planLayout(terminalCols, terminalRows, "detail", lastNav);
3151
+ const snakeFits = gameLayout.detailRows >= SNAKE_MIN_PANE_ROWS && gameLayout.detailWidth >= SNAKE_MIN_PANE_COLS;
3152
+ const overlayFits = terminalRows >= 12 && terminalCols >= 22;
3153
+ const overlayOpen = snakeOverlay && !dialogOpen;
3154
+ const overlayFocused = overlayOpen && overlayFits;
3155
+ const snakeFocused = snakeFocus !== null && waiting && snakeFits && !dialogOpen && !snakeOverlay;
3156
+ const focus = snakeFocused ? "detail" : columnFocus;
3157
+ const layout = planLayout(terminalCols, terminalRows, focus, lastNav, screenMode);
2691
3158
  const detailWidth = layout.detailWidth;
2692
3159
  const sectionRows = (section) => layout.sections.find((entry) => entry.section === section)?.rows ?? 0;
2693
3160
  const previewSessionId = selectedSession !== null && !isPendingId(selectedSession.id) ? selectedSession.id : null;
@@ -2738,27 +3205,28 @@ var App = ({ ctx, onQuit }) => {
2738
3205
  [transcript.data, detailWidth]
2739
3206
  );
2740
3207
  const [previewOffset, setPreviewOffset] = useState3(0);
2741
- const factRows = Math.max(
3208
+ const allFactRows = Math.max(
2742
3209
  0,
2743
3210
  Math.min(SESSION_FACT_ROWS + workspaceRows.length, layout.detailRows - 2)
2744
3211
  );
3212
+ const factRows = waiting ? Math.max(0, Math.min(allFactRows, layout.detailRows - SNAKE_MIN_PANE_ROWS - 1)) : allFactRows;
2745
3213
  const showFactRule = factRows > 0 && layout.detailRows - factRows > 1;
2746
- const waiting = selectedSession !== null && (selectedSession.status === "starting" || isPendingId(selectedSession.id));
3214
+ const snakeRoom = {
3215
+ cols: detailWidth - 4,
3216
+ rows: layout.detailRows - factRows - (showFactRule ? 1 : 0) - SNAKE_CHROME_ROWS
3217
+ };
3218
+ const snakeShown = snakeRoom.rows >= SNAKE_MIN_BOARD_ROWS && snakeRoom.cols >= SNAKE_MIN_BOARD_COLS;
2747
3219
  const snake = useSnake({
2748
- width: Math.max(8, Math.min(40, detailWidth - 4)),
2749
- // The pane minus the facts, the rule, the starting line, two lines of hints and the border.
2750
- height: Math.max(4, Math.min(14, layout.detailRows - factRows - 8)),
2751
- enabled: waiting
3220
+ width: Math.max(SNAKE_MIN_BOARD_COLS, Math.min(40, snakeRoom.cols)),
3221
+ height: Math.max(SNAKE_MIN_BOARD_ROWS, Math.min(14, snakeRoom.rows)),
3222
+ focused: snakeFocused
2752
3223
  });
2753
- const [snakeAwayFor, setSnakeAwayFor] = useState3(null);
2754
- const snakeShown = waiting && snakeAwayFor !== selectedSession?.id;
2755
- const [snakeOverlay, setSnakeOverlay] = useState3(ctx.openSnake === true);
2756
3224
  const overlayWidth = Math.max(16, Math.min(60, terminalCols - 8));
2757
3225
  const overlayHeight = Math.max(6, Math.min(20, terminalRows - 10));
2758
3226
  const overlaySnake = useSnake({
2759
3227
  width: overlayWidth,
2760
3228
  height: overlayHeight,
2761
- enabled: snakeOverlay
3229
+ focused: overlayFocused
2762
3230
  });
2763
3231
  const previewRows = Math.max(1, layout.detailRows - factRows - (showFactRule ? 1 : 0));
2764
3232
  const previewView = previewWindow(preview, previewRows, previewOffset);
@@ -2771,6 +3239,9 @@ var App = ({ ctx, onQuit }) => {
2771
3239
  setPreviewOffset(0);
2772
3240
  };
2773
3241
  const say = (text) => setStatus({ text, at: Date.now() });
3242
+ const clearFilters = (...sections) => setFilters(
3243
+ (current) => sections.every((section) => current[section] === "") ? current : { ...current, ...Object.fromEntries(sections.map((section) => [section, ""])) }
3244
+ );
2774
3245
  const refetch = () => void queryClient.invalidateQueries({ queryKey: WORKBENCH_KEY });
2775
3246
  useEffect3(() => {
2776
3247
  const share = ctx.agentShare;
@@ -2909,6 +3380,7 @@ ${tunneled}
2909
3380
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2910
3381
  })
2911
3382
  );
3383
+ clearFilters("worktrees", "sessions");
2912
3384
  setWorktreeKey(vars.pendingKey);
2913
3385
  selectSession(vars.pendingKey);
2914
3386
  setBusy(`provisioning ${vars.harness} workspace \xB7`);
@@ -2916,6 +3388,7 @@ ${tunneled}
2916
3388
  },
2917
3389
  onError: (error, vars) => {
2918
3390
  patchWorkbench((current) => removeSession(current, vars.projectId, vars.pendingKey));
3391
+ setSnakeFocus((current) => current?.start === vars.gateKey ? null : current);
2919
3392
  setBusy(null);
2920
3393
  say(errorText(error));
2921
3394
  },
@@ -2956,7 +3429,8 @@ ${tunneled}
2956
3429
  );
2957
3430
  setBusyStarted(Date.now());
2958
3431
  },
2959
- onError: (error) => {
3432
+ onError: (error, vars) => {
3433
+ setSnakeFocus((current) => current?.start === vars.gateKey ? null : current);
2960
3434
  setBusy(null);
2961
3435
  say(errorText(error));
2962
3436
  refetch();
@@ -3014,6 +3488,7 @@ ${tunneled}
3014
3488
  return;
3015
3489
  }
3016
3490
  const project = result.project;
3491
+ clearFilters("projects");
3017
3492
  say(`adopted \xB7 ${project.name} \xB7 w starts a worktree`);
3018
3493
  setProjectKey(project.id);
3019
3494
  setWorktreeKey(null);
@@ -3101,12 +3576,14 @@ ${tunneled}
3101
3576
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3102
3577
  })
3103
3578
  );
3579
+ clearFilters("sessions");
3104
3580
  selectSession(vars.pendingKey);
3105
3581
  setBusy(`starting ${vars.harness} in the worktree \xB7`);
3106
3582
  setBusyStarted(Date.now());
3107
3583
  },
3108
3584
  onError: (error, vars) => {
3109
3585
  patchWorkbench((current) => removeSession(current, vars.projectId, vars.pendingKey));
3586
+ setSnakeFocus((current) => current?.start === vars.gateKey ? null : current);
3110
3587
  setBusy(null);
3111
3588
  say(errorText(error));
3112
3589
  },
@@ -3333,6 +3810,7 @@ ${tunneled}
3333
3810
  return;
3334
3811
  }
3335
3812
  setCreating(null);
3813
+ setSnakeFocus({ returnTo: focus, start: gateKey });
3336
3814
  setFocus("sessions");
3337
3815
  launchMutation.mutate({
3338
3816
  projectId: current.projectId,
@@ -3406,6 +3884,7 @@ ${tunneled}
3406
3884
  say(`already resuming \xB7 ${sessionDisplayName(session)}`);
3407
3885
  return;
3408
3886
  }
3887
+ setSnakeFocus({ returnTo: focus, start: gateKey });
3409
3888
  setFocus("sessions");
3410
3889
  resumeMutation.mutate({ projectId, session, harness, gateKey });
3411
3890
  };
@@ -3437,6 +3916,7 @@ ${tunneled}
3437
3916
  say(`already starting a session in ${group.name}`);
3438
3917
  return;
3439
3918
  }
3919
+ setSnakeFocus({ returnTo: focus, start: gateKey });
3440
3920
  setFocus("sessions");
3441
3921
  launchInWorktreeMutation.mutate({
3442
3922
  projectId,
@@ -3485,34 +3965,34 @@ ${tunneled}
3485
3965
  return;
3486
3966
  }
3487
3967
  };
3488
- const moveColumn = (delta) => setFocus(stepColumn(focus, delta));
3968
+ const goTo = (next) => {
3969
+ if (next === "detail" && waiting && snakeFits) {
3970
+ setSnakeFocus({ returnTo: focus, start: null });
3971
+ return;
3972
+ }
3973
+ setFocus(next);
3974
+ };
3975
+ const moveColumn = (delta) => goTo(stepColumn(focus, delta));
3489
3976
  useKeyboard2((key) => {
3490
3977
  if (reviewing !== null) return;
3491
3978
  if (lockRef.current) return;
3492
3979
  if (key.ctrl && key.name === "c") return onQuit();
3493
- if (snakeOverlay) {
3494
- if (key.name === "up" || key.name === "down" || key.name === "left" || key.name === "right") {
3495
- overlaySnake.steer(key.name);
3496
- } else if (key.name === "space") {
3497
- overlaySnake.togglePause();
3498
- } else if (key.name === "escape" || key.name === "q") {
3499
- setSnakeOverlay(false);
3500
- }
3501
- return;
3502
- }
3503
- if (waiting && focus === "detail" && picker === null && editing === null && creating === null) {
3504
- if (snakeShown) {
3505
- if (key.name === "up" || key.name === "down" || key.name === "left" || key.name === "right") {
3506
- snake.steer(key.name);
3507
- return;
3980
+ const verb = verbForKey(strokeName(key), key.shift === true);
3981
+ const game = overlayFocused ? overlaySnake : snakeFocused ? snake : null;
3982
+ if (game !== null || overlayOpen) {
3983
+ const action = snakeKey(key.name ?? "");
3984
+ if (action?.type === "leave") {
3985
+ if (overlayOpen) {
3986
+ setSnakeOverlay(false);
3987
+ } else if (snakeFocus !== null) {
3988
+ setFocus(snakeFocus.returnTo);
3989
+ setSnakeFocus(null);
3508
3990
  }
3509
- if (key.name === "space") return snake.togglePause();
3510
- if (key.name === "escape") return setSnakeAwayFor(selectedSession?.id ?? null);
3511
- } else if (key.name === "space") {
3512
- return setSnakeAwayFor(null);
3991
+ return;
3513
3992
  }
3993
+ if (action !== null) return game?.send(action);
3994
+ if (gameSwallows(verb) || game === null) return;
3514
3995
  }
3515
- const verb = verbForKey(key.name ?? "", key.shift === true);
3516
3996
  if (verb !== "stop") setStopArmed(null);
3517
3997
  if (verb !== "remove") setRemoveArmed(null);
3518
3998
  if (adoptOffer !== null) {
@@ -3587,6 +4067,26 @@ ${tunneled}
3587
4067
  }
3588
4068
  return;
3589
4069
  }
4070
+ if (helpOpen) {
4071
+ if (key.name === "escape" || key.name === "q" || verb === "help") {
4072
+ setHelpOpen(false);
4073
+ } else if (verb === "moveDown" || verb === "pageDown") {
4074
+ setHelpOffset(
4075
+ (current) => Math.max(0, Math.min(helpMaxOffset, current + (verb === "pageDown" ? 10 : 1)))
4076
+ );
4077
+ } else if (verb === "moveUp" || verb === "pageUp") {
4078
+ setHelpOffset((current) => Math.max(0, current - (verb === "pageUp" ? 10 : 1)));
4079
+ }
4080
+ return;
4081
+ }
4082
+ if (filtering !== null) {
4083
+ if (key.name === "escape") {
4084
+ clearFilters(filtering);
4085
+ setFiltering(null);
4086
+ } else if (key.name === "down") moveSelection(1);
4087
+ else if (key.name === "up") moveSelection(-1);
4088
+ return;
4089
+ }
3590
4090
  if (picker !== null) {
3591
4091
  switch (key.name) {
3592
4092
  case "down":
@@ -3619,6 +4119,10 @@ ${tunneled}
3619
4119
  return;
3620
4120
  }
3621
4121
  }
4122
+ if (snakeFocus !== null && verb !== null) setSnakeFocus(null);
4123
+ if (key.name === "space" && waiting && snakeFits && focus === "detail") {
4124
+ return setSnakeFocus({ returnTo: focus, start: null });
4125
+ }
3622
4126
  if (key.ctrl === true) return;
3623
4127
  if (focus === "projects" && ["attach", "resume", "stop", "remove", "rename", "openWeb", "review"].includes(verb ?? "")) {
3624
4128
  say("select a session first \xB7 \u2192 opens worktrees");
@@ -3639,6 +4143,37 @@ ${tunneled}
3639
4143
  return moveColumn(1);
3640
4144
  case "columnLeft":
3641
4145
  return moveColumn(-1);
4146
+ case "nextPane":
4147
+ return goTo(cyclePane(focus, 1));
4148
+ case "prevPane":
4149
+ return goTo(cyclePane(focus, -1));
4150
+ case "jumpProjects":
4151
+ case "jumpWorktrees":
4152
+ case "jumpSessions":
4153
+ case "jumpDetail":
4154
+ return goTo(paneForDigit(strokeName(key)) ?? focus);
4155
+ case "back":
4156
+ if (isNavSection(focus) && filters[focus] !== "") {
4157
+ clearFilters(focus);
4158
+ return;
4159
+ }
4160
+ return goTo(backFrom(focus, lastNav));
4161
+ case "filter":
4162
+ if (!isNavSection(focus)) {
4163
+ say("/ filters a list \xB7 1 2 3 jump to one");
4164
+ return;
4165
+ }
4166
+ return setFiltering(focus);
4167
+ case "nextScreenMode":
4168
+ case "prevScreenMode": {
4169
+ const next = stepScreenMode(screenMode, verb === "nextScreenMode" ? 1 : -1);
4170
+ setScreenMode(next);
4171
+ say(`screen mode \xB7 ${next}${next === "normal" ? "" : " \xB7 + and _ cycle it"}`);
4172
+ return;
4173
+ }
4174
+ case "help":
4175
+ setHelpOffset(0);
4176
+ return setHelpOpen(true);
3642
4177
  case "attach":
3643
4178
  return attachSelected();
3644
4179
  case "resume":
@@ -3695,18 +4230,20 @@ ${tunneled}
3695
4230
  });
3696
4231
  const liveTotal = projectItems.reduce((sum, item) => sum + item.live, 0);
3697
4232
  const stoppingTotal = projectItems.reduce((sum, item) => sum + item.stopping, 0);
3698
- const columnTitle = (column) => {
4233
+ const named = (section) => filters[section] === "" ? section : `${section} /${filters[section]}`;
4234
+ const columnTitle = (column) => numberedTitle(column, columnWords(column));
4235
+ const columnWords = (column) => {
3699
4236
  if (column === "worktrees") {
3700
- return selectedProject === null ? "worktrees" : `worktrees \xB7 ${worktreeGroups.length}${selectedProject.live > 0 ? ` \xB7 ${selectedProject.live} live` : ""}`;
4237
+ return selectedProject === null ? named("worktrees") : `${named("worktrees")} \xB7 ${worktreeGroups.length}${selectedProject.live > 0 ? ` \xB7 ${selectedProject.live} live` : ""}`;
3701
4238
  }
3702
4239
  if (column === "sessions") {
3703
- return selectedGroup === null ? "sessions" : `sessions \xB7 ${sessionItems.length}`;
4240
+ return selectedGroup === null ? named("sessions") : `${named("sessions")} \xB7 ${sessionItems.length}`;
3704
4241
  }
3705
4242
  if (column === "detail") {
3706
4243
  if (selectedSession === null) return "session";
3707
4244
  return previewView.offset === 0 ? "session \xB7 read-only" : `session \xB7 read-only \xB7 ${previewView.later} newer below`;
3708
4245
  }
3709
- return COLUMN_TITLE[column];
4246
+ return named("projects");
3710
4247
  };
3711
4248
  const pickerTitle = picker === null ? "" : picker.worktree === void 0 ? picker.session === null ? "new session \xB7 pick a harness" : `resume ${sessionDisplayName(picker.session)} \xB7 pick a harness` : `new session in ${picker.worktree.name} \xB7 pick a harness`;
3712
4249
  const modalWidth = Math.min(74, terminalCols - 4);
@@ -3726,7 +4263,20 @@ ${tunneled}
3726
4263
  modalWidth - 7
3727
4264
  );
3728
4265
  const creatingHeight = 2 + 1 + 1 + 6 + 1 + deriveHarnesses(null).length;
3729
- 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";
4266
+ const modalFieldWidth = Math.max(4, modalWidth - 2 - 11 - 1);
4267
+ const footerText = adoptOffer === null ? editing === null ? creating === null ? picker === null ? overlayFocused || snakeFocused ? ` ${snakeHints(overlayFocused ? overlaySnake.play : snake.play)}` : ` ${footerHints(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";
4268
+ const helpWidth = Math.min(96, terminalCols - 4);
4269
+ const helpLines = helpSections(focus).flatMap((section, index) => [
4270
+ ...index === 0 ? [] : [{ kind: "blank", key: `blank-${section.title}` }],
4271
+ { kind: "title", key: `title-${section.title}`, text: section.title },
4272
+ ...section.rows.map((row) => ({ kind: "row", key: row.verb, row }))
4273
+ ]);
4274
+ const helpRows = Math.max(1, Math.min(helpLines.length, terminalRows - 6));
4275
+ const helpMaxOffset = Math.max(0, helpLines.length - helpRows);
4276
+ const helpView = helpLines.slice(
4277
+ Math.min(helpOffset, helpMaxOffset),
4278
+ Math.min(helpOffset, helpMaxOffset) + helpRows
4279
+ );
3730
4280
  const loadFailure = data === void 0 && failureReason !== null ? errorText(failureReason) : null;
3731
4281
  if (reviewing !== null) {
3732
4282
  return /* @__PURE__ */ jsx3(
@@ -3811,7 +4361,12 @@ ${tunneled}
3811
4361
  },
3812
4362
  item.project.id
3813
4363
  )),
3814
- data !== void 0 && projectItems.length === 0 ? /* @__PURE__ */ jsx3(EmptyNote, { text: "none adopted yet" }) : null
4364
+ data !== void 0 && projectItems.length === 0 ? /* @__PURE__ */ jsx3(
4365
+ EmptyNote,
4366
+ {
4367
+ text: filters.projects === "" ? "none adopted yet" : `nothing matches /${filters.projects} \xB7 esc clears`
4368
+ }
4369
+ ) : null
3815
4370
  ]
3816
4371
  }
3817
4372
  )
@@ -3843,7 +4398,12 @@ ${tunneled}
3843
4398
  },
3844
4399
  group.key
3845
4400
  )),
3846
- data !== void 0 && worktreeGroups.length === 0 ? /* @__PURE__ */ jsx3(EmptyNote, { text: "no worktrees \xB7 w starts one" }) : null
4401
+ data !== void 0 && worktreeGroups.length === 0 ? /* @__PURE__ */ jsx3(
4402
+ EmptyNote,
4403
+ {
4404
+ text: filters.worktrees === "" ? "no worktrees \xB7 w starts one" : `nothing matches /${filters.worktrees} \xB7 esc clears`
4405
+ }
4406
+ ) : null
3847
4407
  ]
3848
4408
  }
3849
4409
  )
@@ -3878,7 +4438,7 @@ ${tunneled}
3878
4438
  data !== void 0 && sessionItems.length === 0 ? /* @__PURE__ */ jsx3(
3879
4439
  EmptyNote,
3880
4440
  {
3881
- text: selectedGroup === null ? "no worktree selected" : "no sessions \xB7 n starts one"
4441
+ text: selectedGroup === null ? "no worktree selected" : filters.sessions === "" ? "no sessions \xB7 n starts one" : `nothing matches /${filters.sessions} \xB7 esc clears`
3882
4442
  }
3883
4443
  ) : null,
3884
4444
  loadFailure === null ? null : /* @__PURE__ */ jsx3("text", { height: 1, fg: INK_2, bg: "transparent", children: ` ${loadFailure} \xB7 retrying` })
@@ -3908,7 +4468,13 @@ ${tunneled}
3908
4468
  /* @__PURE__ */ jsx3("span", { fg: INK_2, children: "starting" }),
3909
4469
  /* @__PURE__ */ jsx3("span", { fg: FAINT, children: ` \xB7 ${selectedSession === null ? "launching" : startingExplanationOf(selectedSession)}` })
3910
4470
  ] }),
3911
- snakeShown ? /* @__PURE__ */ jsx3(SnakeBoard, { handle: snake, focused: focus === "detail" }) : /* @__PURE__ */ jsx3(EmptyNote, { text: "snake is put away \xB7 space brings it back" })
4471
+ snakeShown ? /* @__PURE__ */ jsx3(
4472
+ SnakeBoard,
4473
+ {
4474
+ handle: snake,
4475
+ idle: snakeFocus !== null && dialogOpen ? SNAKE_BEHIND_DIALOG : SNAKE_IDLE
4476
+ }
4477
+ ) : /* @__PURE__ */ jsx3(EmptyNote, { text: snakeTooSmall(snakeRoom.rows < SNAKE_MIN_BOARD_ROWS) })
3912
4478
  ] }) : previewSessionId === null ? /* @__PURE__ */ jsx3(EmptyNote, { text: "provisioning \xB7 no record yet" }) : selectedSession?.harness === "shell" ? /* @__PURE__ */ jsx3(EmptyNote, { text: "shell \xB7 no conversation record; a attaches if live" }) : transcript.isPending ? /* @__PURE__ */ jsx3(EmptyNote, { text: "reading the record\u2026" }) : transcript.error === null ? previewView.lines.length === 0 ? /* @__PURE__ */ jsx3(EmptyNote, { text: "no conversation recorded yet" }) : previewView.lines.map((line, index) => /* @__PURE__ */ jsx3(PreviewRow, { line }, `${index}-${line.kind}`)) : /* @__PURE__ */ jsx3("text", { height: 1, fg: INK_2, bg: "transparent", children: ` could not read the record \xB7 ${errorText(transcript.error)}` }) })
3913
4479
  ] }) });
3914
4480
  const offNav = layout.offscreen.filter(isNavSection);
@@ -3947,7 +4513,7 @@ ${tunneled}
3947
4513
  /* @__PURE__ */ jsx3("span", { fg: shareState === "connected" ? MUTED : FAINT, children: shareState === "connected" ? "agent shared" : "agent share reconnecting" })
3948
4514
  ] })
3949
4515
  ] }),
3950
- /* @__PURE__ */ jsx3("text", { height: 1, fg: FAINT, bg: "transparent", children: `${ctx.config.url} ` })
4516
+ /* @__PURE__ */ jsx3("text", { height: 1, fg: FAINT, bg: "transparent", children: `${screenMode === "normal" ? "" : `${screenMode} screen \xB7 `}${ctx.config.url} ` })
3951
4517
  ] }),
3952
4518
  !layout.breadcrumb ? null : /* @__PURE__ */ jsxs3("text", { height: 1, bg: "transparent", children: [
3953
4519
  /* @__PURE__ */ jsx3("span", { fg: FAINT, children: " " }),
@@ -3971,28 +4537,51 @@ ${tunneled}
3971
4537
  ),
3972
4538
  detailWidth === 0 ? null : renderDetail()
3973
4539
  ] }),
3974
- snakeOverlay ? /* @__PURE__ */ jsxs3(
4540
+ snakeOverlay && !overlayFits ? /* @__PURE__ */ jsx3(
3975
4541
  "box",
3976
4542
  {
3977
4543
  position: "absolute",
3978
- zIndex: 13,
3979
- left: Math.max(1, Math.floor((terminalCols - (overlayWidth + 4)) / 2)),
3980
- top: Math.max(1, Math.floor((terminalRows - (overlayHeight + 5)) / 2)),
3981
- width: overlayWidth + 4,
3982
- height: overlayHeight + 5,
4544
+ zIndex: 9,
4545
+ left: 1,
4546
+ top: Math.max(1, Math.floor((terminalRows - 3) / 2)),
4547
+ width: Math.max(4, terminalCols - 2),
4548
+ height: 3,
3983
4549
  border: true,
3984
4550
  borderStyle: "rounded",
3985
- borderColor: ACCENT,
3986
- title: " snake ",
3987
- titleAlignment: "left",
4551
+ borderColor: RULE,
3988
4552
  backgroundColor: SURFACE,
3989
- flexDirection: "column",
3990
- children: [
3991
- /* @__PURE__ */ jsx3(SnakeHeading, { handle: overlaySnake, hint: "arrows steer \xB7 space pauses \xB7 esc closes" }),
3992
- /* @__PURE__ */ jsx3("box", { marginLeft: 1, flexDirection: "column", children: /* @__PURE__ */ jsx3(SnakeRows, { game: overlaySnake.game }) })
3993
- ]
4553
+ children: /* @__PURE__ */ jsx3("text", { height: 1, bg: "transparent", fg: FAINT, children: fit(
4554
+ ` ${snakeTooSmall(terminalRows < 12)} \xB7 esc closes`,
4555
+ Math.max(1, terminalCols - 4)
4556
+ ) })
3994
4557
  }
3995
4558
  ) : null,
4559
+ snakeOverlay && overlayFits ? (
4560
+ // Under every dialog: one that opens over the game has the keyboard until it closes.
4561
+ /* @__PURE__ */ jsxs3(
4562
+ "box",
4563
+ {
4564
+ position: "absolute",
4565
+ zIndex: 9,
4566
+ left: Math.max(1, Math.floor((terminalCols - (overlayWidth + 4)) / 2)),
4567
+ top: Math.max(1, Math.floor((terminalRows - (overlayHeight + 4)) / 2)),
4568
+ width: overlayWidth + 4,
4569
+ height: overlayHeight + 4,
4570
+ border: true,
4571
+ borderStyle: "rounded",
4572
+ borderColor: overlayFocused ? ACCENT : RULE,
4573
+ title: " snake ",
4574
+ titleAlignment: "left",
4575
+ backgroundColor: SURFACE,
4576
+ flexDirection: "column",
4577
+ children: [
4578
+ /* @__PURE__ */ jsx3(SnakeHeading, { play: overlaySnake.play }),
4579
+ /* @__PURE__ */ jsx3("box", { marginLeft: 1, flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx3(SnakeRows, { play: overlaySnake.play }) }),
4580
+ /* @__PURE__ */ jsx3(SnakeFooter, { play: overlaySnake.play, idle: SNAKE_BEHIND_DIALOG })
4581
+ ]
4582
+ }
4583
+ )
4584
+ ) : null,
3996
4585
  picker === null ? null : /* @__PURE__ */ jsxs3(
3997
4586
  "box",
3998
4587
  {
@@ -4020,7 +4609,8 @@ ${tunneled}
4020
4609
  {
4021
4610
  item,
4022
4611
  selected: index === pickerIndex,
4023
- background: SURFACE
4612
+ background: SURFACE,
4613
+ width: modalWidth - 2
4024
4614
  },
4025
4615
  String(item.harness)
4026
4616
  ))
@@ -4127,7 +4717,10 @@ ${tunneled}
4127
4717
  {
4128
4718
  focused: true,
4129
4719
  value: creating.name,
4130
- placeholder: "e.g. fix-auth (empty = auto \xB7 an existing name joins it)",
4720
+ placeholder: fit(
4721
+ "e.g. fix-auth (empty = auto \xB7 an existing name joins it)",
4722
+ modalFieldWidth
4723
+ ),
4131
4724
  backgroundColor: SURFACE,
4132
4725
  focusedBackgroundColor: SURFACE,
4133
4726
  textColor: INK,
@@ -4158,7 +4751,10 @@ ${tunneled}
4158
4751
  {
4159
4752
  focused: true,
4160
4753
  value: "",
4161
- placeholder: "type to filter \xB7 enter takes the highlighted branch, or the default when empty",
4754
+ placeholder: fit(
4755
+ "filter \xB7 enter takes the highlighted branch, or the default",
4756
+ modalFieldWidth
4757
+ ),
4162
4758
  backgroundColor: SURFACE,
4163
4759
  focusedBackgroundColor: SURFACE,
4164
4760
  textColor: INK,
@@ -4191,7 +4787,7 @@ ${tunneled}
4191
4787
  height: 1,
4192
4788
  bg: SURFACE,
4193
4789
  fg: creating.branchError === null ? FAINT : ERROR,
4194
- children: index === 0 && notice !== null ? ` ${fit(notice, 66)}` : " "
4790
+ children: index === 0 && notice !== null ? ` ${fit(notice, modalWidth - 7)}` : " "
4195
4791
  },
4196
4792
  `slot-${index}`
4197
4793
  );
@@ -4219,7 +4815,7 @@ ${tunneled}
4219
4815
  /* @__PURE__ */ jsx3(Gutter, { selected: creating.step === "harness" }),
4220
4816
  /* @__PURE__ */ jsx3("span", { fg: FAINT, children: "harness" })
4221
4817
  ] }),
4222
- deriveHarnesses(null).map((item, index) => {
4818
+ deriveHarnesses(null, creating.joins ? creating.name : null).map((item, index) => {
4223
4819
  const active = creating.step === "harness";
4224
4820
  const highlighted = active && index === creating.harnessIndex;
4225
4821
  return /* @__PURE__ */ jsx3(
@@ -4232,7 +4828,7 @@ ${tunneled}
4232
4828
  /* @__PURE__ */ jsx3("span", { fg: FAINT, children: " " }),
4233
4829
  /* @__PURE__ */ jsx3(Gutter, { selected: highlighted }),
4234
4830
  /* @__PURE__ */ jsx3("span", { fg: active ? INK : FAINT, children: item.label.padEnd(12) }),
4235
- /* @__PURE__ */ jsx3("span", { fg: FAINT, children: active ? item.hint : "" })
4831
+ /* @__PURE__ */ jsx3("span", { fg: FAINT, children: active ? fit(item.hint, Math.max(4, modalWidth - 2 - 3 - 2 - 12)) : "" })
4236
4832
  ] })
4237
4833
  },
4238
4834
  String(item.harness)
@@ -4242,7 +4838,57 @@ ${tunneled}
4242
4838
  }
4243
4839
  ),
4244
4840
  loadFailure === null ? /* @__PURE__ */ jsx3(StatusLine, { busy, busyStarted, status }) : /* @__PURE__ */ jsx3("text", { height: 1, fg: ERROR, bg: "transparent", children: ` could not load workbench \xB7 ${loadFailure} \xB7 retrying` }),
4245
- /* @__PURE__ */ jsx3("text", { height: 1, fg: FAINT, bg: "transparent", children: footerText })
4841
+ helpOpen ? /* @__PURE__ */ jsxs3(
4842
+ "box",
4843
+ {
4844
+ position: "absolute",
4845
+ zIndex: 14,
4846
+ left: Math.max(1, Math.floor((terminalCols - helpWidth) / 2)),
4847
+ top: Math.max(1, Math.floor((terminalRows - (helpRows + 3)) / 2)),
4848
+ width: helpWidth,
4849
+ height: helpRows + 3,
4850
+ border: true,
4851
+ borderStyle: "rounded",
4852
+ borderColor: ACCENT,
4853
+ title: ` keys \xB7 ${fit(columnTitle(focus), Math.max(6, helpWidth - 14))} `,
4854
+ titleAlignment: "left",
4855
+ backgroundColor: SURFACE,
4856
+ flexDirection: "column",
4857
+ children: [
4858
+ helpView.map(
4859
+ (line) => line.kind === "blank" ? /* @__PURE__ */ jsx3("text", { height: 1, bg: SURFACE, children: " " }, line.key) : line.kind === "title" ? /* @__PURE__ */ jsx3("text", { height: 1, bg: SURFACE, fg: MUTED, children: ` ${line.text}` }, line.key) : /* @__PURE__ */ jsxs3("text", { height: 1, bg: SURFACE, children: [
4860
+ /* @__PURE__ */ jsx3("span", { fg: INK, children: ` ${fit(line.row.keys, 18).padEnd(19)}` }),
4861
+ /* @__PURE__ */ jsx3("span", { fg: line.row.here ? INK_2 : FAINT, children: fit(line.row.help, Math.max(8, helpWidth - 25)) })
4862
+ ] }, line.key)
4863
+ ),
4864
+ /* @__PURE__ */ jsx3("text", { height: 1, bg: SURFACE, fg: FAINT, children: ` ${helpMaxOffset > 0 ? "\u2191\u2193 scroll \xB7 " : ""}esc closes` })
4865
+ ]
4866
+ }
4867
+ ) : null,
4868
+ filtering === null ? /* @__PURE__ */ jsx3("text", { height: 1, fg: FAINT, bg: "transparent", children: footerText }) : /* @__PURE__ */ jsxs3("box", { height: 1, flexShrink: 0, flexDirection: "row", children: [
4869
+ /* @__PURE__ */ jsx3("text", { height: 1, bg: "transparent", fg: ACCENT, children: " / " }),
4870
+ /* @__PURE__ */ jsx3(
4871
+ "input",
4872
+ {
4873
+ focused: true,
4874
+ value: filters[filtering],
4875
+ placeholder: `filter ${columnTitle(filtering)} \xB7 enter keeps it \xB7 esc clears it`,
4876
+ backgroundColor: CANVAS,
4877
+ focusedBackgroundColor: CANVAS,
4878
+ textColor: INK,
4879
+ focusedTextColor: INK,
4880
+ placeholderColor: FAINT,
4881
+ cursorColor: INK,
4882
+ flexGrow: 1,
4883
+ onInput: (value) => {
4884
+ const section = filtering;
4885
+ const clean = value.replace(/\t/g, "");
4886
+ setFilters((current) => ({ ...current, [section]: clean }));
4887
+ },
4888
+ onSubmit: () => setFiltering(null)
4889
+ }
4890
+ )
4891
+ ] })
4246
4892
  ] });
4247
4893
  };
4248
4894
  var runDashboard = async (ctx) => {
@@ -4313,5 +4959,6 @@ var runDashboard = async (ctx) => {
4313
4959
  });
4314
4960
  };
4315
4961
  export {
4962
+ App,
4316
4963
  runDashboard
4317
4964
  };