opencode-worktree 0.3.5 → 0.4.1
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 +60 -75
- package/package.json +1 -1
- package/src/config.ts +168 -34
- package/src/git.ts +131 -0
- package/src/types.ts +26 -0
- package/src/ui.ts +368 -58
- package/src/update-check.ts +74 -94
package/src/ui.ts
CHANGED
|
@@ -10,12 +10,15 @@ import {
|
|
|
10
10
|
type KeyEvent,
|
|
11
11
|
type SelectOption,
|
|
12
12
|
} from "@opentui/core";
|
|
13
|
-
import {
|
|
13
|
+
import { checkForUpdatesOnLaunch, getCachedUpdateNotice } 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,
|
|
@@ -24,7 +27,7 @@ import {
|
|
|
24
27
|
} from "./git.js";
|
|
25
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";
|
|
@@ -106,7 +109,16 @@ class WorktreeSelector {
|
|
|
106
109
|
private configOpenInput: InputRenderable | null = null;
|
|
107
110
|
private configLaunchInput: InputRenderable | null = null;
|
|
108
111
|
private configActiveField: "hook" | "open" | "launch" = "hook";
|
|
109
|
-
private
|
|
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;
|
|
110
122
|
|
|
111
123
|
constructor(
|
|
112
124
|
private renderer: CliRenderer,
|
|
@@ -115,7 +127,11 @@ class WorktreeSelector {
|
|
|
115
127
|
) {
|
|
116
128
|
// Load worktrees first to get initial options
|
|
117
129
|
this.repoRoot = resolveRepoRoot(this.targetPath);
|
|
118
|
-
|
|
130
|
+
if (this.repoRoot) {
|
|
131
|
+
const { config, repoKey } = loadRepoConfig(this.repoRoot);
|
|
132
|
+
this.repoConfig = config;
|
|
133
|
+
this.repoKey = repoKey;
|
|
134
|
+
}
|
|
119
135
|
this.opencodeAvailable = isCommandAvailable(this.repoConfig.launchCommand || "opencode");
|
|
120
136
|
this.worktreeOptions = this.buildInitialOptions();
|
|
121
137
|
|
|
@@ -129,21 +145,17 @@ class WorktreeSelector {
|
|
|
129
145
|
});
|
|
130
146
|
this.renderer.root.add(this.title);
|
|
131
147
|
|
|
132
|
-
// Display version
|
|
148
|
+
// Display version/update notice from previous launch, then refresh cache now.
|
|
133
149
|
if (this.pkg) {
|
|
134
|
-
const updateInfo =
|
|
150
|
+
const updateInfo = getCachedUpdateNotice(this.pkg);
|
|
151
|
+
checkForUpdatesOnLaunch(this.pkg);
|
|
135
152
|
|
|
136
|
-
let noticeContent
|
|
137
|
-
let noticeColor
|
|
153
|
+
let noticeContent = `v${this.pkg.version}`;
|
|
154
|
+
let noticeColor = "#64748B";
|
|
138
155
|
|
|
139
156
|
if (updateInfo?.hasUpdate) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
noticeColor = "#F59E0B"; // Amber
|
|
143
|
-
} else {
|
|
144
|
-
// On latest version (or no cache yet)
|
|
145
|
-
noticeContent = `v${this.pkg.version}`;
|
|
146
|
-
noticeColor = "#64748B"; // Subtle gray
|
|
157
|
+
noticeContent = `Update: ${updateInfo.current} -> ${updateInfo.latest} (npm i -g)`;
|
|
158
|
+
noticeColor = "#F59E0B";
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
this.versionNotice = new TextRenderable(renderer, {
|
|
@@ -195,7 +207,7 @@ class WorktreeSelector {
|
|
|
195
207
|
left: 2,
|
|
196
208
|
top: 20,
|
|
197
209
|
content:
|
|
198
|
-
"↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit",
|
|
210
|
+
"↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit",
|
|
199
211
|
fg: "#64748B",
|
|
200
212
|
});
|
|
201
213
|
this.renderer.root.add(this.instructions);
|
|
@@ -216,11 +228,6 @@ class WorktreeSelector {
|
|
|
216
228
|
});
|
|
217
229
|
|
|
218
230
|
this.selectElement.focus();
|
|
219
|
-
|
|
220
|
-
// Check for first-time setup
|
|
221
|
-
if (this.repoRoot && !configExists(this.repoRoot)) {
|
|
222
|
-
this.showFirstTimeSetup();
|
|
223
|
-
}
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
private getInitialStatusMessage(): string {
|
|
@@ -265,7 +272,7 @@ class WorktreeSelector {
|
|
|
265
272
|
this.selectElement.visible = true;
|
|
266
273
|
this.selectElement.focus();
|
|
267
274
|
this.instructions.content =
|
|
268
|
-
"↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
|
|
275
|
+
"↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
|
|
269
276
|
return;
|
|
270
277
|
}
|
|
271
278
|
this.cleanup(true);
|
|
@@ -353,6 +360,24 @@ class WorktreeSelector {
|
|
|
353
360
|
return;
|
|
354
361
|
}
|
|
355
362
|
|
|
363
|
+
// Handle branch creation mode
|
|
364
|
+
if (this.isCreatingBranch) {
|
|
365
|
+
if (key.name === "escape") {
|
|
366
|
+
this.hideBranchCreateInput();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Handle checkout confirmation mode
|
|
373
|
+
if (this.isAskingCheckout) {
|
|
374
|
+
if (key.name === "escape") {
|
|
375
|
+
this.hideCheckoutConfirm();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
356
381
|
if (key.name === "q" || key.name === "escape") {
|
|
357
382
|
this.cleanup(true);
|
|
358
383
|
return;
|
|
@@ -386,6 +411,12 @@ class WorktreeSelector {
|
|
|
386
411
|
this.showConfigEditor();
|
|
387
412
|
return;
|
|
388
413
|
}
|
|
414
|
+
|
|
415
|
+
// 'b' for creating a new branch from selected worktree
|
|
416
|
+
if (key.name === "b") {
|
|
417
|
+
this.showBranchCreateInput();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
389
420
|
}
|
|
390
421
|
|
|
391
422
|
private handleSelection(value: SelectionValue): void {
|
|
@@ -412,9 +443,8 @@ class WorktreeSelector {
|
|
|
412
443
|
return;
|
|
413
444
|
}
|
|
414
445
|
|
|
415
|
-
//
|
|
416
|
-
const
|
|
417
|
-
const customCommand = config.openCommand;
|
|
446
|
+
// Use the already-loaded config's openCommand
|
|
447
|
+
const customCommand = this.repoConfig.openCommand;
|
|
418
448
|
|
|
419
449
|
const success = openInFileManager(worktree.path, customCommand);
|
|
420
450
|
if (success) {
|
|
@@ -497,7 +527,7 @@ class WorktreeSelector {
|
|
|
497
527
|
|
|
498
528
|
this.selectElement.visible = true;
|
|
499
529
|
this.instructions.content =
|
|
500
|
-
"↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
|
|
530
|
+
"↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
|
|
501
531
|
this.selectElement.focus();
|
|
502
532
|
this.loadWorktrees(selectWorktreePath);
|
|
503
533
|
}
|
|
@@ -523,11 +553,10 @@ class WorktreeSelector {
|
|
|
523
553
|
if (result.success) {
|
|
524
554
|
this.setStatus(`Worktree created at ${result.path}`, "success");
|
|
525
555
|
|
|
526
|
-
// Check for post-create hook
|
|
527
|
-
|
|
528
|
-
if (config.postCreateHook) {
|
|
556
|
+
// Check for post-create hook (use already-loaded config)
|
|
557
|
+
if (this.repoConfig.postCreateHook) {
|
|
529
558
|
this.pendingWorktreePath = result.path;
|
|
530
|
-
this.runHook(result.path,
|
|
559
|
+
this.runHook(result.path, this.repoConfig.postCreateHook);
|
|
531
560
|
} else {
|
|
532
561
|
// No hook, launch command directly
|
|
533
562
|
this.hideCreateWorktreeInput();
|
|
@@ -692,7 +721,7 @@ class WorktreeSelector {
|
|
|
692
721
|
this.selectElement.visible = true;
|
|
693
722
|
this.selectElement.focus();
|
|
694
723
|
this.instructions.content =
|
|
695
|
-
"↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
|
|
724
|
+
"↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
|
|
696
725
|
}
|
|
697
726
|
}
|
|
698
727
|
|
|
@@ -716,11 +745,6 @@ class WorktreeSelector {
|
|
|
716
745
|
|
|
717
746
|
// ========== Config Editor Methods ==========
|
|
718
747
|
|
|
719
|
-
private showFirstTimeSetup(): void {
|
|
720
|
-
this.isFirstTimeSetup = true;
|
|
721
|
-
this.showConfigEditor();
|
|
722
|
-
}
|
|
723
|
-
|
|
724
748
|
private showConfigEditor(): void {
|
|
725
749
|
if (!this.repoRoot) {
|
|
726
750
|
this.setStatus("No git repository found.", "error");
|
|
@@ -732,12 +756,18 @@ class WorktreeSelector {
|
|
|
732
756
|
this.selectElement.visible = false;
|
|
733
757
|
this.selectElement.blur();
|
|
734
758
|
|
|
735
|
-
//
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
759
|
+
// Build title showing repo key
|
|
760
|
+
let title: string;
|
|
761
|
+
if (this.repoKey) {
|
|
762
|
+
// Truncate if too long for the box
|
|
763
|
+
const maxKeyLen = 50;
|
|
764
|
+
const displayKey = this.repoKey.length > maxKeyLen
|
|
765
|
+
? "..." + this.repoKey.slice(-maxKeyLen + 3)
|
|
766
|
+
: this.repoKey;
|
|
767
|
+
title = `Config: ${displayKey}`;
|
|
768
|
+
} else {
|
|
769
|
+
title = "Config: [no remote]";
|
|
770
|
+
}
|
|
741
771
|
|
|
742
772
|
this.configContainer = new BoxRenderable(this.renderer, {
|
|
743
773
|
id: "config-container",
|
|
@@ -773,7 +803,7 @@ class WorktreeSelector {
|
|
|
773
803
|
top: 2,
|
|
774
804
|
width: 72,
|
|
775
805
|
placeholder: "npm install",
|
|
776
|
-
value:
|
|
806
|
+
value: this.repoConfig.postCreateHook || "",
|
|
777
807
|
focusedBackgroundColor: "#1E293B",
|
|
778
808
|
backgroundColor: "#1E293B",
|
|
779
809
|
});
|
|
@@ -797,7 +827,7 @@ class WorktreeSelector {
|
|
|
797
827
|
top: 5,
|
|
798
828
|
width: 72,
|
|
799
829
|
placeholder: "open (default)",
|
|
800
|
-
value:
|
|
830
|
+
value: this.repoConfig.openCommand || "",
|
|
801
831
|
focusedBackgroundColor: "#1E293B",
|
|
802
832
|
backgroundColor: "#1E293B",
|
|
803
833
|
});
|
|
@@ -821,30 +851,38 @@ class WorktreeSelector {
|
|
|
821
851
|
top: 8,
|
|
822
852
|
width: 72,
|
|
823
853
|
placeholder: "opencode (default)",
|
|
824
|
-
value:
|
|
854
|
+
value: this.repoConfig.launchCommand || "",
|
|
825
855
|
focusedBackgroundColor: "#1E293B",
|
|
826
856
|
backgroundColor: "#1E293B",
|
|
827
857
|
});
|
|
828
858
|
this.configContainer.add(this.configLaunchInput);
|
|
829
859
|
|
|
830
|
-
// Help text
|
|
860
|
+
// Help text - show warning if no remote
|
|
861
|
+
let helpContent: string;
|
|
862
|
+
if (this.repoKey) {
|
|
863
|
+
helpContent = "Tab to switch fields • Leave empty to use defaults";
|
|
864
|
+
} else {
|
|
865
|
+
helpContent = "No remote - config won't be saved for this repo";
|
|
866
|
+
}
|
|
867
|
+
|
|
831
868
|
const helpText = new TextRenderable(this.renderer, {
|
|
832
869
|
id: "config-help",
|
|
833
870
|
position: "absolute",
|
|
834
871
|
left: 1,
|
|
835
872
|
top: 10,
|
|
836
|
-
content:
|
|
837
|
-
fg: "#64748B",
|
|
873
|
+
content: helpContent,
|
|
874
|
+
fg: this.repoKey ? "#64748B" : "#F59E0B",
|
|
838
875
|
});
|
|
839
876
|
this.configContainer.add(helpText);
|
|
840
877
|
|
|
841
878
|
this.instructions.content = "Tab switch • Enter save • Esc cancel";
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
879
|
+
|
|
880
|
+
// Show warning if no remote
|
|
881
|
+
if (this.repoKey) {
|
|
882
|
+
this.setStatus("Edit project configuration.", "info");
|
|
883
|
+
} else {
|
|
884
|
+
this.setStatus("Warning: No git remote. Config changes won't be saved.", "warning");
|
|
885
|
+
}
|
|
848
886
|
|
|
849
887
|
// Delay focus to prevent the triggering keypress from being captured
|
|
850
888
|
setTimeout(() => {
|
|
@@ -855,7 +893,6 @@ class WorktreeSelector {
|
|
|
855
893
|
|
|
856
894
|
private hideConfigEditor(): void {
|
|
857
895
|
this.isEditingConfig = false;
|
|
858
|
-
this.isFirstTimeSetup = false;
|
|
859
896
|
|
|
860
897
|
if (this.configHookInput) {
|
|
861
898
|
this.configHookInput.blur();
|
|
@@ -877,7 +914,7 @@ class WorktreeSelector {
|
|
|
877
914
|
|
|
878
915
|
this.selectElement.visible = true;
|
|
879
916
|
this.instructions.content =
|
|
880
|
-
"↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
|
|
917
|
+
"↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
|
|
881
918
|
|
|
882
919
|
// Delay focus to prevent the Enter keypress from triggering a selection
|
|
883
920
|
setTimeout(() => {
|
|
@@ -935,6 +972,279 @@ class WorktreeSelector {
|
|
|
935
972
|
this.hideConfigEditor();
|
|
936
973
|
}
|
|
937
974
|
|
|
975
|
+
// ========== Branch Creation Methods ==========
|
|
976
|
+
|
|
977
|
+
private showBranchCreateInput(): void {
|
|
978
|
+
const worktree = this.getSelectedWorktree();
|
|
979
|
+
if (!worktree) {
|
|
980
|
+
this.setStatus("Select a worktree to create a branch from.", "warning");
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (!this.repoRoot) {
|
|
985
|
+
this.setStatus("No git repository found.", "error");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
this.isCreatingBranch = true;
|
|
990
|
+
this.sourceWorktree = worktree;
|
|
991
|
+
this.selectElement.visible = false;
|
|
992
|
+
this.selectElement.blur();
|
|
993
|
+
|
|
994
|
+
const sourceName = worktree.branch || basename(worktree.path);
|
|
995
|
+
|
|
996
|
+
this.branchCreateContainer = new BoxRenderable(this.renderer, {
|
|
997
|
+
id: "branch-create-container",
|
|
998
|
+
position: "absolute",
|
|
999
|
+
left: 2,
|
|
1000
|
+
top: 3,
|
|
1001
|
+
width: 76,
|
|
1002
|
+
height: 7,
|
|
1003
|
+
borderStyle: "single",
|
|
1004
|
+
borderColor: "#38BDF8",
|
|
1005
|
+
title: `New Branch from: ${sourceName}`,
|
|
1006
|
+
titleAlignment: "center",
|
|
1007
|
+
backgroundColor: "#0F172A",
|
|
1008
|
+
border: true,
|
|
1009
|
+
});
|
|
1010
|
+
this.renderer.root.add(this.branchCreateContainer);
|
|
1011
|
+
|
|
1012
|
+
const inputLabel = new TextRenderable(this.renderer, {
|
|
1013
|
+
id: "branch-name-label",
|
|
1014
|
+
position: "absolute",
|
|
1015
|
+
left: 1,
|
|
1016
|
+
top: 1,
|
|
1017
|
+
content: "Branch name:",
|
|
1018
|
+
fg: "#E2E8F0",
|
|
1019
|
+
});
|
|
1020
|
+
this.branchCreateContainer.add(inputLabel);
|
|
1021
|
+
|
|
1022
|
+
this.branchNameInput = new InputRenderable(this.renderer, {
|
|
1023
|
+
id: "branch-name-input",
|
|
1024
|
+
position: "absolute",
|
|
1025
|
+
left: 14,
|
|
1026
|
+
top: 1,
|
|
1027
|
+
width: 58,
|
|
1028
|
+
placeholder: "feature/new-branch",
|
|
1029
|
+
focusedBackgroundColor: "#1E293B",
|
|
1030
|
+
backgroundColor: "#1E293B",
|
|
1031
|
+
});
|
|
1032
|
+
this.branchCreateContainer.add(this.branchNameInput);
|
|
1033
|
+
|
|
1034
|
+
const helpText = new TextRenderable(this.renderer, {
|
|
1035
|
+
id: "branch-create-help",
|
|
1036
|
+
position: "absolute",
|
|
1037
|
+
left: 1,
|
|
1038
|
+
top: 3,
|
|
1039
|
+
content: `Branch will start from commit: ${worktree.head.slice(0, 8)}`,
|
|
1040
|
+
fg: "#64748B",
|
|
1041
|
+
});
|
|
1042
|
+
this.branchCreateContainer.add(helpText);
|
|
1043
|
+
|
|
1044
|
+
this.branchNameInput.on(InputRenderableEvents.CHANGE, (value: string) => {
|
|
1045
|
+
this.handleBranchCreate(value);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
this.instructions.content = "Enter to create • Esc to cancel";
|
|
1049
|
+
this.setStatus("Enter a name for the new branch.", "info");
|
|
1050
|
+
|
|
1051
|
+
this.branchNameInput.focus();
|
|
1052
|
+
this.renderer.requestRender();
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private hideBranchCreateInput(): void {
|
|
1056
|
+
this.isCreatingBranch = false;
|
|
1057
|
+
this.sourceWorktree = null;
|
|
1058
|
+
|
|
1059
|
+
if (this.branchNameInput) {
|
|
1060
|
+
this.branchNameInput.blur();
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (this.branchCreateContainer) {
|
|
1064
|
+
this.renderer.root.remove(this.branchCreateContainer.id);
|
|
1065
|
+
this.branchCreateContainer = null;
|
|
1066
|
+
this.branchNameInput = null;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
this.selectElement.visible = true;
|
|
1070
|
+
this.instructions.content =
|
|
1071
|
+
"↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
|
|
1072
|
+
this.selectElement.focus();
|
|
1073
|
+
this.renderer.requestRender();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
private handleBranchCreate(branchName: string): void {
|
|
1077
|
+
const trimmed = branchName.trim();
|
|
1078
|
+
if (!trimmed) {
|
|
1079
|
+
this.setStatus("Branch name cannot be empty.", "error");
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (!this.repoRoot || !this.sourceWorktree) {
|
|
1084
|
+
this.setStatus("No source worktree selected.", "error");
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const commitHash = this.sourceWorktree.head;
|
|
1089
|
+
if (!commitHash) {
|
|
1090
|
+
this.setStatus("Could not determine source commit.", "error");
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
this.setStatus(`Creating branch '${trimmed}'...`, "info");
|
|
1095
|
+
this.renderer.requestRender();
|
|
1096
|
+
|
|
1097
|
+
const result = createBranchFromCommit(this.repoRoot, trimmed, commitHash);
|
|
1098
|
+
|
|
1099
|
+
if (result.success) {
|
|
1100
|
+
this.pendingBranchName = trimmed;
|
|
1101
|
+
// Hide the input and show checkout confirmation
|
|
1102
|
+
if (this.branchNameInput) {
|
|
1103
|
+
this.branchNameInput.blur();
|
|
1104
|
+
}
|
|
1105
|
+
if (this.branchCreateContainer) {
|
|
1106
|
+
this.renderer.root.remove(this.branchCreateContainer.id);
|
|
1107
|
+
this.branchCreateContainer = null;
|
|
1108
|
+
this.branchNameInput = null;
|
|
1109
|
+
}
|
|
1110
|
+
this.isCreatingBranch = false;
|
|
1111
|
+
|
|
1112
|
+
this.showCheckoutConfirm(trimmed);
|
|
1113
|
+
} else {
|
|
1114
|
+
this.setStatus(`Failed to create branch: ${result.error}`, "error");
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
private showCheckoutConfirm(branchName: string): void {
|
|
1119
|
+
if (!this.sourceWorktree) {
|
|
1120
|
+
this.setStatus("No source worktree.", "error");
|
|
1121
|
+
this.hideBranchCreateInput();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
this.isAskingCheckout = true;
|
|
1126
|
+
|
|
1127
|
+
const sourceName = this.sourceWorktree.branch || basename(this.sourceWorktree.path);
|
|
1128
|
+
|
|
1129
|
+
this.branchCreateContainer = new BoxRenderable(this.renderer, {
|
|
1130
|
+
id: "checkout-confirm-container",
|
|
1131
|
+
position: "absolute",
|
|
1132
|
+
left: 2,
|
|
1133
|
+
top: 3,
|
|
1134
|
+
width: 76,
|
|
1135
|
+
height: 8,
|
|
1136
|
+
borderStyle: "single",
|
|
1137
|
+
borderColor: "#10B981",
|
|
1138
|
+
title: `Branch '${branchName}' Created`,
|
|
1139
|
+
titleAlignment: "center",
|
|
1140
|
+
backgroundColor: "#0F172A",
|
|
1141
|
+
border: true,
|
|
1142
|
+
});
|
|
1143
|
+
this.renderer.root.add(this.branchCreateContainer);
|
|
1144
|
+
|
|
1145
|
+
const infoText = new TextRenderable(this.renderer, {
|
|
1146
|
+
id: "checkout-info",
|
|
1147
|
+
position: "absolute",
|
|
1148
|
+
left: 1,
|
|
1149
|
+
top: 1,
|
|
1150
|
+
content: `Checkout '${branchName}' in worktree '${sourceName}'?`,
|
|
1151
|
+
fg: "#E2E8F0",
|
|
1152
|
+
});
|
|
1153
|
+
this.branchCreateContainer.add(infoText);
|
|
1154
|
+
|
|
1155
|
+
this.checkoutSelect = new SelectRenderable(this.renderer, {
|
|
1156
|
+
id: "checkout-select",
|
|
1157
|
+
position: "absolute",
|
|
1158
|
+
left: 1,
|
|
1159
|
+
top: 3,
|
|
1160
|
+
width: 72,
|
|
1161
|
+
height: 3,
|
|
1162
|
+
options: [
|
|
1163
|
+
{
|
|
1164
|
+
name: "Yes, checkout the new branch",
|
|
1165
|
+
description: "Switch this worktree to the new branch",
|
|
1166
|
+
value: "checkout",
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
name: "No, keep current branch",
|
|
1170
|
+
description: "Branch created but worktree stays on current branch",
|
|
1171
|
+
value: "keep",
|
|
1172
|
+
},
|
|
1173
|
+
],
|
|
1174
|
+
backgroundColor: "#0F172A",
|
|
1175
|
+
focusedBackgroundColor: "#1E293B",
|
|
1176
|
+
selectedBackgroundColor: "#1E3A5F",
|
|
1177
|
+
textColor: "#E2E8F0",
|
|
1178
|
+
selectedTextColor: "#38BDF8",
|
|
1179
|
+
descriptionColor: "#94A3B8",
|
|
1180
|
+
selectedDescriptionColor: "#E2E8F0",
|
|
1181
|
+
showDescription: true,
|
|
1182
|
+
wrapSelection: true,
|
|
1183
|
+
});
|
|
1184
|
+
this.branchCreateContainer.add(this.checkoutSelect);
|
|
1185
|
+
|
|
1186
|
+
this.checkoutSelect.on(
|
|
1187
|
+
SelectRenderableEvents.ITEM_SELECTED,
|
|
1188
|
+
(_index: number, option: SelectOption) => {
|
|
1189
|
+
this.handleCheckoutChoice(option.value as string);
|
|
1190
|
+
}
|
|
1191
|
+
);
|
|
1192
|
+
|
|
1193
|
+
this.instructions.content = "↑/↓ select • Enter confirm • Esc cancel";
|
|
1194
|
+
this.setStatus(`Branch '${branchName}' created successfully!`, "success");
|
|
1195
|
+
|
|
1196
|
+
this.checkoutSelect.focus();
|
|
1197
|
+
this.renderer.requestRender();
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
private hideCheckoutConfirm(): void {
|
|
1201
|
+
this.isAskingCheckout = false;
|
|
1202
|
+
this.pendingBranchName = null;
|
|
1203
|
+
this.sourceWorktree = null;
|
|
1204
|
+
|
|
1205
|
+
if (this.checkoutSelect) {
|
|
1206
|
+
this.checkoutSelect.blur();
|
|
1207
|
+
this.checkoutSelect = null;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (this.branchCreateContainer) {
|
|
1211
|
+
this.renderer.root.remove(this.branchCreateContainer.id);
|
|
1212
|
+
this.branchCreateContainer = null;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
this.selectElement.visible = true;
|
|
1216
|
+
this.instructions.content =
|
|
1217
|
+
"↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
|
|
1218
|
+
this.selectElement.focus();
|
|
1219
|
+
this.loadWorktrees();
|
|
1220
|
+
this.renderer.requestRender();
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
private handleCheckoutChoice(choice: string): void {
|
|
1224
|
+
if (choice === "checkout" && this.pendingBranchName && this.sourceWorktree) {
|
|
1225
|
+
this.setStatus(`Checking out '${this.pendingBranchName}'...`, "info");
|
|
1226
|
+
this.renderer.requestRender();
|
|
1227
|
+
|
|
1228
|
+
const result = checkoutBranch(this.sourceWorktree.path, this.pendingBranchName);
|
|
1229
|
+
|
|
1230
|
+
if (result.success) {
|
|
1231
|
+
this.setStatus(
|
|
1232
|
+
`Switched to branch '${this.pendingBranchName}'.`,
|
|
1233
|
+
"success"
|
|
1234
|
+
);
|
|
1235
|
+
} else {
|
|
1236
|
+
this.setStatus(`Failed to checkout: ${result.error}`, "error");
|
|
1237
|
+
}
|
|
1238
|
+
} else {
|
|
1239
|
+
this.setStatus(
|
|
1240
|
+
`Branch '${this.pendingBranchName}' created (not checked out).`,
|
|
1241
|
+
"success"
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
this.hideCheckoutConfirm();
|
|
1246
|
+
}
|
|
1247
|
+
|
|
938
1248
|
private loadWorktrees(selectWorktreePath?: string): void {
|
|
939
1249
|
this.repoRoot = resolveRepoRoot(this.targetPath);
|
|
940
1250
|
if (!this.repoRoot) {
|
|
@@ -1242,7 +1552,7 @@ class WorktreeSelector {
|
|
|
1242
1552
|
|
|
1243
1553
|
this.selectElement.visible = true;
|
|
1244
1554
|
this.instructions.content =
|
|
1245
|
-
"↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
|
|
1555
|
+
"↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
|
|
1246
1556
|
this.selectElement.focus();
|
|
1247
1557
|
this.loadWorktrees();
|
|
1248
1558
|
}
|
|
@@ -1371,7 +1681,7 @@ class WorktreeSelector {
|
|
|
1371
1681
|
|
|
1372
1682
|
this.loadWorktrees();
|
|
1373
1683
|
this.instructions.content =
|
|
1374
|
-
"↑/↓ navigate • Enter open • o folder • d delete • n new • c config • q quit";
|
|
1684
|
+
"↑/↓ navigate • Enter open • o folder • d delete • n new • b branch • c config • q quit";
|
|
1375
1685
|
this.renderer.requestRender();
|
|
1376
1686
|
}
|
|
1377
1687
|
|