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/README.md +76 -46
- package/package.json +1 -1
- package/src/config.ts +169 -30
- package/src/git.ts +131 -0
- package/src/opencode.ts +27 -4
- package/src/types.ts +26 -0
- package/src/ui.ts +425 -62
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 {
|
|
28
|
+
import { isCommandAvailable, launchCommand, openInFileManager } from "./opencode.js";
|
|
26
29
|
import { WorktreeInfo } from "./types.js";
|
|
27
|
-
import { loadRepoConfig, saveRepoConfig,
|
|
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
|
|
107
|
-
private
|
|
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.
|
|
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
|
-
//
|
|
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.
|
|
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(
|
|
435
|
+
this.setStatus(`${cmdName} is not available on PATH.`, "error");
|
|
393
436
|
return;
|
|
394
437
|
}
|
|
395
438
|
|
|
396
439
|
this.cleanup(false);
|
|
397
|
-
|
|
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
|
-
//
|
|
408
|
-
const
|
|
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
|
-
|
|
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,
|
|
563
|
+
this.runHook(result.path, this.repoConfig.postCreateHook);
|
|
523
564
|
} else {
|
|
524
|
-
// No hook, launch
|
|
565
|
+
// No hook, launch command directly
|
|
525
566
|
this.hideCreateWorktreeInput();
|
|
526
567
|
this.cleanup(false);
|
|
527
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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:
|
|
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:
|
|
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.,
|
|
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:
|
|
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
|
-
//
|
|
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:
|
|
804
|
-
content:
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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.
|
|
1287
|
+
const cmdName = this.repoConfig.launchCommand || "opencode";
|
|
1288
|
+
this.opencodeAvailable = isCommandAvailable(cmdName);
|
|
926
1289
|
if (!this.opencodeAvailable) {
|
|
927
|
-
this.setStatus(
|
|
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
|
|