@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,167 @@
|
|
|
1
|
+
import React 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
|
+
import type { CleanupTarget } from '../../types.js';
|
|
8
|
+
|
|
9
|
+
export interface PRItem {
|
|
10
|
+
label: string;
|
|
11
|
+
value: string;
|
|
12
|
+
target: CleanupTarget;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PRCleanupScreenProps {
|
|
16
|
+
targets: CleanupTarget[];
|
|
17
|
+
loading: boolean;
|
|
18
|
+
error: Error | null;
|
|
19
|
+
statusMessage?: string | null;
|
|
20
|
+
onBack: () => void;
|
|
21
|
+
onRefresh: () => void;
|
|
22
|
+
onCleanup: (target: CleanupTarget) => void;
|
|
23
|
+
version?: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* PRCleanupScreen - Screen for cleaning up merged pull requests
|
|
28
|
+
* Layout: Header + Stats + PR List + Footer
|
|
29
|
+
*/
|
|
30
|
+
export function PRCleanupScreen({
|
|
31
|
+
targets,
|
|
32
|
+
loading,
|
|
33
|
+
error,
|
|
34
|
+
statusMessage,
|
|
35
|
+
onBack,
|
|
36
|
+
onRefresh,
|
|
37
|
+
onCleanup,
|
|
38
|
+
version,
|
|
39
|
+
}: PRCleanupScreenProps) {
|
|
40
|
+
const { rows } = useTerminalSize();
|
|
41
|
+
|
|
42
|
+
// Handle keyboard input
|
|
43
|
+
// Note: Select component handles Enter and arrow keys
|
|
44
|
+
useInput((input, key) => {
|
|
45
|
+
if (key.escape) {
|
|
46
|
+
onBack();
|
|
47
|
+
} else if (input === 'r') {
|
|
48
|
+
onRefresh();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Format pull requests for Select component
|
|
53
|
+
const prItems: PRItem[] = targets.map((target) => {
|
|
54
|
+
const pr = target.pullRequest;
|
|
55
|
+
const flags: string[] = [];
|
|
56
|
+
if (target.cleanupType === 'worktree-and-branch') {
|
|
57
|
+
flags.push('worktree');
|
|
58
|
+
} else {
|
|
59
|
+
flags.push('branch');
|
|
60
|
+
}
|
|
61
|
+
if (target.reasons?.includes('merged-pr')) {
|
|
62
|
+
flags.push('merged');
|
|
63
|
+
}
|
|
64
|
+
if (target.reasons?.includes('no-diff-with-base')) {
|
|
65
|
+
flags.push('base');
|
|
66
|
+
}
|
|
67
|
+
if (target.hasUncommittedChanges) {
|
|
68
|
+
flags.push('changes');
|
|
69
|
+
}
|
|
70
|
+
if (target.hasUnpushedCommits) {
|
|
71
|
+
flags.push('unpushed');
|
|
72
|
+
}
|
|
73
|
+
if (target.isAccessible === false) {
|
|
74
|
+
flags.push('inaccessible');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const flagText = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
|
|
78
|
+
|
|
79
|
+
const label = pr
|
|
80
|
+
? `${target.branch} - #${pr.number} ${pr.title}${flagText}`
|
|
81
|
+
: `${target.branch}${flagText}`;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
label,
|
|
85
|
+
value: target.branch,
|
|
86
|
+
target,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Calculate available space for PR list
|
|
91
|
+
const headerLines = 2;
|
|
92
|
+
const statsLines = 1;
|
|
93
|
+
const emptyLine = 1;
|
|
94
|
+
const footerLines = 1;
|
|
95
|
+
const fixedLines = headerLines + statsLines + emptyLine + footerLines;
|
|
96
|
+
const contentHeight = rows - fixedLines;
|
|
97
|
+
const limit = Math.max(5, contentHeight);
|
|
98
|
+
|
|
99
|
+
// Footer actions
|
|
100
|
+
const footerActions = [
|
|
101
|
+
{ key: 'enter', description: 'Cleanup' },
|
|
102
|
+
{ key: 'r', description: 'Refresh' },
|
|
103
|
+
{ key: 'esc', description: 'Back' },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Box flexDirection="column" height={rows}>
|
|
108
|
+
{/* Header */}
|
|
109
|
+
<Header title="Branch Cleanup" titleColor="yellow" version={version} />
|
|
110
|
+
|
|
111
|
+
{/* Stats */}
|
|
112
|
+
<Box marginTop={1}>
|
|
113
|
+
<Box flexDirection="row">
|
|
114
|
+
<Box marginRight={2}>
|
|
115
|
+
<Text>
|
|
116
|
+
Total: <Text bold>{targets.length}</Text>
|
|
117
|
+
</Text>
|
|
118
|
+
</Box>
|
|
119
|
+
{loading && (
|
|
120
|
+
<Box marginRight={2}>
|
|
121
|
+
<Text color="cyan">Loading...</Text>
|
|
122
|
+
</Box>
|
|
123
|
+
)}
|
|
124
|
+
</Box>
|
|
125
|
+
</Box>
|
|
126
|
+
|
|
127
|
+
{error && (
|
|
128
|
+
<Box marginTop={1}>
|
|
129
|
+
<Text color="red">
|
|
130
|
+
Error: <Text bold>{error.message}</Text>
|
|
131
|
+
</Text>
|
|
132
|
+
</Box>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{statusMessage && (
|
|
136
|
+
<Box marginTop={1}>
|
|
137
|
+
<Text color="green">{statusMessage}</Text>
|
|
138
|
+
</Box>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{/* Empty line */}
|
|
142
|
+
<Box height={1} />
|
|
143
|
+
|
|
144
|
+
{/* Content */}
|
|
145
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
146
|
+
{loading ? (
|
|
147
|
+
<Box>
|
|
148
|
+
<Text dimColor>Loading cleanup targets...</Text>
|
|
149
|
+
</Box>
|
|
150
|
+
) : targets.length === 0 ? (
|
|
151
|
+
<Box>
|
|
152
|
+
<Text dimColor>No cleanup targets found</Text>
|
|
153
|
+
</Box>
|
|
154
|
+
) : (
|
|
155
|
+
<Select<PRItem>
|
|
156
|
+
items={prItems}
|
|
157
|
+
onSelect={(item) => onCleanup(item.target)}
|
|
158
|
+
limit={limit}
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
</Box>
|
|
162
|
+
|
|
163
|
+
{/* Footer */}
|
|
164
|
+
<Footer actions={footerActions} />
|
|
165
|
+
</Box>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React 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 interface SessionItem {
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SessionSelectorScreenProps {
|
|
14
|
+
sessions: string[];
|
|
15
|
+
onBack: () => void;
|
|
16
|
+
onSelect: (session: string) => void;
|
|
17
|
+
version?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* SessionSelectorScreen - Screen for selecting a session
|
|
22
|
+
* Layout: Header + Stats + Session List + Footer
|
|
23
|
+
*/
|
|
24
|
+
export function SessionSelectorScreen({
|
|
25
|
+
sessions,
|
|
26
|
+
onBack,
|
|
27
|
+
onSelect,
|
|
28
|
+
version,
|
|
29
|
+
}: SessionSelectorScreenProps) {
|
|
30
|
+
const { rows } = useTerminalSize();
|
|
31
|
+
|
|
32
|
+
// Handle keyboard input
|
|
33
|
+
// Note: Select component handles Enter and arrow keys
|
|
34
|
+
useInput((input, key) => {
|
|
35
|
+
if (key.escape) {
|
|
36
|
+
onBack();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Format sessions for Select component
|
|
41
|
+
const sessionItems: SessionItem[] = sessions.map((session) => ({
|
|
42
|
+
label: session,
|
|
43
|
+
value: session,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Handle session selection
|
|
47
|
+
const handleSelect = (item: SessionItem) => {
|
|
48
|
+
onSelect(item.value);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Calculate available space for session list
|
|
52
|
+
const headerLines = 2;
|
|
53
|
+
const statsLines = 1;
|
|
54
|
+
const emptyLine = 1;
|
|
55
|
+
const footerLines = 1;
|
|
56
|
+
const fixedLines = headerLines + statsLines + emptyLine + footerLines;
|
|
57
|
+
const contentHeight = rows - fixedLines;
|
|
58
|
+
const limit = Math.max(5, contentHeight);
|
|
59
|
+
|
|
60
|
+
// Footer actions
|
|
61
|
+
const footerActions = [
|
|
62
|
+
{ key: 'enter', description: 'Select' },
|
|
63
|
+
{ key: 'esc', description: 'Back' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Box flexDirection="column" height={rows}>
|
|
68
|
+
{/* Header */}
|
|
69
|
+
<Header title="Session Selection" titleColor="cyan" version={version} />
|
|
70
|
+
|
|
71
|
+
{/* Stats */}
|
|
72
|
+
<Box marginTop={1}>
|
|
73
|
+
<Box flexDirection="row">
|
|
74
|
+
<Box marginRight={2}>
|
|
75
|
+
<Text>
|
|
76
|
+
Total: <Text bold>{sessions.length}</Text>
|
|
77
|
+
</Text>
|
|
78
|
+
</Box>
|
|
79
|
+
</Box>
|
|
80
|
+
</Box>
|
|
81
|
+
|
|
82
|
+
{/* Empty line */}
|
|
83
|
+
<Box height={1} />
|
|
84
|
+
|
|
85
|
+
{/* Content */}
|
|
86
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
87
|
+
{sessions.length === 0 ? (
|
|
88
|
+
<Box>
|
|
89
|
+
<Text dimColor>No sessions found</Text>
|
|
90
|
+
</Box>
|
|
91
|
+
) : (
|
|
92
|
+
<Select items={sessionItems} onSelect={handleSelect} limit={limit} />
|
|
93
|
+
)}
|
|
94
|
+
</Box>
|
|
95
|
+
|
|
96
|
+
{/* Footer */}
|
|
97
|
+
<Footer actions={footerActions} />
|
|
98
|
+
</Box>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React 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 interface WorktreeItem {
|
|
9
|
+
branch: string;
|
|
10
|
+
path: string;
|
|
11
|
+
isAccessible: boolean;
|
|
12
|
+
label?: string;
|
|
13
|
+
value?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WorktreeManagerScreenProps {
|
|
17
|
+
worktrees: WorktreeItem[];
|
|
18
|
+
onBack: () => void;
|
|
19
|
+
onSelect: (worktree: WorktreeItem) => void;
|
|
20
|
+
version?: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* WorktreeManagerScreen - Screen for managing worktrees
|
|
25
|
+
* Layout: Header + Stats + Worktree List + Footer
|
|
26
|
+
*/
|
|
27
|
+
export function WorktreeManagerScreen({
|
|
28
|
+
worktrees,
|
|
29
|
+
onBack,
|
|
30
|
+
onSelect,
|
|
31
|
+
version,
|
|
32
|
+
}: WorktreeManagerScreenProps) {
|
|
33
|
+
const { rows } = useTerminalSize();
|
|
34
|
+
|
|
35
|
+
// Handle keyboard input
|
|
36
|
+
// Note: Select component handles Enter and arrow keys
|
|
37
|
+
useInput((input, key) => {
|
|
38
|
+
if (key.escape) {
|
|
39
|
+
onBack();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Calculate accessible and inaccessible counts
|
|
44
|
+
const accessibleCount = worktrees.filter((w) => w.isAccessible).length;
|
|
45
|
+
const inaccessibleCount = worktrees.filter((w) => !w.isAccessible).length;
|
|
46
|
+
|
|
47
|
+
// Format worktrees for Select component
|
|
48
|
+
const worktreeItems = worktrees.map((wt) => ({
|
|
49
|
+
...wt,
|
|
50
|
+
label: wt.isAccessible
|
|
51
|
+
? `${wt.branch} (${wt.path})`
|
|
52
|
+
: `${wt.branch} (${wt.path}) [Inaccessible]`,
|
|
53
|
+
value: wt.branch,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
// Calculate available space for worktree list
|
|
57
|
+
const headerLines = 2;
|
|
58
|
+
const statsLines = 1;
|
|
59
|
+
const emptyLine = 1;
|
|
60
|
+
const footerLines = 1;
|
|
61
|
+
const fixedLines = headerLines + statsLines + emptyLine + footerLines;
|
|
62
|
+
const contentHeight = rows - fixedLines;
|
|
63
|
+
const limit = Math.max(5, contentHeight);
|
|
64
|
+
|
|
65
|
+
// Footer actions
|
|
66
|
+
const footerActions = [
|
|
67
|
+
{ key: 'enter', description: 'Select' },
|
|
68
|
+
{ key: 'esc', description: 'Back' },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Box flexDirection="column" height={rows}>
|
|
73
|
+
{/* Header */}
|
|
74
|
+
<Header title="Worktree Manager" titleColor="magenta" version={version} />
|
|
75
|
+
|
|
76
|
+
{/* Stats */}
|
|
77
|
+
<Box marginTop={1}>
|
|
78
|
+
<Box flexDirection="row">
|
|
79
|
+
<Box marginRight={2}>
|
|
80
|
+
<Text>
|
|
81
|
+
Total: <Text bold>{worktrees.length}</Text>
|
|
82
|
+
</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
<Box marginRight={2}>
|
|
85
|
+
<Text color="green">
|
|
86
|
+
Accessible: <Text bold>{accessibleCount}</Text>
|
|
87
|
+
</Text>
|
|
88
|
+
</Box>
|
|
89
|
+
{inaccessibleCount > 0 && (
|
|
90
|
+
<Box>
|
|
91
|
+
<Text color="red">
|
|
92
|
+
Inaccessible: <Text bold>{inaccessibleCount}</Text>
|
|
93
|
+
</Text>
|
|
94
|
+
</Box>
|
|
95
|
+
)}
|
|
96
|
+
</Box>
|
|
97
|
+
</Box>
|
|
98
|
+
|
|
99
|
+
{/* Empty line */}
|
|
100
|
+
<Box height={1} />
|
|
101
|
+
|
|
102
|
+
{/* Content */}
|
|
103
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
104
|
+
{worktrees.length === 0 ? (
|
|
105
|
+
<Box>
|
|
106
|
+
<Text dimColor>No worktrees found</Text>
|
|
107
|
+
</Box>
|
|
108
|
+
) : (
|
|
109
|
+
<Select items={worktreeItems} onSelect={onSelect} limit={limit} />
|
|
110
|
+
)}
|
|
111
|
+
</Box>
|
|
112
|
+
|
|
113
|
+
{/* Footer */}
|
|
114
|
+
<Footer actions={footerActions} />
|
|
115
|
+
</Box>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import { BatchMergeService } from "../../../services/BatchMergeService.js";
|
|
3
|
+
import type {
|
|
4
|
+
BatchMergeConfig,
|
|
5
|
+
BatchMergeProgress,
|
|
6
|
+
BatchMergeResult,
|
|
7
|
+
BranchMergeStatus,
|
|
8
|
+
} from "../types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* useBatchMerge hook - Manages batch merge state and execution
|
|
12
|
+
* @see specs/SPEC-ee33ca26/plan.md - Service layer integration
|
|
13
|
+
*/
|
|
14
|
+
export function useBatchMerge() {
|
|
15
|
+
const [isExecuting, setIsExecuting] = useState(false);
|
|
16
|
+
const [progress, setProgress] = useState<BatchMergeProgress | null>(null);
|
|
17
|
+
const [statuses, setStatuses] = useState<BranchMergeStatus[]>([]);
|
|
18
|
+
const [result, setResult] = useState<BatchMergeResult | null>(null);
|
|
19
|
+
const [error, setError] = useState<Error | null>(null);
|
|
20
|
+
|
|
21
|
+
const service = new BatchMergeService();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute batch merge
|
|
25
|
+
*/
|
|
26
|
+
const executeBatchMerge = useCallback(
|
|
27
|
+
async (config: BatchMergeConfig) => {
|
|
28
|
+
try {
|
|
29
|
+
setIsExecuting(true);
|
|
30
|
+
setProgress(null);
|
|
31
|
+
setStatuses([]);
|
|
32
|
+
setResult(null);
|
|
33
|
+
setError(null);
|
|
34
|
+
|
|
35
|
+
const mergeResult = await service.executeBatchMerge(
|
|
36
|
+
config,
|
|
37
|
+
(progressUpdate) => {
|
|
38
|
+
setProgress(progressUpdate);
|
|
39
|
+
|
|
40
|
+
// Update statuses as branches are processed
|
|
41
|
+
// This is a simplified version; real implementation would track completed branches
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
setResult(mergeResult);
|
|
46
|
+
setStatuses(mergeResult.statuses);
|
|
47
|
+
return mergeResult;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
50
|
+
setError(error);
|
|
51
|
+
throw error;
|
|
52
|
+
} finally {
|
|
53
|
+
setIsExecuting(false);
|
|
54
|
+
setProgress(null);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[service],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Determine source branch automatically
|
|
62
|
+
*/
|
|
63
|
+
const determineSourceBranch = useCallback(async () => {
|
|
64
|
+
return await service.determineSourceBranch();
|
|
65
|
+
}, [service]);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get target branches
|
|
69
|
+
*/
|
|
70
|
+
const getTargetBranches = useCallback(async () => {
|
|
71
|
+
return await service.getTargetBranches();
|
|
72
|
+
}, [service]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reset state
|
|
76
|
+
*/
|
|
77
|
+
const reset = useCallback(() => {
|
|
78
|
+
setIsExecuting(false);
|
|
79
|
+
setProgress(null);
|
|
80
|
+
setStatuses([]);
|
|
81
|
+
setResult(null);
|
|
82
|
+
setError(null);
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
isExecuting,
|
|
87
|
+
progress,
|
|
88
|
+
statuses,
|
|
89
|
+
result,
|
|
90
|
+
error,
|
|
91
|
+
executeBatchMerge,
|
|
92
|
+
determineSourceBranch,
|
|
93
|
+
getTargetBranches,
|
|
94
|
+
reset,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
getAllBranches,
|
|
4
|
+
hasUnpushedCommitsInRepo,
|
|
5
|
+
getRepositoryRoot,
|
|
6
|
+
} from "../../../git.js";
|
|
7
|
+
import { listAdditionalWorktrees } from "../../../worktree.js";
|
|
8
|
+
import { getPullRequestByBranch } from "../../../github.js";
|
|
9
|
+
import type { BranchInfo, WorktreeInfo } from "../types.js";
|
|
10
|
+
import type { WorktreeInfo as GitWorktreeInfo } from "../../../worktree.js";
|
|
11
|
+
|
|
12
|
+
export interface UseGitDataOptions {
|
|
13
|
+
enableAutoRefresh?: boolean;
|
|
14
|
+
refreshInterval?: number; // milliseconds (default: 5000ms = 5s)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseGitDataResult {
|
|
18
|
+
branches: BranchInfo[];
|
|
19
|
+
worktrees: GitWorktreeInfo[];
|
|
20
|
+
loading: boolean;
|
|
21
|
+
error: Error | null;
|
|
22
|
+
refresh: () => void;
|
|
23
|
+
lastUpdated: Date | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Hook to fetch and manage Git data (branches and worktrees)
|
|
28
|
+
* @param options - Configuration options for auto-refresh and polling interval
|
|
29
|
+
*/
|
|
30
|
+
export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
31
|
+
const { enableAutoRefresh = false, refreshInterval = 5000 } = options || {};
|
|
32
|
+
const [branches, setBranches] = useState<BranchInfo[]>([]);
|
|
33
|
+
const [worktrees, setWorktrees] = useState<GitWorktreeInfo[]>([]);
|
|
34
|
+
const [loading, setLoading] = useState(true);
|
|
35
|
+
const [error, setError] = useState<Error | null>(null);
|
|
36
|
+
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
|
37
|
+
|
|
38
|
+
const loadData = useCallback(async () => {
|
|
39
|
+
setLoading(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const [branchesData, worktreesData] = await Promise.all([
|
|
44
|
+
getAllBranches(),
|
|
45
|
+
listAdditionalWorktrees(),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// Store worktrees separately
|
|
49
|
+
setWorktrees(worktreesData);
|
|
50
|
+
|
|
51
|
+
// Map worktrees to branches
|
|
52
|
+
const worktreeMap = new Map<string, WorktreeInfo>();
|
|
53
|
+
for (const worktree of worktreesData) {
|
|
54
|
+
// Convert worktree.ts WorktreeInfo to ui/types.ts WorktreeInfo
|
|
55
|
+
const uiWorktreeInfo: WorktreeInfo = {
|
|
56
|
+
path: worktree.path,
|
|
57
|
+
locked: false, // worktree.ts doesn't expose locked status
|
|
58
|
+
prunable: worktree.isAccessible === false,
|
|
59
|
+
isAccessible: worktree.isAccessible ?? true, // Default to true if undefined
|
|
60
|
+
};
|
|
61
|
+
worktreeMap.set(worktree.branch, uiWorktreeInfo);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get repository root for unpushed commits check
|
|
65
|
+
const repoRoot = await getRepositoryRoot();
|
|
66
|
+
|
|
67
|
+
// Attach worktree info and check unpushed/PR status for local branches
|
|
68
|
+
const enrichedBranches = await Promise.all(
|
|
69
|
+
branchesData.map(async (branch) => {
|
|
70
|
+
const worktreeInfo = worktreeMap.get(branch.name);
|
|
71
|
+
let hasUnpushed = false;
|
|
72
|
+
let prInfo = null;
|
|
73
|
+
|
|
74
|
+
// Only check unpushed commits and PR status for local branches
|
|
75
|
+
if (branch.type === "local") {
|
|
76
|
+
try {
|
|
77
|
+
// Check for unpushed commits
|
|
78
|
+
hasUnpushed = await hasUnpushedCommitsInRepo(
|
|
79
|
+
branch.name,
|
|
80
|
+
repoRoot,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Check for PR status
|
|
84
|
+
prInfo = await getPullRequestByBranch(branch.name);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// Silently ignore errors to avoid breaking the UI
|
|
87
|
+
if (process.env.DEBUG) {
|
|
88
|
+
console.error(
|
|
89
|
+
`Failed to check status for ${branch.name}:`,
|
|
90
|
+
error,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
...branch,
|
|
98
|
+
...(worktreeInfo ? { worktree: worktreeInfo } : {}),
|
|
99
|
+
...(hasUnpushed ? { hasUnpushedCommits: true } : {}),
|
|
100
|
+
...(prInfo?.state === "OPEN"
|
|
101
|
+
? { openPR: { number: prInfo.number, title: prInfo.title } }
|
|
102
|
+
: {}),
|
|
103
|
+
...(prInfo?.state === "MERGED" && prInfo.mergedAt
|
|
104
|
+
? {
|
|
105
|
+
mergedPR: {
|
|
106
|
+
number: prInfo.number,
|
|
107
|
+
mergedAt: prInfo.mergedAt,
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
: {}),
|
|
111
|
+
};
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
setBranches(enrichedBranches);
|
|
116
|
+
setLastUpdated(new Date());
|
|
117
|
+
} catch (err) {
|
|
118
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
119
|
+
setBranches([]);
|
|
120
|
+
setWorktrees([]);
|
|
121
|
+
} finally {
|
|
122
|
+
setLoading(false);
|
|
123
|
+
}
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
const refresh = useCallback(() => {
|
|
127
|
+
loadData();
|
|
128
|
+
}, [loadData]);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
loadData();
|
|
132
|
+
}, [loadData]);
|
|
133
|
+
|
|
134
|
+
// Auto-refresh polling (if enabled)
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!enableAutoRefresh) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const intervalId = setInterval(() => {
|
|
141
|
+
loadData();
|
|
142
|
+
}, refreshInterval);
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
clearInterval(intervalId);
|
|
146
|
+
};
|
|
147
|
+
}, [enableAutoRefresh, refreshInterval, loadData]);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
branches,
|
|
151
|
+
worktrees,
|
|
152
|
+
loading,
|
|
153
|
+
error,
|
|
154
|
+
refresh,
|
|
155
|
+
lastUpdated,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import type { ScreenType } from "../types.js";
|
|
3
|
+
|
|
4
|
+
export interface ScreenStateResult {
|
|
5
|
+
currentScreen: ScreenType;
|
|
6
|
+
navigateTo: (screen: ScreenType) => void;
|
|
7
|
+
goBack: () => void;
|
|
8
|
+
reset: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const INITIAL_SCREEN: ScreenType = "branch-list";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hook to manage screen navigation state with history
|
|
15
|
+
*/
|
|
16
|
+
export function useScreenState(): ScreenStateResult {
|
|
17
|
+
const [history, setHistory] = useState<ScreenType[]>([INITIAL_SCREEN]);
|
|
18
|
+
|
|
19
|
+
const currentScreen = history[history.length - 1] ?? INITIAL_SCREEN;
|
|
20
|
+
|
|
21
|
+
const navigateTo = useCallback((screen: ScreenType) => {
|
|
22
|
+
setHistory((prev) => [...prev, screen]);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const goBack = useCallback(() => {
|
|
26
|
+
setHistory((prev) => {
|
|
27
|
+
if (prev.length <= 1) {
|
|
28
|
+
return prev; // Stay at initial screen
|
|
29
|
+
}
|
|
30
|
+
return prev.slice(0, -1);
|
|
31
|
+
});
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const reset = useCallback(() => {
|
|
35
|
+
setHistory([INITIAL_SCREEN]);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
currentScreen,
|
|
40
|
+
navigateTo,
|
|
41
|
+
goBack,
|
|
42
|
+
reset,
|
|
43
|
+
};
|
|
44
|
+
}
|