@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.
Files changed (132) hide show
  1. package/README.ja.md +323 -0
  2. package/README.md +347 -0
  3. package/bin/gwt.js +5 -0
  4. package/package.json +125 -0
  5. package/src/claude-history.ts +717 -0
  6. package/src/claude.ts +292 -0
  7. package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
  8. package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
  9. package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
  10. package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
  11. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
  12. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
  13. package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
  14. package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
  15. package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
  16. package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
  17. package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
  18. package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
  19. package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
  20. package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
  21. package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
  22. package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
  23. package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
  24. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
  25. package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
  26. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
  27. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
  28. package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
  29. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
  30. package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
  31. package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
  32. package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
  33. package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
  34. package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
  35. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
  36. package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
  37. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
  38. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
  39. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
  40. package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
  41. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
  42. package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
  43. package/src/cli/ui/components/App.tsx +793 -0
  44. package/src/cli/ui/components/common/Confirm.tsx +40 -0
  45. package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
  46. package/src/cli/ui/components/common/Input.tsx +36 -0
  47. package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
  48. package/src/cli/ui/components/common/Select.tsx +216 -0
  49. package/src/cli/ui/components/parts/Footer.tsx +41 -0
  50. package/src/cli/ui/components/parts/Header.test.tsx +85 -0
  51. package/src/cli/ui/components/parts/Header.tsx +63 -0
  52. package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
  53. package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
  54. package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
  55. package/src/cli/ui/components/parts/Stats.tsx +67 -0
  56. package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
  57. package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
  58. package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
  59. package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
  60. package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
  61. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
  62. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
  63. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
  64. package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
  65. package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
  66. package/src/cli/ui/hooks/useGitData.ts +157 -0
  67. package/src/cli/ui/hooks/useScreenState.ts +44 -0
  68. package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
  69. package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
  70. package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
  71. package/src/cli/ui/types.ts +295 -0
  72. package/src/cli/ui/utils/baseBranch.ts +34 -0
  73. package/src/cli/ui/utils/branchFormatter.ts +222 -0
  74. package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
  75. package/src/codex.ts +139 -0
  76. package/src/config/builtin-tools.ts +44 -0
  77. package/src/config/constants.ts +100 -0
  78. package/src/config/env-history.ts +45 -0
  79. package/src/config/index.ts +204 -0
  80. package/src/config/tools.ts +293 -0
  81. package/src/git.ts +1102 -0
  82. package/src/github.ts +158 -0
  83. package/src/index.test.ts +87 -0
  84. package/src/index.ts +684 -0
  85. package/src/index.ts.backup +1543 -0
  86. package/src/launcher.ts +142 -0
  87. package/src/repositories/git.repository.ts +129 -0
  88. package/src/repositories/github.repository.ts +83 -0
  89. package/src/repositories/worktree.repository.ts +69 -0
  90. package/src/services/BatchMergeService.ts +251 -0
  91. package/src/services/WorktreeOrchestrator.ts +115 -0
  92. package/src/services/__tests__/BatchMergeService.test.ts +518 -0
  93. package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
  94. package/src/services/dependency-installer.ts +199 -0
  95. package/src/services/git.service.ts +113 -0
  96. package/src/services/github.service.ts +61 -0
  97. package/src/services/worktree.service.ts +66 -0
  98. package/src/types/api.ts +241 -0
  99. package/src/types/tools.ts +235 -0
  100. package/src/utils/spinner.ts +54 -0
  101. package/src/utils/terminal.ts +272 -0
  102. package/src/utils.test.ts +43 -0
  103. package/src/utils.ts +60 -0
  104. package/src/web/client/index.html +12 -0
  105. package/src/web/client/src/components/BranchGraph.tsx +231 -0
  106. package/src/web/client/src/components/EnvEditor.tsx +145 -0
  107. package/src/web/client/src/components/Terminal.tsx +137 -0
  108. package/src/web/client/src/hooks/useBranches.ts +41 -0
  109. package/src/web/client/src/hooks/useConfig.ts +31 -0
  110. package/src/web/client/src/hooks/useSessions.ts +59 -0
  111. package/src/web/client/src/hooks/useWorktrees.ts +47 -0
  112. package/src/web/client/src/index.css +834 -0
  113. package/src/web/client/src/lib/api.ts +184 -0
  114. package/src/web/client/src/lib/websocket.ts +174 -0
  115. package/src/web/client/src/main.tsx +29 -0
  116. package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
  117. package/src/web/client/src/pages/BranchListPage.tsx +264 -0
  118. package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
  119. package/src/web/client/src/router.tsx +27 -0
  120. package/src/web/client/vite.config.ts +21 -0
  121. package/src/web/server/env/importer.ts +54 -0
  122. package/src/web/server/index.ts +74 -0
  123. package/src/web/server/pty/manager.ts +189 -0
  124. package/src/web/server/routes/branches.ts +126 -0
  125. package/src/web/server/routes/config.ts +220 -0
  126. package/src/web/server/routes/index.ts +37 -0
  127. package/src/web/server/routes/sessions.ts +130 -0
  128. package/src/web/server/routes/worktrees.ts +108 -0
  129. package/src/web/server/services/branches.ts +368 -0
  130. package/src/web/server/services/worktrees.ts +85 -0
  131. package/src/web/server/websocket/handler.ts +180 -0
  132. 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
+ }