@akiojin/gwt 2.0.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.ja.md +323 -0
- package/README.md +347 -0
- package/bin/gwt.js +5 -0
- package/package.json +125 -0
- package/src/claude-history.ts +717 -0
- package/src/claude.ts +292 -0
- package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
- package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
- package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
- package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
- package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
- package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
- package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
- package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
- package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
- package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
- package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
- package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
- package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
- package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
- package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
- package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
- package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
- package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
- package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
- package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
- package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
- package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
- package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
- package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
- package/src/cli/ui/components/App.tsx +793 -0
- package/src/cli/ui/components/common/Confirm.tsx +40 -0
- package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
- package/src/cli/ui/components/common/Input.tsx +36 -0
- package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
- package/src/cli/ui/components/common/Select.tsx +216 -0
- package/src/cli/ui/components/parts/Footer.tsx +41 -0
- package/src/cli/ui/components/parts/Header.test.tsx +85 -0
- package/src/cli/ui/components/parts/Header.tsx +63 -0
- package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
- package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
- package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
- package/src/cli/ui/components/parts/Stats.tsx +67 -0
- package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
- package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
- package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
- package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
- package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
- package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
- package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
- package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
- package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
- package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
- package/src/cli/ui/hooks/useGitData.ts +157 -0
- package/src/cli/ui/hooks/useScreenState.ts +44 -0
- package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
- package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
- package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
- package/src/cli/ui/types.ts +295 -0
- package/src/cli/ui/utils/baseBranch.ts +34 -0
- package/src/cli/ui/utils/branchFormatter.ts +222 -0
- package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
- package/src/codex.ts +139 -0
- package/src/config/builtin-tools.ts +44 -0
- package/src/config/constants.ts +100 -0
- package/src/config/env-history.ts +45 -0
- package/src/config/index.ts +204 -0
- package/src/config/tools.ts +293 -0
- package/src/git.ts +1102 -0
- package/src/github.ts +158 -0
- package/src/index.test.ts +87 -0
- package/src/index.ts +684 -0
- package/src/index.ts.backup +1543 -0
- package/src/launcher.ts +142 -0
- package/src/repositories/git.repository.ts +129 -0
- package/src/repositories/github.repository.ts +83 -0
- package/src/repositories/worktree.repository.ts +69 -0
- package/src/services/BatchMergeService.ts +251 -0
- package/src/services/WorktreeOrchestrator.ts +115 -0
- package/src/services/__tests__/BatchMergeService.test.ts +518 -0
- package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
- package/src/services/dependency-installer.ts +199 -0
- package/src/services/git.service.ts +113 -0
- package/src/services/github.service.ts +61 -0
- package/src/services/worktree.service.ts +66 -0
- package/src/types/api.ts +241 -0
- package/src/types/tools.ts +235 -0
- package/src/utils/spinner.ts +54 -0
- package/src/utils/terminal.ts +272 -0
- package/src/utils.test.ts +43 -0
- package/src/utils.ts +60 -0
- package/src/web/client/index.html +12 -0
- package/src/web/client/src/components/BranchGraph.tsx +231 -0
- package/src/web/client/src/components/EnvEditor.tsx +145 -0
- package/src/web/client/src/components/Terminal.tsx +137 -0
- package/src/web/client/src/hooks/useBranches.ts +41 -0
- package/src/web/client/src/hooks/useConfig.ts +31 -0
- package/src/web/client/src/hooks/useSessions.ts +59 -0
- package/src/web/client/src/hooks/useWorktrees.ts +47 -0
- package/src/web/client/src/index.css +834 -0
- package/src/web/client/src/lib/api.ts +184 -0
- package/src/web/client/src/lib/websocket.ts +174 -0
- package/src/web/client/src/main.tsx +29 -0
- package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
- package/src/web/client/src/pages/BranchListPage.tsx +264 -0
- package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
- package/src/web/client/src/router.tsx +27 -0
- package/src/web/client/vite.config.ts +21 -0
- package/src/web/server/env/importer.ts +54 -0
- package/src/web/server/index.ts +74 -0
- package/src/web/server/pty/manager.ts +189 -0
- package/src/web/server/routes/branches.ts +126 -0
- package/src/web/server/routes/config.ts +220 -0
- package/src/web/server/routes/index.ts +37 -0
- package/src/web/server/routes/sessions.ts +130 -0
- package/src/web/server/routes/worktrees.ts +108 -0
- package/src/web/server/services/branches.ts +368 -0
- package/src/web/server/services/worktrees.ts +85 -0
- package/src/web/server/websocket/handler.ts +180 -0
- package/src/worktree.ts +703 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useApp } from 'ink';
|
|
3
|
+
import { ErrorBoundary } from './common/ErrorBoundary.js';
|
|
4
|
+
import { BranchListScreen } from './screens/BranchListScreen.js';
|
|
5
|
+
import { WorktreeManagerScreen } from './screens/WorktreeManagerScreen.js';
|
|
6
|
+
import { BranchCreatorScreen } from './screens/BranchCreatorScreen.js';
|
|
7
|
+
import { BranchActionSelectorScreen } from '../screens/BranchActionSelectorScreen.js';
|
|
8
|
+
import { AIToolSelectorScreen } from './screens/AIToolSelectorScreen.js';
|
|
9
|
+
import { SessionSelectorScreen } from './screens/SessionSelectorScreen.js';
|
|
10
|
+
import { ExecutionModeSelectorScreen } from './screens/ExecutionModeSelectorScreen.js';
|
|
11
|
+
import type { AITool } from './screens/AIToolSelectorScreen.js';
|
|
12
|
+
import type { ExecutionMode } from './screens/ExecutionModeSelectorScreen.js';
|
|
13
|
+
import type { WorktreeItem } from './screens/WorktreeManagerScreen.js';
|
|
14
|
+
import { useGitData } from '../hooks/useGitData.js';
|
|
15
|
+
import { useScreenState } from '../hooks/useScreenState.js';
|
|
16
|
+
import { formatBranchItems } from '../utils/branchFormatter.js';
|
|
17
|
+
import { calculateStatistics } from '../utils/statisticsCalculator.js';
|
|
18
|
+
import type { BranchInfo, BranchItem, SelectedBranchState } from '../types.js';
|
|
19
|
+
import { getRepositoryRoot, deleteBranch } from '../../../git.js';
|
|
20
|
+
import {
|
|
21
|
+
createWorktree,
|
|
22
|
+
generateWorktreePath,
|
|
23
|
+
getMergedPRWorktrees,
|
|
24
|
+
isProtectedBranchName,
|
|
25
|
+
removeWorktree,
|
|
26
|
+
switchToProtectedBranch,
|
|
27
|
+
} from '../../../worktree.js';
|
|
28
|
+
import { getPackageVersion } from '../../../utils.js';
|
|
29
|
+
import {
|
|
30
|
+
resolveBaseBranchLabel,
|
|
31
|
+
resolveBaseBranchRef,
|
|
32
|
+
} from '../utils/baseBranch.js';
|
|
33
|
+
|
|
34
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧'];
|
|
35
|
+
const COMPLETION_HOLD_DURATION_MS = 3000;
|
|
36
|
+
const PROTECTED_BRANCH_WARNING =
|
|
37
|
+
'Root branches operate directly in the repository root. Create a new branch if you need a dedicated worktree.';
|
|
38
|
+
|
|
39
|
+
const getSpinnerFrame = (index: number): string => {
|
|
40
|
+
const frame = SPINNER_FRAMES[index];
|
|
41
|
+
if (typeof frame === 'string') {
|
|
42
|
+
return frame;
|
|
43
|
+
}
|
|
44
|
+
return SPINNER_FRAMES[0] ?? '⠋';
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface SelectionResult {
|
|
48
|
+
branch: string; // Local branch name (without remote prefix)
|
|
49
|
+
displayName: string; // Name that was selected in the UI (may include remote prefix)
|
|
50
|
+
branchType: 'local' | 'remote';
|
|
51
|
+
remoteBranch?: string; // Full remote ref when branchType === 'remote'
|
|
52
|
+
tool: AITool;
|
|
53
|
+
mode: ExecutionMode;
|
|
54
|
+
skipPermissions: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AppProps {
|
|
58
|
+
onExit: (result?: SelectionResult) => void;
|
|
59
|
+
loadingIndicatorDelay?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* App - Top-level component for Ink.js UI
|
|
64
|
+
* Integrates ErrorBoundary, data fetching, screen navigation, and all screens
|
|
65
|
+
*/
|
|
66
|
+
export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
67
|
+
const { exit } = useApp();
|
|
68
|
+
|
|
69
|
+
// 起動ディレクトリの取得
|
|
70
|
+
const workingDirectory = process.cwd();
|
|
71
|
+
|
|
72
|
+
const { branches, worktrees, loading, error, refresh, lastUpdated } = useGitData({
|
|
73
|
+
enableAutoRefresh: false, // Manual refresh with 'r' key
|
|
74
|
+
});
|
|
75
|
+
const { currentScreen, navigateTo, goBack } = useScreenState();
|
|
76
|
+
|
|
77
|
+
// Version state
|
|
78
|
+
const [version, setVersion] = useState<string | null>(null);
|
|
79
|
+
|
|
80
|
+
// Selection state (for branch → tool → mode flow)
|
|
81
|
+
const [selectedBranch, setSelectedBranch] = useState<SelectedBranchState | null>(null);
|
|
82
|
+
const [creationSourceBranch, setCreationSourceBranch] = useState<SelectedBranchState | null>(null);
|
|
83
|
+
const [selectedTool, setSelectedTool] = useState<AITool | null>(null);
|
|
84
|
+
|
|
85
|
+
// PR cleanup feedback
|
|
86
|
+
const [cleanupIndicators, setCleanupIndicators] = useState<Record<string, { icon: string; color?: 'cyan' | 'green' | 'yellow' | 'red' }>>({});
|
|
87
|
+
const [cleanupProcessingBranch, setCleanupProcessingBranch] = useState<string | null>(null);
|
|
88
|
+
const [cleanupInputLocked, setCleanupInputLocked] = useState(false);
|
|
89
|
+
const [cleanupFooterMessage, setCleanupFooterMessage] = useState<{ text: string; color?: 'cyan' | 'green' | 'yellow' | 'red' } | null>(null);
|
|
90
|
+
const [hiddenBranches, setHiddenBranches] = useState<string[]>([]);
|
|
91
|
+
const spinnerFrameIndexRef = useRef(0);
|
|
92
|
+
const [spinnerFrameIndex, setSpinnerFrameIndex] = useState(0);
|
|
93
|
+
const completionTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
94
|
+
|
|
95
|
+
// Fetch version on mount
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
getPackageVersion()
|
|
98
|
+
.then(setVersion)
|
|
99
|
+
.catch(() => setVersion(null));
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!cleanupInputLocked) {
|
|
104
|
+
spinnerFrameIndexRef.current = 0;
|
|
105
|
+
setSpinnerFrameIndex(0);
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const interval = setInterval(() => {
|
|
110
|
+
spinnerFrameIndexRef.current = (spinnerFrameIndexRef.current + 1) % SPINNER_FRAMES.length;
|
|
111
|
+
setSpinnerFrameIndex(spinnerFrameIndexRef.current);
|
|
112
|
+
}, 120);
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
clearInterval(interval);
|
|
116
|
+
spinnerFrameIndexRef.current = 0;
|
|
117
|
+
setSpinnerFrameIndex(0);
|
|
118
|
+
};
|
|
119
|
+
}, [cleanupInputLocked]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!cleanupInputLocked) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const frame = getSpinnerFrame(spinnerFrameIndex);
|
|
127
|
+
|
|
128
|
+
if (cleanupProcessingBranch) {
|
|
129
|
+
setCleanupIndicators((prev) => {
|
|
130
|
+
const current = prev[cleanupProcessingBranch];
|
|
131
|
+
if (current && current.icon === frame && current.color === 'cyan') {
|
|
132
|
+
return prev;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const next: Record<string, { icon: string; color?: 'cyan' | 'green' | 'yellow' | 'red' }> = {
|
|
136
|
+
...prev,
|
|
137
|
+
[cleanupProcessingBranch]: { icon: frame, color: 'cyan' },
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return next;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
setCleanupFooterMessage({ text: `Processing... ${frame}`, color: 'cyan' });
|
|
145
|
+
}, [cleanupInputLocked, cleanupProcessingBranch, spinnerFrameIndex]);
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!hiddenBranches.length) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const existing = new Set(branches.map((branch) => branch.name));
|
|
153
|
+
const filtered = hiddenBranches.filter((name) => existing.has(name));
|
|
154
|
+
|
|
155
|
+
if (filtered.length !== hiddenBranches.length) {
|
|
156
|
+
setHiddenBranches(filtered);
|
|
157
|
+
}
|
|
158
|
+
}, [branches, hiddenBranches]);
|
|
159
|
+
|
|
160
|
+
useEffect(() => () => {
|
|
161
|
+
if (completionTimerRef.current) {
|
|
162
|
+
clearTimeout(completionTimerRef.current);
|
|
163
|
+
completionTimerRef.current = null;
|
|
164
|
+
}
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
const visibleBranches = useMemo(
|
|
168
|
+
() => branches.filter((branch) => !hiddenBranches.includes(branch.name)),
|
|
169
|
+
[branches, hiddenBranches]
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Helper function to create content-based hash for branches
|
|
173
|
+
const branchHash = useMemo(
|
|
174
|
+
() => visibleBranches.map((b) => `${b.name}-${b.type}-${b.isCurrent}`).join(','),
|
|
175
|
+
[visibleBranches]
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Helper function to create content-based hash for worktrees
|
|
179
|
+
const worktreeHash = useMemo(
|
|
180
|
+
() => worktrees.map((w) => `${w.branch}-${w.path}`).join(','),
|
|
181
|
+
[worktrees]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Format branches to BranchItems (memoized for performance with content-based dependencies)
|
|
185
|
+
const branchItems: BranchItem[] = useMemo(() => {
|
|
186
|
+
// Build worktreeMap for sorting
|
|
187
|
+
const worktreeMap = new Map();
|
|
188
|
+
for (const wt of worktrees) {
|
|
189
|
+
worktreeMap.set(wt.branch, {
|
|
190
|
+
path: wt.path,
|
|
191
|
+
locked: false,
|
|
192
|
+
prunable: wt.isAccessible === false,
|
|
193
|
+
isAccessible: wt.isAccessible ?? true,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return formatBranchItems(visibleBranches, worktreeMap);
|
|
197
|
+
}, [branchHash, worktreeHash, visibleBranches, worktrees]);
|
|
198
|
+
|
|
199
|
+
// Calculate statistics (memoized for performance)
|
|
200
|
+
const stats = useMemo(() => calculateStatistics(visibleBranches), [visibleBranches]);
|
|
201
|
+
|
|
202
|
+
// Format worktrees to WorktreeItems
|
|
203
|
+
const worktreeItems: WorktreeItem[] = useMemo(
|
|
204
|
+
() =>
|
|
205
|
+
worktrees.map((wt): WorktreeItem => ({
|
|
206
|
+
branch: wt.branch,
|
|
207
|
+
path: wt.path,
|
|
208
|
+
isAccessible: wt.isAccessible ?? true,
|
|
209
|
+
})),
|
|
210
|
+
[worktrees]
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const resolveBaseBranch = useCallback(() => {
|
|
214
|
+
const localMain = branches.find(
|
|
215
|
+
(branch) =>
|
|
216
|
+
branch.type === 'local' && (branch.name === 'main' || branch.name === 'master')
|
|
217
|
+
);
|
|
218
|
+
if (localMain) {
|
|
219
|
+
return localMain.name;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const develop = branches.find(
|
|
223
|
+
(branch) => branch.type === 'local' && (branch.name === 'develop' || branch.name === 'dev')
|
|
224
|
+
);
|
|
225
|
+
if (develop) {
|
|
226
|
+
return develop.name;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return 'main';
|
|
230
|
+
}, [branches]);
|
|
231
|
+
|
|
232
|
+
const baseBranchLabel = useMemo(
|
|
233
|
+
() => resolveBaseBranchLabel(creationSourceBranch, selectedBranch, resolveBaseBranch),
|
|
234
|
+
[creationSourceBranch, resolveBaseBranch, selectedBranch]
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Handle branch selection
|
|
238
|
+
const toLocalBranchName = useCallback((remoteName: string) => {
|
|
239
|
+
const segments = remoteName.split('/');
|
|
240
|
+
if (segments.length <= 1) {
|
|
241
|
+
return remoteName;
|
|
242
|
+
}
|
|
243
|
+
return segments.slice(1).join('/');
|
|
244
|
+
}, []);
|
|
245
|
+
|
|
246
|
+
const inferBranchCategory = useCallback(
|
|
247
|
+
(branchName: string): BranchInfo['branchType'] => {
|
|
248
|
+
const matched = branches.find((branch) => branch.name === branchName);
|
|
249
|
+
if (matched) {
|
|
250
|
+
return matched.branchType;
|
|
251
|
+
}
|
|
252
|
+
if (branchName === 'main' || branchName === 'master') {
|
|
253
|
+
return 'main';
|
|
254
|
+
}
|
|
255
|
+
if (branchName === 'develop' || branchName === 'dev') {
|
|
256
|
+
return 'develop';
|
|
257
|
+
}
|
|
258
|
+
if (branchName.startsWith('feature/')) {
|
|
259
|
+
return 'feature';
|
|
260
|
+
}
|
|
261
|
+
if (branchName.startsWith('hotfix/')) {
|
|
262
|
+
return 'hotfix';
|
|
263
|
+
}
|
|
264
|
+
if (branchName.startsWith('release/')) {
|
|
265
|
+
return 'release';
|
|
266
|
+
}
|
|
267
|
+
return 'other';
|
|
268
|
+
},
|
|
269
|
+
[branches]
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const isProtectedSelection = useCallback(
|
|
273
|
+
(branch: SelectedBranchState | null): boolean => {
|
|
274
|
+
if (!branch) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
return (
|
|
278
|
+
isProtectedBranchName(branch.name) ||
|
|
279
|
+
isProtectedBranchName(branch.displayName) ||
|
|
280
|
+
(branch.remoteBranch ? isProtectedBranchName(branch.remoteBranch) : false) ||
|
|
281
|
+
branch.branchCategory === 'main' ||
|
|
282
|
+
branch.branchCategory === 'develop'
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
[isProtectedBranchName]
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const protectedBranchInfo = useMemo(() => {
|
|
289
|
+
if (!selectedBranch) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
if (!isProtectedSelection(selectedBranch)) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const label = selectedBranch.displayName ?? selectedBranch.name;
|
|
296
|
+
return {
|
|
297
|
+
label,
|
|
298
|
+
message: `${label} is a root branch. Switch within the repository root instead of creating a worktree.`,
|
|
299
|
+
};
|
|
300
|
+
}, [selectedBranch, isProtectedSelection]);
|
|
301
|
+
|
|
302
|
+
const handleSelect = useCallback(
|
|
303
|
+
(item: BranchItem) => {
|
|
304
|
+
const selection: SelectedBranchState =
|
|
305
|
+
item.type === 'remote'
|
|
306
|
+
? {
|
|
307
|
+
name: toLocalBranchName(item.name),
|
|
308
|
+
displayName: item.name,
|
|
309
|
+
branchType: 'remote',
|
|
310
|
+
branchCategory: item.branchType,
|
|
311
|
+
remoteBranch: item.name,
|
|
312
|
+
}
|
|
313
|
+
: {
|
|
314
|
+
name: item.name,
|
|
315
|
+
displayName: item.name,
|
|
316
|
+
branchType: 'local',
|
|
317
|
+
branchCategory: item.branchType,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const protectedSelected = isProtectedSelection(selection);
|
|
321
|
+
|
|
322
|
+
setSelectedBranch(selection);
|
|
323
|
+
setSelectedTool(null);
|
|
324
|
+
setCreationSourceBranch(null);
|
|
325
|
+
|
|
326
|
+
if (protectedSelected) {
|
|
327
|
+
setCleanupFooterMessage({
|
|
328
|
+
text: PROTECTED_BRANCH_WARNING,
|
|
329
|
+
color: 'yellow',
|
|
330
|
+
});
|
|
331
|
+
} else {
|
|
332
|
+
setCleanupFooterMessage(null);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
navigateTo('branch-action-selector');
|
|
336
|
+
},
|
|
337
|
+
[isProtectedSelection, navigateTo, setCleanupFooterMessage, setCreationSourceBranch, setSelectedTool, toLocalBranchName]
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Handle navigation
|
|
341
|
+
const handleNavigate = useCallback(
|
|
342
|
+
(screen: string) => {
|
|
343
|
+
navigateTo(screen as any);
|
|
344
|
+
},
|
|
345
|
+
[navigateTo]
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const handleWorktreeSelect = useCallback(
|
|
349
|
+
(worktree: WorktreeItem) => {
|
|
350
|
+
setSelectedBranch({
|
|
351
|
+
name: worktree.branch,
|
|
352
|
+
displayName: worktree.branch,
|
|
353
|
+
branchType: 'local',
|
|
354
|
+
branchCategory: inferBranchCategory(worktree.branch),
|
|
355
|
+
});
|
|
356
|
+
setSelectedTool(null);
|
|
357
|
+
setCreationSourceBranch(null);
|
|
358
|
+
setCleanupFooterMessage(null);
|
|
359
|
+
navigateTo('ai-tool-selector');
|
|
360
|
+
},
|
|
361
|
+
[inferBranchCategory, navigateTo, setCleanupFooterMessage, setCreationSourceBranch]
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Handle branch action selection
|
|
365
|
+
const handleProtectedBranchSwitch = useCallback(async () => {
|
|
366
|
+
if (!selectedBranch) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
setCleanupFooterMessage({
|
|
372
|
+
text: `Preparing root branch '${selectedBranch.displayName ?? selectedBranch.name}'...`,
|
|
373
|
+
color: 'cyan',
|
|
374
|
+
});
|
|
375
|
+
const repoRoot = await getRepositoryRoot();
|
|
376
|
+
const remoteRef =
|
|
377
|
+
selectedBranch.remoteBranch ??
|
|
378
|
+
(selectedBranch.branchType === 'remote'
|
|
379
|
+
? selectedBranch.displayName ?? selectedBranch.name
|
|
380
|
+
: null);
|
|
381
|
+
|
|
382
|
+
const result = await switchToProtectedBranch({
|
|
383
|
+
branchName: selectedBranch.name,
|
|
384
|
+
repoRoot,
|
|
385
|
+
remoteRef: remoteRef ?? null,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
let successMessage = `'${selectedBranch.displayName ?? selectedBranch.name}' will use the repository root.`;
|
|
389
|
+
if (result === 'remote') {
|
|
390
|
+
successMessage = `Created a local tracking branch for '${selectedBranch.displayName ?? selectedBranch.name}' and switched to the protected branch.`;
|
|
391
|
+
} else if (result === 'local') {
|
|
392
|
+
successMessage = `Checked out '${selectedBranch.displayName ?? selectedBranch.name}' in the repository root.`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
setCleanupFooterMessage({
|
|
396
|
+
text: successMessage,
|
|
397
|
+
color: 'green',
|
|
398
|
+
});
|
|
399
|
+
refresh();
|
|
400
|
+
navigateTo('ai-tool-selector');
|
|
401
|
+
} catch (error) {
|
|
402
|
+
const message =
|
|
403
|
+
error instanceof Error ? error.message : String(error);
|
|
404
|
+
setCleanupFooterMessage({
|
|
405
|
+
text: `Failed to switch root branch: ${message}`,
|
|
406
|
+
color: 'red',
|
|
407
|
+
});
|
|
408
|
+
console.error('Failed to switch protected branch:', error);
|
|
409
|
+
}
|
|
410
|
+
}, [
|
|
411
|
+
navigateTo,
|
|
412
|
+
refresh,
|
|
413
|
+
selectedBranch,
|
|
414
|
+
setCleanupFooterMessage,
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
const handleUseExistingBranch = useCallback(() => {
|
|
418
|
+
if (selectedBranch && isProtectedSelection(selectedBranch)) {
|
|
419
|
+
void handleProtectedBranchSwitch();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
navigateTo('ai-tool-selector');
|
|
423
|
+
}, [handleProtectedBranchSwitch, isProtectedSelection, navigateTo, selectedBranch]);
|
|
424
|
+
|
|
425
|
+
const handleCreateNewBranch = useCallback(() => {
|
|
426
|
+
setCreationSourceBranch(selectedBranch);
|
|
427
|
+
navigateTo('branch-creator');
|
|
428
|
+
}, [navigateTo, selectedBranch]);
|
|
429
|
+
|
|
430
|
+
// Handle quit
|
|
431
|
+
const handleQuit = useCallback(() => {
|
|
432
|
+
onExit();
|
|
433
|
+
exit();
|
|
434
|
+
}, [onExit, exit]);
|
|
435
|
+
|
|
436
|
+
// Handle branch creation
|
|
437
|
+
const handleCreate = useCallback(
|
|
438
|
+
async (branchName: string) => {
|
|
439
|
+
try {
|
|
440
|
+
const repoRoot = await getRepositoryRoot();
|
|
441
|
+
const worktreePath = await generateWorktreePath(repoRoot, branchName);
|
|
442
|
+
// Use selectedBranch as base if available, otherwise resolve from repo
|
|
443
|
+
const baseBranch = resolveBaseBranchRef(
|
|
444
|
+
creationSourceBranch,
|
|
445
|
+
selectedBranch,
|
|
446
|
+
resolveBaseBranch,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
await createWorktree({
|
|
450
|
+
branchName,
|
|
451
|
+
worktreePath,
|
|
452
|
+
repoRoot,
|
|
453
|
+
isNewBranch: true,
|
|
454
|
+
baseBranch,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
refresh();
|
|
458
|
+
setCreationSourceBranch(null);
|
|
459
|
+
setSelectedBranch({
|
|
460
|
+
name: branchName,
|
|
461
|
+
displayName: branchName,
|
|
462
|
+
branchType: 'local',
|
|
463
|
+
branchCategory: inferBranchCategory(branchName),
|
|
464
|
+
});
|
|
465
|
+
setSelectedTool(null);
|
|
466
|
+
setCleanupFooterMessage(null);
|
|
467
|
+
|
|
468
|
+
navigateTo('ai-tool-selector');
|
|
469
|
+
} catch (error) {
|
|
470
|
+
// On error, go back to branch list
|
|
471
|
+
console.error('Failed to create branch:', error);
|
|
472
|
+
goBack();
|
|
473
|
+
refresh();
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
[
|
|
477
|
+
navigateTo,
|
|
478
|
+
goBack,
|
|
479
|
+
refresh,
|
|
480
|
+
resolveBaseBranch,
|
|
481
|
+
selectedBranch,
|
|
482
|
+
creationSourceBranch,
|
|
483
|
+
inferBranchCategory,
|
|
484
|
+
setCleanupFooterMessage,
|
|
485
|
+
]
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
const handleCleanupCommand = useCallback(async () => {
|
|
489
|
+
if (cleanupInputLocked) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (completionTimerRef.current) {
|
|
494
|
+
clearTimeout(completionTimerRef.current);
|
|
495
|
+
completionTimerRef.current = null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const succeededBranches: string[] = [];
|
|
499
|
+
|
|
500
|
+
const resetAfterWait = () => {
|
|
501
|
+
setCleanupIndicators({});
|
|
502
|
+
setCleanupInputLocked(false);
|
|
503
|
+
setCleanupFooterMessage(null);
|
|
504
|
+
if (succeededBranches.length > 0) {
|
|
505
|
+
setHiddenBranches((prev) => {
|
|
506
|
+
const merged = new Set(prev);
|
|
507
|
+
succeededBranches.forEach((branch) => merged.add(branch));
|
|
508
|
+
return Array.from(merged);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
refresh();
|
|
512
|
+
completionTimerRef.current = null;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// Provide immediate feedback before fetching targets
|
|
516
|
+
setCleanupInputLocked(true);
|
|
517
|
+
setCleanupIndicators({});
|
|
518
|
+
const initialFrame = getSpinnerFrame(0);
|
|
519
|
+
setCleanupFooterMessage({ text: `Processing... ${initialFrame}`, color: 'cyan' });
|
|
520
|
+
setCleanupProcessingBranch(null);
|
|
521
|
+
spinnerFrameIndexRef.current = 0;
|
|
522
|
+
setSpinnerFrameIndex(0);
|
|
523
|
+
|
|
524
|
+
let targets;
|
|
525
|
+
try {
|
|
526
|
+
targets = await getMergedPRWorktrees();
|
|
527
|
+
} catch (error) {
|
|
528
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
529
|
+
setCleanupIndicators({});
|
|
530
|
+
setCleanupFooterMessage({ text: `❌ ${message}`, color: 'red' });
|
|
531
|
+
setCleanupInputLocked(false);
|
|
532
|
+
completionTimerRef.current = setTimeout(() => {
|
|
533
|
+
setCleanupFooterMessage(null);
|
|
534
|
+
completionTimerRef.current = null;
|
|
535
|
+
}, COMPLETION_HOLD_DURATION_MS);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (targets.length === 0) {
|
|
540
|
+
setCleanupIndicators({});
|
|
541
|
+
setCleanupFooterMessage({ text: '✅ Nothing to clean up.', color: 'green' });
|
|
542
|
+
setCleanupInputLocked(false);
|
|
543
|
+
completionTimerRef.current = setTimeout(() => {
|
|
544
|
+
setCleanupFooterMessage(null);
|
|
545
|
+
completionTimerRef.current = null;
|
|
546
|
+
}, COMPLETION_HOLD_DURATION_MS);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Reset hidden branches that may already be gone
|
|
551
|
+
setHiddenBranches((prev) => prev.filter((name) => targets.find((t) => t.branch === name) === undefined));
|
|
552
|
+
|
|
553
|
+
const initialIndicators = targets.reduce<Record<string, { icon: string; color?: 'cyan' | 'green' | 'yellow' | 'red' }>>((acc, target, index) => {
|
|
554
|
+
const icon = index === 0 ? getSpinnerFrame(0) : '⏳';
|
|
555
|
+
const color: 'cyan' | 'green' | 'yellow' | 'red' = index === 0 ? 'cyan' : 'yellow';
|
|
556
|
+
acc[target.branch] = { icon, color };
|
|
557
|
+
return acc;
|
|
558
|
+
}, {});
|
|
559
|
+
|
|
560
|
+
setCleanupIndicators(initialIndicators);
|
|
561
|
+
const firstTarget = targets.length > 0 ? targets[0] : undefined;
|
|
562
|
+
setCleanupProcessingBranch(firstTarget ? firstTarget.branch : null);
|
|
563
|
+
spinnerFrameIndexRef.current = 0;
|
|
564
|
+
setSpinnerFrameIndex(0);
|
|
565
|
+
setCleanupFooterMessage({ text: `Processing... ${getSpinnerFrame(0)}`, color: 'cyan' });
|
|
566
|
+
|
|
567
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
568
|
+
const currentTarget = targets[index];
|
|
569
|
+
if (!currentTarget) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const target = currentTarget;
|
|
573
|
+
|
|
574
|
+
setCleanupProcessingBranch(target.branch);
|
|
575
|
+
spinnerFrameIndexRef.current = 0;
|
|
576
|
+
setSpinnerFrameIndex(0);
|
|
577
|
+
|
|
578
|
+
setCleanupIndicators((prev) => {
|
|
579
|
+
const updated = { ...prev };
|
|
580
|
+
updated[target.branch] = { icon: getSpinnerFrame(0), color: 'cyan' };
|
|
581
|
+
for (const pending of targets.slice(index + 1)) {
|
|
582
|
+
const current = updated[pending.branch];
|
|
583
|
+
if (!current || current.icon !== '⏳') {
|
|
584
|
+
updated[pending.branch] = { icon: '⏳', color: 'yellow' };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return updated;
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const shouldSkip =
|
|
591
|
+
target.hasUncommittedChanges ||
|
|
592
|
+
target.hasUnpushedCommits ||
|
|
593
|
+
(target.cleanupType === 'worktree-and-branch' && (!target.worktreePath || target.isAccessible === false));
|
|
594
|
+
|
|
595
|
+
if (shouldSkip) {
|
|
596
|
+
setCleanupIndicators((prev) => ({
|
|
597
|
+
...prev,
|
|
598
|
+
[target.branch]: { icon: '⏭️', color: 'yellow' },
|
|
599
|
+
}));
|
|
600
|
+
setCleanupProcessingBranch(null);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
if (target.cleanupType === 'worktree-and-branch' && target.worktreePath) {
|
|
606
|
+
await removeWorktree(target.worktreePath, true);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
await deleteBranch(target.branch, true);
|
|
610
|
+
succeededBranches.push(target.branch);
|
|
611
|
+
setCleanupIndicators((prev) => ({
|
|
612
|
+
...prev,
|
|
613
|
+
[target.branch]: { icon: '✅', color: 'green' },
|
|
614
|
+
}));
|
|
615
|
+
} catch {
|
|
616
|
+
const icon = '❌';
|
|
617
|
+
setCleanupIndicators((prev) => ({
|
|
618
|
+
...prev,
|
|
619
|
+
[target.branch]: { icon, color: 'red' },
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
setCleanupProcessingBranch(null);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
setCleanupProcessingBranch(null);
|
|
627
|
+
setCleanupInputLocked(false);
|
|
628
|
+
setCleanupFooterMessage({ text: 'Cleanup completed. Finalizing...', color: 'green' });
|
|
629
|
+
|
|
630
|
+
const holdDuration =
|
|
631
|
+
typeof process !== 'undefined' && process.env?.NODE_ENV === 'test'
|
|
632
|
+
? 0
|
|
633
|
+
: COMPLETION_HOLD_DURATION_MS;
|
|
634
|
+
|
|
635
|
+
completionTimerRef.current = setTimeout(resetAfterWait, holdDuration);
|
|
636
|
+
}, [cleanupInputLocked, deleteBranch, getMergedPRWorktrees, refresh, removeWorktree]);
|
|
637
|
+
|
|
638
|
+
// Handle AI tool selection
|
|
639
|
+
const handleToolSelect = useCallback(
|
|
640
|
+
(tool: AITool) => {
|
|
641
|
+
setSelectedTool(tool);
|
|
642
|
+
navigateTo('execution-mode-selector');
|
|
643
|
+
},
|
|
644
|
+
[navigateTo]
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
// Handle session selection
|
|
648
|
+
const handleSessionSelect = useCallback(
|
|
649
|
+
(_session: string) => {
|
|
650
|
+
// TODO: Load selected session and navigate to next screen
|
|
651
|
+
// For now, just go back to branch list
|
|
652
|
+
goBack();
|
|
653
|
+
},
|
|
654
|
+
[goBack]
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
// Handle execution mode and skipPermissions selection
|
|
658
|
+
const handleModeSelect = useCallback(
|
|
659
|
+
(result: { mode: ExecutionMode; skipPermissions: boolean }) => {
|
|
660
|
+
// All selections complete - exit with result
|
|
661
|
+
if (selectedBranch && selectedTool) {
|
|
662
|
+
const payload: SelectionResult = {
|
|
663
|
+
branch: selectedBranch.name,
|
|
664
|
+
displayName: selectedBranch.displayName,
|
|
665
|
+
branchType: selectedBranch.branchType,
|
|
666
|
+
tool: selectedTool,
|
|
667
|
+
mode: result.mode,
|
|
668
|
+
skipPermissions: result.skipPermissions,
|
|
669
|
+
...(selectedBranch.remoteBranch
|
|
670
|
+
? { remoteBranch: selectedBranch.remoteBranch }
|
|
671
|
+
: {}),
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
onExit(payload);
|
|
675
|
+
exit();
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
[selectedBranch, selectedTool, onExit, exit]
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
// Render screen based on currentScreen
|
|
682
|
+
const renderScreen = () => {
|
|
683
|
+
switch (currentScreen) {
|
|
684
|
+
case 'branch-list':
|
|
685
|
+
return (
|
|
686
|
+
<BranchListScreen
|
|
687
|
+
branches={branchItems}
|
|
688
|
+
stats={stats}
|
|
689
|
+
onSelect={handleSelect}
|
|
690
|
+
onNavigate={handleNavigate}
|
|
691
|
+
onQuit={handleQuit}
|
|
692
|
+
onCleanupCommand={handleCleanupCommand}
|
|
693
|
+
onRefresh={refresh}
|
|
694
|
+
loading={loading}
|
|
695
|
+
error={error}
|
|
696
|
+
lastUpdated={lastUpdated}
|
|
697
|
+
loadingIndicatorDelay={loadingIndicatorDelay}
|
|
698
|
+
cleanupUI={{
|
|
699
|
+
indicators: cleanupIndicators,
|
|
700
|
+
footerMessage: cleanupFooterMessage,
|
|
701
|
+
inputLocked: cleanupInputLocked,
|
|
702
|
+
}}
|
|
703
|
+
version={version}
|
|
704
|
+
workingDirectory={workingDirectory}
|
|
705
|
+
/>
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
case 'worktree-manager':
|
|
709
|
+
return (
|
|
710
|
+
<WorktreeManagerScreen
|
|
711
|
+
worktrees={worktreeItems}
|
|
712
|
+
onBack={goBack}
|
|
713
|
+
onSelect={handleWorktreeSelect}
|
|
714
|
+
version={version}
|
|
715
|
+
/>
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
case 'branch-creator':
|
|
719
|
+
return (
|
|
720
|
+
<BranchCreatorScreen
|
|
721
|
+
onBack={goBack}
|
|
722
|
+
onCreate={handleCreate}
|
|
723
|
+
baseBranch={baseBranchLabel}
|
|
724
|
+
version={version}
|
|
725
|
+
/>
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
case 'branch-action-selector': {
|
|
729
|
+
const isProtected = Boolean(protectedBranchInfo);
|
|
730
|
+
const baseProps = {
|
|
731
|
+
selectedBranch: selectedBranch?.displayName ?? '',
|
|
732
|
+
onUseExisting: handleUseExistingBranch,
|
|
733
|
+
onCreateNew: handleCreateNewBranch,
|
|
734
|
+
onBack: goBack,
|
|
735
|
+
canCreateNew: Boolean(selectedBranch),
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
if (isProtected) {
|
|
739
|
+
return (
|
|
740
|
+
<BranchActionSelectorScreen
|
|
741
|
+
{...baseProps}
|
|
742
|
+
mode="protected"
|
|
743
|
+
infoMessage={protectedBranchInfo?.message ?? null}
|
|
744
|
+
primaryLabel="Use root branch (no worktree)"
|
|
745
|
+
secondaryLabel="Create new branch from this branch"
|
|
746
|
+
/>
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return <BranchActionSelectorScreen {...baseProps} />;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
case 'ai-tool-selector':
|
|
754
|
+
return <AIToolSelectorScreen onBack={goBack} onSelect={handleToolSelect} version={version} />;
|
|
755
|
+
|
|
756
|
+
case 'session-selector':
|
|
757
|
+
// TODO: Implement session data fetching
|
|
758
|
+
return (
|
|
759
|
+
<SessionSelectorScreen
|
|
760
|
+
sessions={[]}
|
|
761
|
+
onBack={goBack}
|
|
762
|
+
onSelect={handleSessionSelect}
|
|
763
|
+
version={version}
|
|
764
|
+
/>
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
case 'execution-mode-selector':
|
|
768
|
+
return (
|
|
769
|
+
<ExecutionModeSelectorScreen onBack={goBack} onSelect={handleModeSelect} version={version} />
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
default:
|
|
773
|
+
return (
|
|
774
|
+
<BranchListScreen
|
|
775
|
+
branches={branchItems}
|
|
776
|
+
stats={stats}
|
|
777
|
+
onSelect={handleSelect}
|
|
778
|
+
onNavigate={handleNavigate}
|
|
779
|
+
onQuit={handleQuit}
|
|
780
|
+
onRefresh={refresh}
|
|
781
|
+
loading={loading}
|
|
782
|
+
error={error}
|
|
783
|
+
lastUpdated={lastUpdated}
|
|
784
|
+
loadingIndicatorDelay={loadingIndicatorDelay}
|
|
785
|
+
version={version}
|
|
786
|
+
workingDirectory={workingDirectory}
|
|
787
|
+
/>
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
return <ErrorBoundary>{renderScreen()}</ErrorBoundary>;
|
|
793
|
+
}
|