@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,213 @@
|
|
|
1
|
+
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { Header } from '../parts/Header.js';
|
|
4
|
+
import { Footer } from '../parts/Footer.js';
|
|
5
|
+
import { Select } from '../common/Select.js';
|
|
6
|
+
import { Input } from '../common/Input.js';
|
|
7
|
+
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
|
8
|
+
import { BRANCH_PREFIXES } from '../../../../config/constants.js';
|
|
9
|
+
|
|
10
|
+
type BranchType = 'feature' | 'hotfix' | 'release';
|
|
11
|
+
type Step = 'type-selection' | 'name-input';
|
|
12
|
+
|
|
13
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧'];
|
|
14
|
+
|
|
15
|
+
export interface BranchCreatorScreenProps {
|
|
16
|
+
onBack: () => void;
|
|
17
|
+
onCreate: (branchName: string) => Promise<void>;
|
|
18
|
+
baseBranch?: string;
|
|
19
|
+
version?: string | null;
|
|
20
|
+
disableAnimation?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface BranchTypeItem {
|
|
24
|
+
label: string;
|
|
25
|
+
value: BranchType;
|
|
26
|
+
description: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* BranchCreatorScreen - Screen for creating new branches
|
|
31
|
+
* Layout: Header + Type Selection or Name Input + Footer
|
|
32
|
+
* Flow: Type Selection → Name Input → onCreate
|
|
33
|
+
*/
|
|
34
|
+
export function BranchCreatorScreen({
|
|
35
|
+
onBack,
|
|
36
|
+
onCreate,
|
|
37
|
+
baseBranch,
|
|
38
|
+
version,
|
|
39
|
+
disableAnimation = false,
|
|
40
|
+
}: BranchCreatorScreenProps) {
|
|
41
|
+
const { rows } = useTerminalSize();
|
|
42
|
+
const [step, setStep] = useState<Step>('type-selection');
|
|
43
|
+
const [selectedType, setSelectedType] = useState<BranchType>('feature');
|
|
44
|
+
const [branchName, setBranchName] = useState('');
|
|
45
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
46
|
+
const [pendingBranchName, setPendingBranchName] = useState<string | null>(null);
|
|
47
|
+
const spinnerIndexRef = useRef(0);
|
|
48
|
+
const [spinnerIndex, setSpinnerIndex] = useState(0);
|
|
49
|
+
|
|
50
|
+
const spinnerFrame = SPINNER_FRAMES[spinnerIndex] ?? SPINNER_FRAMES[0];
|
|
51
|
+
|
|
52
|
+
// Handle keyboard input for back navigation
|
|
53
|
+
useInput((input, key) => {
|
|
54
|
+
if (isCreating) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (key.escape) {
|
|
59
|
+
onBack();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Branch type options
|
|
64
|
+
const branchTypeItems: BranchTypeItem[] = [
|
|
65
|
+
{
|
|
66
|
+
label: 'feature',
|
|
67
|
+
value: 'feature',
|
|
68
|
+
description: 'New feature development',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: 'hotfix',
|
|
72
|
+
value: 'hotfix',
|
|
73
|
+
description: 'Critical bug fix',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: 'release',
|
|
77
|
+
value: 'release',
|
|
78
|
+
description: 'Release preparation',
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// Handle branch type selection
|
|
83
|
+
const handleTypeSelect = useCallback((item: BranchTypeItem) => {
|
|
84
|
+
setSelectedType(item.value);
|
|
85
|
+
setStep('name-input');
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
// Handle branch name input
|
|
89
|
+
const handleNameChange = useCallback((value: string) => {
|
|
90
|
+
if (isCreating) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
setBranchName(value);
|
|
94
|
+
}, [isCreating]);
|
|
95
|
+
|
|
96
|
+
// Handle branch creation
|
|
97
|
+
const handleCreate = useCallback(async () => {
|
|
98
|
+
if (isCreating) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const trimmedName = branchName.trim();
|
|
103
|
+
if (!trimmedName) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const prefix = BRANCH_PREFIXES[selectedType.toUpperCase() as keyof typeof BRANCH_PREFIXES];
|
|
108
|
+
const fullBranchName = `${prefix}${trimmedName}`;
|
|
109
|
+
|
|
110
|
+
setIsCreating(true);
|
|
111
|
+
setPendingBranchName(fullBranchName);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await onCreate(fullBranchName);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
setPendingBranchName(null);
|
|
117
|
+
setIsCreating(false);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}, [branchName, selectedType, onCreate, isCreating]);
|
|
121
|
+
|
|
122
|
+
// Footer actions
|
|
123
|
+
const footerActions =
|
|
124
|
+
isCreating
|
|
125
|
+
? []
|
|
126
|
+
: step === 'type-selection'
|
|
127
|
+
? [
|
|
128
|
+
{ key: 'enter', description: 'Select' },
|
|
129
|
+
{ key: 'esc', description: 'Back' },
|
|
130
|
+
]
|
|
131
|
+
: [
|
|
132
|
+
{ key: 'enter', description: 'Create' },
|
|
133
|
+
{ key: 'esc', description: 'Back' },
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!isCreating || disableAnimation) {
|
|
138
|
+
spinnerIndexRef.current = 0;
|
|
139
|
+
setSpinnerIndex(0);
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const interval = setInterval(() => {
|
|
144
|
+
spinnerIndexRef.current = (spinnerIndexRef.current + 1) % SPINNER_FRAMES.length;
|
|
145
|
+
setSpinnerIndex(spinnerIndexRef.current);
|
|
146
|
+
}, 120);
|
|
147
|
+
|
|
148
|
+
return () => {
|
|
149
|
+
clearInterval(interval);
|
|
150
|
+
spinnerIndexRef.current = 0;
|
|
151
|
+
setSpinnerIndex(0);
|
|
152
|
+
};
|
|
153
|
+
}, [isCreating, disableAnimation]);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<Box flexDirection="column" height={rows}>
|
|
157
|
+
{/* Header */}
|
|
158
|
+
<Header title="New Branch" titleColor="green" version={version} />
|
|
159
|
+
|
|
160
|
+
{/* Content */}
|
|
161
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
162
|
+
{baseBranch && (
|
|
163
|
+
<Box marginBottom={1}>
|
|
164
|
+
<Text>
|
|
165
|
+
Base branch: <Text bold color="cyan">{baseBranch}</Text>
|
|
166
|
+
</Text>
|
|
167
|
+
</Box>
|
|
168
|
+
)}
|
|
169
|
+
{isCreating ? (
|
|
170
|
+
<Box flexDirection="column">
|
|
171
|
+
<Box marginBottom={1}>
|
|
172
|
+
<Text>
|
|
173
|
+
{spinnerFrame}{' '}
|
|
174
|
+
<Text color="cyan">
|
|
175
|
+
Creating branch{' '}
|
|
176
|
+
<Text bold>
|
|
177
|
+
{pendingBranchName ??
|
|
178
|
+
`${BRANCH_PREFIXES[selectedType.toUpperCase() as keyof typeof BRANCH_PREFIXES]}${branchName.trim()}`}
|
|
179
|
+
</Text>
|
|
180
|
+
</Text>
|
|
181
|
+
</Text>
|
|
182
|
+
</Box>
|
|
183
|
+
<Text color="gray">Please wait while the branch is being created...</Text>
|
|
184
|
+
</Box>
|
|
185
|
+
) : step === 'type-selection' ? (
|
|
186
|
+
<Box flexDirection="column">
|
|
187
|
+
<Box marginBottom={1}>
|
|
188
|
+
<Text>Select branch type:</Text>
|
|
189
|
+
</Box>
|
|
190
|
+
<Select items={branchTypeItems} onSelect={handleTypeSelect} />
|
|
191
|
+
</Box>
|
|
192
|
+
) : (
|
|
193
|
+
<Box flexDirection="column">
|
|
194
|
+
<Box marginBottom={1}>
|
|
195
|
+
<Text>
|
|
196
|
+
Branch name prefix: <Text bold>{BRANCH_PREFIXES[selectedType.toUpperCase() as keyof typeof BRANCH_PREFIXES]}</Text>
|
|
197
|
+
</Text>
|
|
198
|
+
</Box>
|
|
199
|
+
<Input
|
|
200
|
+
value={branchName}
|
|
201
|
+
onChange={handleNameChange}
|
|
202
|
+
onSubmit={handleCreate}
|
|
203
|
+
placeholder="Enter branch name (e.g., add-new-feature)"
|
|
204
|
+
/>
|
|
205
|
+
</Box>
|
|
206
|
+
)}
|
|
207
|
+
</Box>
|
|
208
|
+
|
|
209
|
+
{/* Footer */}
|
|
210
|
+
<Footer actions={footerActions} />
|
|
211
|
+
</Box>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { Header } from '../parts/Header.js';
|
|
4
|
+
import { Stats } from '../parts/Stats.js';
|
|
5
|
+
import { Footer } from '../parts/Footer.js';
|
|
6
|
+
import { Select } from '../common/Select.js';
|
|
7
|
+
import { LoadingIndicator } from '../common/LoadingIndicator.js';
|
|
8
|
+
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
|
9
|
+
import type { BranchItem, Statistics } from '../../types.js';
|
|
10
|
+
import stringWidth from 'string-width';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
|
|
13
|
+
const WIDTH_OVERRIDES: Record<string, number> = {
|
|
14
|
+
'⬆': 1,
|
|
15
|
+
'☁': 1,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const measureDisplayWidth = (value: string): number => {
|
|
19
|
+
let width = 0;
|
|
20
|
+
for (const char of Array.from(value)) {
|
|
21
|
+
const override = WIDTH_OVERRIDES[char];
|
|
22
|
+
if (override !== undefined) {
|
|
23
|
+
width += override;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
width += stringWidth(char);
|
|
27
|
+
}
|
|
28
|
+
return width;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type IndicatorColor = 'cyan' | 'green' | 'yellow' | 'red';
|
|
32
|
+
|
|
33
|
+
interface CleanupIndicator {
|
|
34
|
+
icon: string;
|
|
35
|
+
color?: IndicatorColor;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface CleanupFooterMessage {
|
|
39
|
+
text: string;
|
|
40
|
+
color?: IndicatorColor;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface CleanupUIState {
|
|
44
|
+
indicators: Record<string, CleanupIndicator>;
|
|
45
|
+
footerMessage: CleanupFooterMessage | null;
|
|
46
|
+
inputLocked: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BranchListScreenProps {
|
|
50
|
+
branches: BranchItem[];
|
|
51
|
+
stats: Statistics;
|
|
52
|
+
onSelect: (branch: BranchItem) => void;
|
|
53
|
+
onNavigate?: (screen: string) => void;
|
|
54
|
+
onQuit?: () => void;
|
|
55
|
+
onCleanupCommand?: () => void;
|
|
56
|
+
onRefresh?: () => void;
|
|
57
|
+
loading?: boolean;
|
|
58
|
+
error?: Error | null;
|
|
59
|
+
lastUpdated?: Date | null;
|
|
60
|
+
loadingIndicatorDelay?: number;
|
|
61
|
+
cleanupUI?: CleanupUIState;
|
|
62
|
+
version?: string | null;
|
|
63
|
+
workingDirectory?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* BranchListScreen - Main screen for branch selection
|
|
68
|
+
* Layout: Header + Stats + Branch List + Footer
|
|
69
|
+
*/
|
|
70
|
+
export function BranchListScreen({
|
|
71
|
+
branches,
|
|
72
|
+
stats,
|
|
73
|
+
onSelect,
|
|
74
|
+
onNavigate,
|
|
75
|
+
onCleanupCommand,
|
|
76
|
+
onRefresh,
|
|
77
|
+
loading = false,
|
|
78
|
+
error = null,
|
|
79
|
+
lastUpdated = null,
|
|
80
|
+
loadingIndicatorDelay = 300,
|
|
81
|
+
cleanupUI,
|
|
82
|
+
version,
|
|
83
|
+
workingDirectory,
|
|
84
|
+
}: BranchListScreenProps) {
|
|
85
|
+
const { rows } = useTerminalSize();
|
|
86
|
+
|
|
87
|
+
// Handle keyboard input
|
|
88
|
+
// Note: Select component handles Enter and arrow keys
|
|
89
|
+
useInput((input) => {
|
|
90
|
+
if (cleanupUI?.inputLocked) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (input === 'm' && onNavigate) {
|
|
95
|
+
onNavigate('worktree-manager');
|
|
96
|
+
} else if (input === 'c') {
|
|
97
|
+
onCleanupCommand?.();
|
|
98
|
+
} else if (input === 'r' && onRefresh) {
|
|
99
|
+
onRefresh();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Calculate available space for branch list
|
|
104
|
+
// Header: 2 lines (title + divider)
|
|
105
|
+
// Stats: 1 line
|
|
106
|
+
// Empty line: 1 line
|
|
107
|
+
// Footer: 1 line
|
|
108
|
+
// Total fixed: 5 lines
|
|
109
|
+
const headerLines = 2;
|
|
110
|
+
const statsLines = 1;
|
|
111
|
+
const emptyLine = 1;
|
|
112
|
+
const footerLines = 1;
|
|
113
|
+
const fixedLines = headerLines + statsLines + emptyLine + footerLines;
|
|
114
|
+
const contentHeight = rows - fixedLines;
|
|
115
|
+
const limit = Math.max(5, contentHeight); // Minimum 5 items visible
|
|
116
|
+
|
|
117
|
+
// Footer actions
|
|
118
|
+
const footerActions = [
|
|
119
|
+
{ key: 'enter', description: 'Select' },
|
|
120
|
+
{ key: 'r', description: 'Refresh' },
|
|
121
|
+
{ key: 'm', description: 'Manage worktrees' },
|
|
122
|
+
{ key: 'c', description: 'Cleanup branches' },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const formatLatestCommit = useCallback((timestamp?: number) => {
|
|
126
|
+
if (!timestamp || Number.isNaN(timestamp)) {
|
|
127
|
+
return '---';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const date = new Date(timestamp * 1000);
|
|
131
|
+
const year = date.getFullYear();
|
|
132
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
133
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
134
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
135
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
136
|
+
|
|
137
|
+
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
const truncateToWidth = useCallback((value: string, maxWidth: number) => {
|
|
141
|
+
if (maxWidth <= 0) {
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (stringWidth(value) <= maxWidth) {
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const ellipsis = '…';
|
|
150
|
+
const ellipsisWidth = stringWidth(ellipsis);
|
|
151
|
+
if (ellipsisWidth >= maxWidth) {
|
|
152
|
+
return ellipsis;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let currentWidth = 0;
|
|
156
|
+
let result = '';
|
|
157
|
+
|
|
158
|
+
for (const char of value) {
|
|
159
|
+
const charWidth = stringWidth(char);
|
|
160
|
+
if (currentWidth + charWidth + ellipsisWidth > maxWidth) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
result += char;
|
|
164
|
+
currentWidth += charWidth;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result + ellipsis;
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
const renderBranchRow = useCallback(
|
|
171
|
+
(item: BranchItem, isSelected: boolean, context: { columns: number }) => {
|
|
172
|
+
const columns = Math.max(20, context.columns);
|
|
173
|
+
const arrow = isSelected ? '>' : ' ';
|
|
174
|
+
const timestampText = formatLatestCommit(item.latestCommitTimestamp);
|
|
175
|
+
const timestampWidth = stringWidth(timestampText);
|
|
176
|
+
|
|
177
|
+
const indicatorInfo = cleanupUI?.indicators?.[item.name];
|
|
178
|
+
let indicatorIcon = indicatorInfo?.icon ?? '';
|
|
179
|
+
if (indicatorIcon && indicatorInfo?.color && !isSelected) {
|
|
180
|
+
switch (indicatorInfo.color) {
|
|
181
|
+
case 'cyan':
|
|
182
|
+
indicatorIcon = chalk.cyan(indicatorIcon);
|
|
183
|
+
break;
|
|
184
|
+
case 'green':
|
|
185
|
+
indicatorIcon = chalk.green(indicatorIcon);
|
|
186
|
+
break;
|
|
187
|
+
case 'yellow':
|
|
188
|
+
indicatorIcon = chalk.yellow(indicatorIcon);
|
|
189
|
+
break;
|
|
190
|
+
case 'red':
|
|
191
|
+
indicatorIcon = chalk.red(indicatorIcon);
|
|
192
|
+
break;
|
|
193
|
+
default:
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const indicatorPrefix = indicatorIcon ? `${indicatorIcon} ` : '';
|
|
198
|
+
const staticPrefix = `${arrow} ${indicatorPrefix}`;
|
|
199
|
+
const staticPrefixWidth = stringWidth(staticPrefix);
|
|
200
|
+
|
|
201
|
+
const availableLeftWidth = Math.max(staticPrefixWidth, columns - timestampWidth - 1);
|
|
202
|
+
const maxLabelWidth = Math.max(0, availableLeftWidth - staticPrefixWidth);
|
|
203
|
+
const truncatedLabel = truncateToWidth(item.label, maxLabelWidth);
|
|
204
|
+
const leftText = `${staticPrefix}${truncatedLabel}`;
|
|
205
|
+
|
|
206
|
+
const leftMeasuredWidth = stringWidth(leftText);
|
|
207
|
+
const leftDisplayWidth = measureDisplayWidth(leftText);
|
|
208
|
+
const baseGapWidth = Math.max(1, columns - leftMeasuredWidth - timestampWidth);
|
|
209
|
+
const displayGapWidth = Math.max(1, columns - leftDisplayWidth - timestampWidth);
|
|
210
|
+
const cursorShift = Math.max(0, displayGapWidth - baseGapWidth);
|
|
211
|
+
|
|
212
|
+
const gap = ' '.repeat(baseGapWidth);
|
|
213
|
+
const cursorAdjust = cursorShift > 0 ? `\u001b[${cursorShift}C` : '';
|
|
214
|
+
|
|
215
|
+
let line = `${leftText}${gap}${cursorAdjust}${timestampText}`;
|
|
216
|
+
const paddingWidth = Math.max(0, columns - stringWidth(line));
|
|
217
|
+
if (paddingWidth > 0) {
|
|
218
|
+
line += ' '.repeat(paddingWidth);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const output = isSelected
|
|
222
|
+
? `[46m[30m${line}[0m`
|
|
223
|
+
: line;
|
|
224
|
+
return <Text>{output}</Text>;
|
|
225
|
+
},
|
|
226
|
+
[cleanupUI, formatLatestCommit, truncateToWidth]
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<Box flexDirection="column" height={rows}>
|
|
231
|
+
{/* Header */}
|
|
232
|
+
<Header
|
|
233
|
+
title="gwt - Branch Selection"
|
|
234
|
+
titleColor="cyan"
|
|
235
|
+
version={version}
|
|
236
|
+
{...(workingDirectory !== undefined && { workingDirectory })}
|
|
237
|
+
/>
|
|
238
|
+
|
|
239
|
+
{/* Stats */}
|
|
240
|
+
<Box marginTop={1}>
|
|
241
|
+
<Stats stats={stats} lastUpdated={lastUpdated} />
|
|
242
|
+
</Box>
|
|
243
|
+
|
|
244
|
+
{/* Content */}
|
|
245
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
246
|
+
<LoadingIndicator
|
|
247
|
+
isLoading={Boolean(loading)}
|
|
248
|
+
delay={loadingIndicatorDelay}
|
|
249
|
+
message="Loading Git information..."
|
|
250
|
+
/>
|
|
251
|
+
|
|
252
|
+
{error && (
|
|
253
|
+
<Box flexDirection="column">
|
|
254
|
+
<Text color="red" bold>
|
|
255
|
+
Error: {error.message}
|
|
256
|
+
</Text>
|
|
257
|
+
{process.env.DEBUG && error.stack && (
|
|
258
|
+
<Box marginTop={1}>
|
|
259
|
+
<Text color="gray">{error.stack}</Text>
|
|
260
|
+
</Box>
|
|
261
|
+
)}
|
|
262
|
+
</Box>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{!loading && !error && branches.length === 0 && (
|
|
266
|
+
<Box>
|
|
267
|
+
<Text dimColor>No branches found</Text>
|
|
268
|
+
</Box>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
{!loading && !error && branches.length > 0 && (
|
|
272
|
+
<Select
|
|
273
|
+
items={branches}
|
|
274
|
+
onSelect={onSelect}
|
|
275
|
+
limit={limit}
|
|
276
|
+
disabled={Boolean(cleanupUI?.inputLocked)}
|
|
277
|
+
renderIndicator={() => null}
|
|
278
|
+
renderItem={renderBranchRow}
|
|
279
|
+
/>
|
|
280
|
+
)}
|
|
281
|
+
</Box>
|
|
282
|
+
|
|
283
|
+
{cleanupUI?.footerMessage && (
|
|
284
|
+
<Box marginBottom={1}>
|
|
285
|
+
{cleanupUI.footerMessage.color ? (
|
|
286
|
+
<Text color={cleanupUI.footerMessage.color}>
|
|
287
|
+
{cleanupUI.footerMessage.text}
|
|
288
|
+
</Text>
|
|
289
|
+
) : (
|
|
290
|
+
<Text>{cleanupUI.footerMessage.text}</Text>
|
|
291
|
+
)}
|
|
292
|
+
</Box>
|
|
293
|
+
)}
|
|
294
|
+
|
|
295
|
+
{/* Footer */}
|
|
296
|
+
<Footer actions={footerActions} />
|
|
297
|
+
</Box>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { Header } from '../parts/Header.js';
|
|
4
|
+
import { Footer } from '../parts/Footer.js';
|
|
5
|
+
import { Select } from '../common/Select.js';
|
|
6
|
+
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
|
7
|
+
|
|
8
|
+
export type ExecutionMode = 'normal' | 'continue' | 'resume';
|
|
9
|
+
|
|
10
|
+
export interface ExecutionModeItem {
|
|
11
|
+
label: string;
|
|
12
|
+
value: ExecutionMode;
|
|
13
|
+
description: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SkipPermissionsItem {
|
|
17
|
+
label: string;
|
|
18
|
+
value: string; // "yes" or "no"
|
|
19
|
+
description: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ExecutionModeResult {
|
|
23
|
+
mode: ExecutionMode;
|
|
24
|
+
skipPermissions: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ExecutionModeSelectorScreenProps {
|
|
28
|
+
onBack: () => void;
|
|
29
|
+
onSelect: (result: ExecutionModeResult) => void;
|
|
30
|
+
version?: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ExecutionModeSelectorScreen - Screen for selecting execution mode (2-step)
|
|
35
|
+
* Step 1: Select mode (Normal/Continue/Resume)
|
|
36
|
+
* Step 2: Select skip permissions (Yes/No)
|
|
37
|
+
* Layout: Header + Selection + Footer
|
|
38
|
+
*/
|
|
39
|
+
export function ExecutionModeSelectorScreen({
|
|
40
|
+
onBack,
|
|
41
|
+
onSelect,
|
|
42
|
+
version,
|
|
43
|
+
}: ExecutionModeSelectorScreenProps) {
|
|
44
|
+
const { rows } = useTerminalSize();
|
|
45
|
+
const [step, setStep] = useState<1 | 2>(1);
|
|
46
|
+
const [selectedMode, setSelectedMode] = useState<ExecutionMode | null>(null);
|
|
47
|
+
|
|
48
|
+
// Handle keyboard input
|
|
49
|
+
useInput((input, key) => {
|
|
50
|
+
if (key.escape) {
|
|
51
|
+
if (step === 2) {
|
|
52
|
+
// Go back to step 1
|
|
53
|
+
setStep(1);
|
|
54
|
+
setSelectedMode(null);
|
|
55
|
+
} else {
|
|
56
|
+
// Go back to previous screen
|
|
57
|
+
onBack();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Execution mode options (Step 1)
|
|
63
|
+
const modeItems: ExecutionModeItem[] = [
|
|
64
|
+
{
|
|
65
|
+
label: 'Normal',
|
|
66
|
+
value: 'normal',
|
|
67
|
+
description: 'Start fresh session',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: 'Continue',
|
|
71
|
+
value: 'continue',
|
|
72
|
+
description: 'Continue from last session',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: 'Resume',
|
|
76
|
+
value: 'resume',
|
|
77
|
+
description: 'Resume specific session',
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Skip permissions options (Step 2)
|
|
82
|
+
const skipPermissionsItems: SkipPermissionsItem[] = [
|
|
83
|
+
{
|
|
84
|
+
label: 'No',
|
|
85
|
+
value: 'no',
|
|
86
|
+
description: 'Normal permission checks',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: 'Yes',
|
|
90
|
+
value: 'yes',
|
|
91
|
+
description: 'Skip permission checks (--dangerously-skip-permissions / --yolo)',
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// Handle mode selection (Step 1)
|
|
96
|
+
const handleModeSelect = (item: ExecutionModeItem) => {
|
|
97
|
+
setSelectedMode(item.value);
|
|
98
|
+
setStep(2);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Handle skip permissions selection (Step 2)
|
|
102
|
+
const handleSkipPermissionsSelect = (item: SkipPermissionsItem) => {
|
|
103
|
+
if (selectedMode) {
|
|
104
|
+
onSelect({
|
|
105
|
+
mode: selectedMode,
|
|
106
|
+
skipPermissions: item.value === 'yes',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Footer actions
|
|
112
|
+
const footerActions = [
|
|
113
|
+
{ key: 'enter', description: 'Select' },
|
|
114
|
+
{ key: 'esc', description: step === 2 ? 'Back to mode selection' : 'Back' },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Box flexDirection="column" height={rows}>
|
|
119
|
+
{/* Header */}
|
|
120
|
+
<Header
|
|
121
|
+
title={step === 1 ? 'Execution Mode' : 'Skip Permissions'}
|
|
122
|
+
titleColor="magenta"
|
|
123
|
+
version={version}
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
{/* Content */}
|
|
127
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
128
|
+
{step === 1 ? (
|
|
129
|
+
<>
|
|
130
|
+
<Box marginBottom={1}>
|
|
131
|
+
<Text>Select execution mode:</Text>
|
|
132
|
+
</Box>
|
|
133
|
+
<Select items={modeItems} onSelect={handleModeSelect} />
|
|
134
|
+
</>
|
|
135
|
+
) : (
|
|
136
|
+
<>
|
|
137
|
+
<Box marginBottom={1}>
|
|
138
|
+
<Text>Skip permission checks? (--dangerously-skip-permissions / --yolo)</Text>
|
|
139
|
+
</Box>
|
|
140
|
+
<Select items={skipPermissionsItems} onSelect={handleSkipPermissionsSelect} />
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
</Box>
|
|
144
|
+
|
|
145
|
+
{/* Footer */}
|
|
146
|
+
<Footer actions={footerActions} />
|
|
147
|
+
</Box>
|
|
148
|
+
);
|
|
149
|
+
}
|