opencode-worktree 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ui.ts CHANGED
@@ -13,18 +13,21 @@ import {
13
13
  import { checkForUpdate } from "./update-check.js";
14
14
  import { basename } from "node:path";
15
15
  import {
16
+ checkoutBranch,
17
+ createBranchFromCommit,
16
18
  createWorktree,
17
19
  deleteWorktree,
18
20
  getDefaultWorktreesDir,
21
+ getHeadCommit,
19
22
  hasUncommittedChanges,
20
23
  isMainWorktree,
21
24
  listWorktrees,
22
25
  resolveRepoRoot,
23
26
  unlinkWorktree,
24
27
  } from "./git.js";
25
- import { isOpenCodeAvailable, launchOpenCode, openInFileManager } from "./opencode.js";
28
+ import { isCommandAvailable, launchCommand, openInFileManager } from "./opencode.js";
26
29
  import { WorktreeInfo } from "./types.js";
27
- import { loadRepoConfig, saveRepoConfig, configExists, type Config } from "./config.js";
30
+ import { loadRepoConfig, saveRepoConfig, type Config } from "./config.js";
28
31
  import { runPostCreateHook, type HookResult } from "./hooks.js";
29
32
 
30
33
  type StatusLevel = "info" | "warning" | "error" | "success";
@@ -81,6 +84,7 @@ class WorktreeSelector {
81
84
 
82
85
  private opencodeAvailable = false;
83
86
  private repoRoot: string | null = null;
87
+ private repoConfig: Config = {};
84
88
  private isCreatingWorktree = false;
85
89
  private worktreeOptions: SelectOption[] = [];
86
90
 
@@ -103,8 +107,18 @@ class WorktreeSelector {
103
107
  private configContainer: BoxRenderable | null = null;
104
108
  private configHookInput: InputRenderable | null = null;
105
109
  private configOpenInput: InputRenderable | null = null;
106
- private configActiveField: "hook" | "open" = "hook";
107
- private isFirstTimeSetup = false;
110
+ private configLaunchInput: InputRenderable | null = null;
111
+ private configActiveField: "hook" | "open" | "launch" = "hook";
112
+ private repoKey: string | null = null; // Normalized git remote URL for config lookup
113
+
114
+ // Branch creation state
115
+ private isCreatingBranch = false;
116
+ private branchCreateContainer: BoxRenderable | null = null;
117
+ private branchNameInput: InputRenderable | null = null;
118
+ private sourceWorktree: WorktreeInfo | null = null;
119
+ private pendingBranchName: string | null = null;
120
+ private isAskingCheckout = false;
121
+ private checkoutSelect: SelectRenderable | null = null;
108
122
 
109
123
  constructor(
110
124
  private renderer: CliRenderer,
@@ -113,7 +127,12 @@ class WorktreeSelector {
113
127
  ) {
114
128
  // Load worktrees first to get initial options
115
129
  this.repoRoot = resolveRepoRoot(this.targetPath);
116
- this.opencodeAvailable = isOpenCodeAvailable();
130
+ if (this.repoRoot) {
131
+ const { config, repoKey } = loadRepoConfig(this.repoRoot);
132
+ this.repoConfig = config;
133
+ this.repoKey = repoKey;
134
+ }
135
+ this.opencodeAvailable = isCommandAvailable(this.repoConfig.launchCommand || "opencode");
117
136
  this.worktreeOptions = this.buildInitialOptions();
118
137
 
119
138
  this.title = new TextRenderable(renderer, {
@@ -192,7 +211,7 @@ class WorktreeSelector {
192
211
  left: 2,
193
212
  top: 20,
194
213
  content:
195
- "↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit",
214
+ "↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit",
196
215
  fg: "#64748B",
197
216
  });
198
217
  this.renderer.root.add(this.instructions);
@@ -213,11 +232,6 @@ class WorktreeSelector {
213
232
  });
214
233
 
215
234
  this.selectElement.focus();
216
-
217
- // Check for first-time setup
218
- if (this.repoRoot && !configExists(this.repoRoot)) {
219
- this.showFirstTimeSetup();
220
- }
221
235
  }
222
236
 
223
237
  private getInitialStatusMessage(): string {
@@ -262,7 +276,7 @@ class WorktreeSelector {
262
276
  this.selectElement.visible = true;
263
277
  this.selectElement.focus();
264
278
  this.instructions.content =
265
- "↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
279
+ "↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
266
280
  return;
267
281
  }
268
282
  this.cleanup(true);
@@ -308,14 +322,18 @@ class WorktreeSelector {
308
322
  return;
309
323
  }
310
324
  if (key.name === "tab") {
311
- // Switch between fields
325
+ // Cycle between fields: hook -> open -> launch -> hook
312
326
  if (this.configActiveField === "hook") {
313
327
  this.configActiveField = "open";
314
328
  this.configHookInput?.blur();
315
329
  this.configOpenInput?.focus();
330
+ } else if (this.configActiveField === "open") {
331
+ this.configActiveField = "launch";
332
+ this.configOpenInput?.blur();
333
+ this.configLaunchInput?.focus();
316
334
  } else {
317
335
  this.configActiveField = "hook";
318
- this.configOpenInput?.blur();
336
+ this.configLaunchInput?.blur();
319
337
  this.configHookInput?.focus();
320
338
  }
321
339
  this.renderer.requestRender();
@@ -346,6 +364,24 @@ class WorktreeSelector {
346
364
  return;
347
365
  }
348
366
 
367
+ // Handle branch creation mode
368
+ if (this.isCreatingBranch) {
369
+ if (key.name === "escape") {
370
+ this.hideBranchCreateInput();
371
+ return;
372
+ }
373
+ return;
374
+ }
375
+
376
+ // Handle checkout confirmation mode
377
+ if (this.isAskingCheckout) {
378
+ if (key.name === "escape") {
379
+ this.hideCheckoutConfirm();
380
+ return;
381
+ }
382
+ return;
383
+ }
384
+
349
385
  if (key.name === "q" || key.name === "escape") {
350
386
  this.cleanup(true);
351
387
  return;
@@ -379,6 +415,12 @@ class WorktreeSelector {
379
415
  this.showConfigEditor();
380
416
  return;
381
417
  }
418
+
419
+ // 'b' for creating a new branch from selected worktree
420
+ if (key.name === "b") {
421
+ this.showBranchCreateInput();
422
+ return;
423
+ }
382
424
  }
383
425
 
384
426
  private handleSelection(value: SelectionValue): void {
@@ -388,13 +430,14 @@ class WorktreeSelector {
388
430
  }
389
431
 
390
432
  const worktree = value as WorktreeInfo;
433
+ const cmdName = this.repoConfig.launchCommand || "opencode";
391
434
  if (!this.opencodeAvailable) {
392
- this.setStatus("opencode is not available on PATH.", "error");
435
+ this.setStatus(`${cmdName} is not available on PATH.`, "error");
393
436
  return;
394
437
  }
395
438
 
396
439
  this.cleanup(false);
397
- launchOpenCode(worktree.path);
440
+ launchCommand(worktree.path, this.repoConfig.launchCommand);
398
441
  }
399
442
 
400
443
  private openWorktreeInFileManager(): void {
@@ -404,9 +447,8 @@ class WorktreeSelector {
404
447
  return;
405
448
  }
406
449
 
407
- // Load config to check for custom open command
408
- const config = this.repoRoot ? loadRepoConfig(this.repoRoot) : {};
409
- const customCommand = config.openCommand;
450
+ // Use the already-loaded config's openCommand
451
+ const customCommand = this.repoConfig.openCommand;
410
452
 
411
453
  const success = openInFileManager(worktree.path, customCommand);
412
454
  if (success) {
@@ -489,7 +531,7 @@ class WorktreeSelector {
489
531
 
490
532
  this.selectElement.visible = true;
491
533
  this.instructions.content =
492
- "↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
534
+ "↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
493
535
  this.selectElement.focus();
494
536
  this.loadWorktrees(selectWorktreePath);
495
537
  }
@@ -515,16 +557,15 @@ class WorktreeSelector {
515
557
  if (result.success) {
516
558
  this.setStatus(`Worktree created at ${result.path}`, "success");
517
559
 
518
- // Check for post-create hook
519
- const config = loadRepoConfig(this.repoRoot);
520
- if (config.postCreateHook) {
560
+ // Check for post-create hook (use already-loaded config)
561
+ if (this.repoConfig.postCreateHook) {
521
562
  this.pendingWorktreePath = result.path;
522
- this.runHook(result.path, config.postCreateHook);
563
+ this.runHook(result.path, this.repoConfig.postCreateHook);
523
564
  } else {
524
- // No hook, launch opencode directly
565
+ // No hook, launch command directly
525
566
  this.hideCreateWorktreeInput();
526
567
  this.cleanup(false);
527
- launchOpenCode(result.path);
568
+ launchCommand(result.path, this.repoConfig.launchCommand);
528
569
  }
529
570
  } else {
530
571
  this.setStatus(`Failed to create worktree: ${result.error}`, "error");
@@ -610,12 +651,12 @@ class WorktreeSelector {
610
651
  this.setStatus("Hook completed successfully!", "success");
611
652
  this.renderer.requestRender();
612
653
 
613
- // Brief delay to show success, then launch opencode
654
+ // Brief delay to show success, then launch command
614
655
  setTimeout(() => {
615
656
  this.hideHookOutput();
616
657
  if (this.pendingWorktreePath) {
617
658
  this.cleanup(false);
618
- launchOpenCode(this.pendingWorktreePath);
659
+ launchCommand(this.pendingWorktreePath, this.repoConfig.launchCommand);
619
660
  }
620
661
  }, 1000);
621
662
  }
@@ -676,7 +717,7 @@ class WorktreeSelector {
676
717
  if (choice === "open" && this.pendingWorktreePath) {
677
718
  this.hideHookOutput();
678
719
  this.cleanup(false);
679
- launchOpenCode(this.pendingWorktreePath);
720
+ launchCommand(this.pendingWorktreePath, this.repoConfig.launchCommand);
680
721
  } else {
681
722
  // Cancel - return to list
682
723
  this.hideHookOutput();
@@ -684,7 +725,7 @@ class WorktreeSelector {
684
725
  this.selectElement.visible = true;
685
726
  this.selectElement.focus();
686
727
  this.instructions.content =
687
- "↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
728
+ "↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
688
729
  }
689
730
  }
690
731
 
@@ -708,11 +749,6 @@ class WorktreeSelector {
708
749
 
709
750
  // ========== Config Editor Methods ==========
710
751
 
711
- private showFirstTimeSetup(): void {
712
- this.isFirstTimeSetup = true;
713
- this.showConfigEditor();
714
- }
715
-
716
752
  private showConfigEditor(): void {
717
753
  if (!this.repoRoot) {
718
754
  this.setStatus("No git repository found.", "error");
@@ -724,12 +760,18 @@ class WorktreeSelector {
724
760
  this.selectElement.visible = false;
725
761
  this.selectElement.blur();
726
762
 
727
- // Load existing config to pre-fill
728
- const existingConfig = loadRepoConfig(this.repoRoot);
729
-
730
- const title = this.isFirstTimeSetup
731
- ? "First-time Setup: Project Configuration"
732
- : "Edit Project Configuration";
763
+ // Build title showing repo key
764
+ let title: string;
765
+ if (this.repoKey) {
766
+ // Truncate if too long for the box
767
+ const maxKeyLen = 50;
768
+ const displayKey = this.repoKey.length > maxKeyLen
769
+ ? "..." + this.repoKey.slice(-maxKeyLen + 3)
770
+ : this.repoKey;
771
+ title = `Config: ${displayKey}`;
772
+ } else {
773
+ title = "Config: [no remote]";
774
+ }
733
775
 
734
776
  this.configContainer = new BoxRenderable(this.renderer, {
735
777
  id: "config-container",
@@ -737,7 +779,7 @@ class WorktreeSelector {
737
779
  left: 2,
738
780
  top: 3,
739
781
  width: 76,
740
- height: 12,
782
+ height: 15,
741
783
  borderStyle: "single",
742
784
  borderColor: "#38BDF8",
743
785
  title,
@@ -765,19 +807,19 @@ class WorktreeSelector {
765
807
  top: 2,
766
808
  width: 72,
767
809
  placeholder: "npm install",
768
- value: existingConfig.postCreateHook || "",
810
+ value: this.repoConfig.postCreateHook || "",
769
811
  focusedBackgroundColor: "#1E293B",
770
812
  backgroundColor: "#1E293B",
771
813
  });
772
814
  this.configContainer.add(this.configHookInput);
773
815
 
774
- // Open command field
816
+ // Open folder command field
775
817
  const openLabel = new TextRenderable(this.renderer, {
776
818
  id: "config-open-label",
777
819
  position: "absolute",
778
820
  left: 1,
779
821
  top: 4,
780
- content: "Open folder command (e.g., webstorm, code):",
822
+ content: "Open folder command (e.g., code, webstorm):",
781
823
  fg: "#94A3B8",
782
824
  });
783
825
  this.configContainer.add(openLabel);
@@ -789,30 +831,62 @@ class WorktreeSelector {
789
831
  top: 5,
790
832
  width: 72,
791
833
  placeholder: "open (default)",
792
- value: existingConfig.openCommand || "",
834
+ value: this.repoConfig.openCommand || "",
793
835
  focusedBackgroundColor: "#1E293B",
794
836
  backgroundColor: "#1E293B",
795
837
  });
796
838
  this.configContainer.add(this.configOpenInput);
797
839
 
798
- // Help text
840
+ // Launch command field (instead of opencode)
841
+ const launchLabel = new TextRenderable(this.renderer, {
842
+ id: "config-launch-label",
843
+ position: "absolute",
844
+ left: 1,
845
+ top: 7,
846
+ content: "Launch command (e.g., cursor, claude, code):",
847
+ fg: "#94A3B8",
848
+ });
849
+ this.configContainer.add(launchLabel);
850
+
851
+ this.configLaunchInput = new InputRenderable(this.renderer, {
852
+ id: "config-launch-input",
853
+ position: "absolute",
854
+ left: 1,
855
+ top: 8,
856
+ width: 72,
857
+ placeholder: "opencode (default)",
858
+ value: this.repoConfig.launchCommand || "",
859
+ focusedBackgroundColor: "#1E293B",
860
+ backgroundColor: "#1E293B",
861
+ });
862
+ this.configContainer.add(this.configLaunchInput);
863
+
864
+ // Help text - show warning if no remote
865
+ let helpContent: string;
866
+ if (this.repoKey) {
867
+ helpContent = "Tab to switch fields • Leave empty to use defaults";
868
+ } else {
869
+ helpContent = "No remote - config won't be saved for this repo";
870
+ }
871
+
799
872
  const helpText = new TextRenderable(this.renderer, {
800
873
  id: "config-help",
801
874
  position: "absolute",
802
875
  left: 1,
803
- top: 7,
804
- content: "Tab to switch fields • Leave empty to use defaults",
805
- fg: "#64748B",
876
+ top: 10,
877
+ content: helpContent,
878
+ fg: this.repoKey ? "#64748B" : "#F59E0B",
806
879
  });
807
880
  this.configContainer.add(helpText);
808
881
 
809
882
  this.instructions.content = "Tab switch • Enter save • Esc cancel";
810
- this.setStatus(
811
- this.isFirstTimeSetup
812
- ? "Welcome! Configure your project settings."
813
- : "Edit project configuration.",
814
- "info"
815
- );
883
+
884
+ // Show warning if no remote
885
+ if (this.repoKey) {
886
+ this.setStatus("Edit project configuration.", "info");
887
+ } else {
888
+ this.setStatus("Warning: No git remote. Config changes won't be saved.", "warning");
889
+ }
816
890
 
817
891
  // Delay focus to prevent the triggering keypress from being captured
818
892
  setTimeout(() => {
@@ -823,7 +897,6 @@ class WorktreeSelector {
823
897
 
824
898
  private hideConfigEditor(): void {
825
899
  this.isEditingConfig = false;
826
- this.isFirstTimeSetup = false;
827
900
 
828
901
  if (this.configHookInput) {
829
902
  this.configHookInput.blur();
@@ -831,17 +904,21 @@ class WorktreeSelector {
831
904
  if (this.configOpenInput) {
832
905
  this.configOpenInput.blur();
833
906
  }
907
+ if (this.configLaunchInput) {
908
+ this.configLaunchInput.blur();
909
+ }
834
910
 
835
911
  if (this.configContainer) {
836
912
  this.renderer.root.remove(this.configContainer.id);
837
913
  this.configContainer = null;
838
914
  this.configHookInput = null;
839
915
  this.configOpenInput = null;
916
+ this.configLaunchInput = null;
840
917
  }
841
918
 
842
919
  this.selectElement.visible = true;
843
920
  this.instructions.content =
844
- "↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
921
+ "↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
845
922
 
846
923
  // Delay focus to prevent the Enter keypress from triggering a selection
847
924
  setTimeout(() => {
@@ -859,6 +936,7 @@ class WorktreeSelector {
859
936
 
860
937
  const hookValue = (this.configHookInput?.value || "").trim();
861
938
  const openValue = (this.configOpenInput?.value || "").trim();
939
+ const launchValue = (this.configLaunchInput?.value || "").trim();
862
940
  const config: Config = {};
863
941
 
864
942
  if (hookValue) {
@@ -867,13 +945,24 @@ class WorktreeSelector {
867
945
  if (openValue) {
868
946
  config.openCommand = openValue;
869
947
  }
948
+ if (launchValue) {
949
+ config.launchCommand = launchValue;
950
+ }
870
951
 
871
952
  const success = saveRepoConfig(this.repoRoot, config);
872
953
 
873
954
  if (success) {
955
+ // Update the in-memory config
956
+ this.repoConfig = config;
957
+
958
+ // Re-check if the launch command is available
959
+ const cmdName = config.launchCommand || "opencode";
960
+ this.opencodeAvailable = isCommandAvailable(cmdName);
961
+
874
962
  const changes: string[] = [];
875
963
  if (hookValue) changes.push(`hook: "${hookValue}"`);
876
964
  if (openValue) changes.push(`open: "${openValue}"`);
965
+ if (launchValue) changes.push(`launch: "${launchValue}"`);
877
966
 
878
967
  if (changes.length > 0) {
879
968
  this.setStatus(`Config saved: ${changes.join(", ")}`, "success");
@@ -887,6 +976,279 @@ class WorktreeSelector {
887
976
  this.hideConfigEditor();
888
977
  }
889
978
 
979
+ // ========== Branch Creation Methods ==========
980
+
981
+ private showBranchCreateInput(): void {
982
+ const worktree = this.getSelectedWorktree();
983
+ if (!worktree) {
984
+ this.setStatus("Select a worktree to create a branch from.", "warning");
985
+ return;
986
+ }
987
+
988
+ if (!this.repoRoot) {
989
+ this.setStatus("No git repository found.", "error");
990
+ return;
991
+ }
992
+
993
+ this.isCreatingBranch = true;
994
+ this.sourceWorktree = worktree;
995
+ this.selectElement.visible = false;
996
+ this.selectElement.blur();
997
+
998
+ const sourceName = worktree.branch || basename(worktree.path);
999
+
1000
+ this.branchCreateContainer = new BoxRenderable(this.renderer, {
1001
+ id: "branch-create-container",
1002
+ position: "absolute",
1003
+ left: 2,
1004
+ top: 3,
1005
+ width: 76,
1006
+ height: 7,
1007
+ borderStyle: "single",
1008
+ borderColor: "#38BDF8",
1009
+ title: `New Branch from: ${sourceName}`,
1010
+ titleAlignment: "center",
1011
+ backgroundColor: "#0F172A",
1012
+ border: true,
1013
+ });
1014
+ this.renderer.root.add(this.branchCreateContainer);
1015
+
1016
+ const inputLabel = new TextRenderable(this.renderer, {
1017
+ id: "branch-name-label",
1018
+ position: "absolute",
1019
+ left: 1,
1020
+ top: 1,
1021
+ content: "Branch name:",
1022
+ fg: "#E2E8F0",
1023
+ });
1024
+ this.branchCreateContainer.add(inputLabel);
1025
+
1026
+ this.branchNameInput = new InputRenderable(this.renderer, {
1027
+ id: "branch-name-input",
1028
+ position: "absolute",
1029
+ left: 14,
1030
+ top: 1,
1031
+ width: 58,
1032
+ placeholder: "feature/new-branch",
1033
+ focusedBackgroundColor: "#1E293B",
1034
+ backgroundColor: "#1E293B",
1035
+ });
1036
+ this.branchCreateContainer.add(this.branchNameInput);
1037
+
1038
+ const helpText = new TextRenderable(this.renderer, {
1039
+ id: "branch-create-help",
1040
+ position: "absolute",
1041
+ left: 1,
1042
+ top: 3,
1043
+ content: `Branch will start from commit: ${worktree.head.slice(0, 8)}`,
1044
+ fg: "#64748B",
1045
+ });
1046
+ this.branchCreateContainer.add(helpText);
1047
+
1048
+ this.branchNameInput.on(InputRenderableEvents.CHANGE, (value: string) => {
1049
+ this.handleBranchCreate(value);
1050
+ });
1051
+
1052
+ this.instructions.content = "Enter to create • Esc to cancel";
1053
+ this.setStatus("Enter a name for the new branch.", "info");
1054
+
1055
+ this.branchNameInput.focus();
1056
+ this.renderer.requestRender();
1057
+ }
1058
+
1059
+ private hideBranchCreateInput(): void {
1060
+ this.isCreatingBranch = false;
1061
+ this.sourceWorktree = null;
1062
+
1063
+ if (this.branchNameInput) {
1064
+ this.branchNameInput.blur();
1065
+ }
1066
+
1067
+ if (this.branchCreateContainer) {
1068
+ this.renderer.root.remove(this.branchCreateContainer.id);
1069
+ this.branchCreateContainer = null;
1070
+ this.branchNameInput = null;
1071
+ }
1072
+
1073
+ this.selectElement.visible = true;
1074
+ this.instructions.content =
1075
+ "↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
1076
+ this.selectElement.focus();
1077
+ this.renderer.requestRender();
1078
+ }
1079
+
1080
+ private handleBranchCreate(branchName: string): void {
1081
+ const trimmed = branchName.trim();
1082
+ if (!trimmed) {
1083
+ this.setStatus("Branch name cannot be empty.", "error");
1084
+ return;
1085
+ }
1086
+
1087
+ if (!this.repoRoot || !this.sourceWorktree) {
1088
+ this.setStatus("No source worktree selected.", "error");
1089
+ return;
1090
+ }
1091
+
1092
+ const commitHash = this.sourceWorktree.head;
1093
+ if (!commitHash) {
1094
+ this.setStatus("Could not determine source commit.", "error");
1095
+ return;
1096
+ }
1097
+
1098
+ this.setStatus(`Creating branch '${trimmed}'...`, "info");
1099
+ this.renderer.requestRender();
1100
+
1101
+ const result = createBranchFromCommit(this.repoRoot, trimmed, commitHash);
1102
+
1103
+ if (result.success) {
1104
+ this.pendingBranchName = trimmed;
1105
+ // Hide the input and show checkout confirmation
1106
+ if (this.branchNameInput) {
1107
+ this.branchNameInput.blur();
1108
+ }
1109
+ if (this.branchCreateContainer) {
1110
+ this.renderer.root.remove(this.branchCreateContainer.id);
1111
+ this.branchCreateContainer = null;
1112
+ this.branchNameInput = null;
1113
+ }
1114
+ this.isCreatingBranch = false;
1115
+
1116
+ this.showCheckoutConfirm(trimmed);
1117
+ } else {
1118
+ this.setStatus(`Failed to create branch: ${result.error}`, "error");
1119
+ }
1120
+ }
1121
+
1122
+ private showCheckoutConfirm(branchName: string): void {
1123
+ if (!this.sourceWorktree) {
1124
+ this.setStatus("No source worktree.", "error");
1125
+ this.hideBranchCreateInput();
1126
+ return;
1127
+ }
1128
+
1129
+ this.isAskingCheckout = true;
1130
+
1131
+ const sourceName = this.sourceWorktree.branch || basename(this.sourceWorktree.path);
1132
+
1133
+ this.branchCreateContainer = new BoxRenderable(this.renderer, {
1134
+ id: "checkout-confirm-container",
1135
+ position: "absolute",
1136
+ left: 2,
1137
+ top: 3,
1138
+ width: 76,
1139
+ height: 8,
1140
+ borderStyle: "single",
1141
+ borderColor: "#10B981",
1142
+ title: `Branch '${branchName}' Created`,
1143
+ titleAlignment: "center",
1144
+ backgroundColor: "#0F172A",
1145
+ border: true,
1146
+ });
1147
+ this.renderer.root.add(this.branchCreateContainer);
1148
+
1149
+ const infoText = new TextRenderable(this.renderer, {
1150
+ id: "checkout-info",
1151
+ position: "absolute",
1152
+ left: 1,
1153
+ top: 1,
1154
+ content: `Checkout '${branchName}' in worktree '${sourceName}'?`,
1155
+ fg: "#E2E8F0",
1156
+ });
1157
+ this.branchCreateContainer.add(infoText);
1158
+
1159
+ this.checkoutSelect = new SelectRenderable(this.renderer, {
1160
+ id: "checkout-select",
1161
+ position: "absolute",
1162
+ left: 1,
1163
+ top: 3,
1164
+ width: 72,
1165
+ height: 3,
1166
+ options: [
1167
+ {
1168
+ name: "Yes, checkout the new branch",
1169
+ description: "Switch this worktree to the new branch",
1170
+ value: "checkout",
1171
+ },
1172
+ {
1173
+ name: "No, keep current branch",
1174
+ description: "Branch created but worktree stays on current branch",
1175
+ value: "keep",
1176
+ },
1177
+ ],
1178
+ backgroundColor: "#0F172A",
1179
+ focusedBackgroundColor: "#1E293B",
1180
+ selectedBackgroundColor: "#1E3A5F",
1181
+ textColor: "#E2E8F0",
1182
+ selectedTextColor: "#38BDF8",
1183
+ descriptionColor: "#94A3B8",
1184
+ selectedDescriptionColor: "#E2E8F0",
1185
+ showDescription: true,
1186
+ wrapSelection: true,
1187
+ });
1188
+ this.branchCreateContainer.add(this.checkoutSelect);
1189
+
1190
+ this.checkoutSelect.on(
1191
+ SelectRenderableEvents.ITEM_SELECTED,
1192
+ (_index: number, option: SelectOption) => {
1193
+ this.handleCheckoutChoice(option.value as string);
1194
+ }
1195
+ );
1196
+
1197
+ this.instructions.content = "↑/↓ select • Enter confirm • Esc cancel";
1198
+ this.setStatus(`Branch '${branchName}' created successfully!`, "success");
1199
+
1200
+ this.checkoutSelect.focus();
1201
+ this.renderer.requestRender();
1202
+ }
1203
+
1204
+ private hideCheckoutConfirm(): void {
1205
+ this.isAskingCheckout = false;
1206
+ this.pendingBranchName = null;
1207
+ this.sourceWorktree = null;
1208
+
1209
+ if (this.checkoutSelect) {
1210
+ this.checkoutSelect.blur();
1211
+ this.checkoutSelect = null;
1212
+ }
1213
+
1214
+ if (this.branchCreateContainer) {
1215
+ this.renderer.root.remove(this.branchCreateContainer.id);
1216
+ this.branchCreateContainer = null;
1217
+ }
1218
+
1219
+ this.selectElement.visible = true;
1220
+ this.instructions.content =
1221
+ "↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
1222
+ this.selectElement.focus();
1223
+ this.loadWorktrees();
1224
+ this.renderer.requestRender();
1225
+ }
1226
+
1227
+ private handleCheckoutChoice(choice: string): void {
1228
+ if (choice === "checkout" && this.pendingBranchName && this.sourceWorktree) {
1229
+ this.setStatus(`Checking out '${this.pendingBranchName}'...`, "info");
1230
+ this.renderer.requestRender();
1231
+
1232
+ const result = checkoutBranch(this.sourceWorktree.path, this.pendingBranchName);
1233
+
1234
+ if (result.success) {
1235
+ this.setStatus(
1236
+ `Switched to branch '${this.pendingBranchName}'.`,
1237
+ "success"
1238
+ );
1239
+ } else {
1240
+ this.setStatus(`Failed to checkout: ${result.error}`, "error");
1241
+ }
1242
+ } else {
1243
+ this.setStatus(
1244
+ `Branch '${this.pendingBranchName}' created (not checked out).`,
1245
+ "success"
1246
+ );
1247
+ }
1248
+
1249
+ this.hideCheckoutConfirm();
1250
+ }
1251
+
890
1252
  private loadWorktrees(selectWorktreePath?: string): void {
891
1253
  this.repoRoot = resolveRepoRoot(this.targetPath);
892
1254
  if (!this.repoRoot) {
@@ -922,9 +1284,10 @@ class WorktreeSelector {
922
1284
  );
923
1285
  }
924
1286
 
925
- this.opencodeAvailable = isOpenCodeAvailable();
1287
+ const cmdName = this.repoConfig.launchCommand || "opencode";
1288
+ this.opencodeAvailable = isCommandAvailable(cmdName);
926
1289
  if (!this.opencodeAvailable) {
927
- this.setStatus("opencode is not available on PATH.", "error");
1290
+ this.setStatus(`${cmdName} is not available on PATH.`, "error");
928
1291
  }
929
1292
 
930
1293
  this.renderer.requestRender();
@@ -1193,7 +1556,7 @@ class WorktreeSelector {
1193
1556
 
1194
1557
  this.selectElement.visible = true;
1195
1558
  this.instructions.content =
1196
- "↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
1559
+ "↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
1197
1560
  this.selectElement.focus();
1198
1561
  this.loadWorktrees();
1199
1562
  }
@@ -1322,7 +1685,7 @@ class WorktreeSelector {
1322
1685
 
1323
1686
  this.loadWorktrees();
1324
1687
  this.instructions.content =
1325
- "↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
1688
+ "↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
1326
1689
  this.renderer.requestRender();
1327
1690
  }
1328
1691