@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,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
+ ? `${line}`
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
+ }