@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,75 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import type { BranchMergeStatus } from "../../types.js";
4
+
5
+ export interface MergeStatusListProps {
6
+ statuses: BranchMergeStatus[];
7
+ maxVisible?: number;
8
+ }
9
+
10
+ /**
11
+ * Get status icon and color based on merge status
12
+ */
13
+ function getStatusDisplay(status: BranchMergeStatus["status"]): {
14
+ icon: string;
15
+ color: string;
16
+ } {
17
+ switch (status) {
18
+ case "success":
19
+ return { icon: "✓", color: "green" };
20
+ case "skipped":
21
+ return { icon: "⊘", color: "yellow" };
22
+ case "failed":
23
+ return { icon: "✗", color: "red" };
24
+ }
25
+ }
26
+
27
+ /**
28
+ * MergeStatusList component - displays list of merge statuses
29
+ * Optimized with React.memo to prevent unnecessary re-renders
30
+ * @see specs/SPEC-ee33ca26/spec.md - FR-015
31
+ */
32
+ export const MergeStatusList = React.memo(function MergeStatusList({
33
+ statuses,
34
+ maxVisible = 10,
35
+ }: MergeStatusListProps) {
36
+ const visibleStatuses = statuses.slice(-maxVisible);
37
+ const hiddenCount = statuses.length - visibleStatuses.length;
38
+
39
+ return (
40
+ <Box flexDirection="column">
41
+ {hiddenCount > 0 && (
42
+ <Box marginBottom={1}>
43
+ <Text dimColor>... {hiddenCount} more branches above</Text>
44
+ </Box>
45
+ )}
46
+
47
+ {visibleStatuses.map((status) => {
48
+ const { icon, color } = getStatusDisplay(status.status);
49
+
50
+ return (
51
+ <Box key={status.branchName}>
52
+ <Text color={color}>{icon} </Text>
53
+ <Text>{status.branchName}</Text>
54
+ <Text dimColor> ({status.durationSeconds.toFixed(1)}s)</Text>
55
+
56
+ {status.status === "skipped" && status.conflictFiles && (
57
+ <Text color="yellow">
58
+ {" "}
59
+ - Conflicts: {status.conflictFiles.length} files
60
+ </Text>
61
+ )}
62
+
63
+ {status.status === "failed" && status.error && (
64
+ <Text color="red"> - {status.error}</Text>
65
+ )}
66
+
67
+ {status.worktreeCreated && (
68
+ <Text dimColor> [worktree created]</Text>
69
+ )}
70
+ </Box>
71
+ );
72
+ })}
73
+ </Box>
74
+ );
75
+ });
@@ -0,0 +1,73 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import type { BatchMergeProgress } from "../../types.js";
4
+
5
+ export interface ProgressBarProps {
6
+ progress: BatchMergeProgress;
7
+ width?: number;
8
+ }
9
+
10
+ /**
11
+ * Format seconds to human-readable time (e.g., "1m 23s", "45s")
12
+ */
13
+ function formatTime(seconds: number): string {
14
+ if (seconds < 60) {
15
+ return `${Math.floor(seconds)}s`;
16
+ }
17
+
18
+ const minutes = Math.floor(seconds / 60);
19
+ const remainingSeconds = Math.floor(seconds % 60);
20
+ return `${minutes}m ${remainingSeconds}s`;
21
+ }
22
+
23
+ /**
24
+ * ProgressBar component - displays batch merge progress
25
+ * Optimized with React.memo to prevent unnecessary re-renders
26
+ * @see specs/SPEC-ee33ca26/spec.md - FR-009
27
+ */
28
+ export const ProgressBar = React.memo(function ProgressBar({
29
+ progress,
30
+ width = 40,
31
+ }: ProgressBarProps) {
32
+ const filledWidth = Math.floor((progress.percentage / 100) * width);
33
+ const emptyWidth = width - filledWidth;
34
+
35
+ const filledBar = "█".repeat(filledWidth);
36
+ const emptyBar = "░".repeat(emptyWidth);
37
+
38
+ return (
39
+ <Box flexDirection="column">
40
+ <Box>
41
+ <Text dimColor>Current: </Text>
42
+ <Text bold color="cyan">
43
+ {progress.currentBranch}
44
+ </Text>
45
+ <Text dimColor>
46
+ {" "}
47
+ ({progress.currentIndex + 1}/{progress.totalBranches})
48
+ </Text>
49
+ </Box>
50
+
51
+ <Box marginTop={1}>
52
+ <Text color="green">{filledBar}</Text>
53
+ <Text dimColor>{emptyBar}</Text>
54
+ <Text dimColor> {progress.percentage}%</Text>
55
+ </Box>
56
+
57
+ <Box marginTop={1}>
58
+ <Text dimColor>Phase: </Text>
59
+ <Text color="yellow">{progress.currentPhase}</Text>
60
+ <Text dimColor> | Elapsed: </Text>
61
+ <Text color="magenta">{formatTime(progress.elapsedSeconds)}</Text>
62
+ {progress.estimatedRemainingSeconds !== undefined && (
63
+ <>
64
+ <Text dimColor> | Remaining: </Text>
65
+ <Text color="gray">
66
+ ~{formatTime(progress.estimatedRemainingSeconds)}
67
+ </Text>
68
+ </>
69
+ )}
70
+ </Box>
71
+ </Box>
72
+ );
73
+ });
@@ -0,0 +1,24 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { Box } from "ink";
3
+
4
+ export interface ScrollableListProps {
5
+ children: ReactNode;
6
+ maxHeight?: number;
7
+ }
8
+
9
+ /**
10
+ * ScrollableList component - container for scrollable content
11
+ * Note: Actual scrolling is handled by ink-select-input's limit prop
12
+ * This component provides a consistent container for list content
13
+ */
14
+ export function ScrollableList({ children, maxHeight }: ScrollableListProps) {
15
+ return (
16
+ <Box
17
+ flexDirection="column"
18
+ {...(maxHeight !== undefined && { height: maxHeight })}
19
+ overflow="hidden"
20
+ >
21
+ {children}
22
+ </Box>
23
+ );
24
+ }
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import type { Statistics } from "../../types.js";
4
+
5
+ export interface StatsProps {
6
+ stats: Statistics;
7
+ separator?: string;
8
+ lastUpdated?: Date | null;
9
+ }
10
+
11
+ /**
12
+ * Format relative time (e.g., "5s ago", "2m ago", "1h ago")
13
+ */
14
+ function formatRelativeTime(date: Date): string {
15
+ const now = new Date();
16
+ const diffMs = now.getTime() - date.getTime();
17
+ const diffSec = Math.floor(diffMs / 1000);
18
+
19
+ if (diffSec < 60) {
20
+ return `${diffSec}s ago`;
21
+ }
22
+
23
+ const diffMin = Math.floor(diffSec / 60);
24
+ if (diffMin < 60) {
25
+ return `${diffMin}m ago`;
26
+ }
27
+
28
+ const diffHour = Math.floor(diffMin / 60);
29
+ return `${diffHour}h ago`;
30
+ }
31
+
32
+ /**
33
+ * Stats component - displays statistics in one line
34
+ * Optimized with React.memo to prevent unnecessary re-renders
35
+ */
36
+ export const Stats = React.memo(function Stats({
37
+ stats,
38
+ separator = " ",
39
+ lastUpdated = null,
40
+ }: StatsProps) {
41
+ const items = [
42
+ { label: "Local", value: stats.localCount, color: "cyan" },
43
+ { label: "Remote", value: stats.remoteCount, color: "green" },
44
+ { label: "Worktrees", value: stats.worktreeCount, color: "yellow" },
45
+ { label: "Changes", value: stats.changesCount, color: "magenta" },
46
+ ];
47
+
48
+ return (
49
+ <Box>
50
+ {items.map((item) => (
51
+ <Box key={item.label}>
52
+ <Text dimColor>{item.label}: </Text>
53
+ <Text bold color={item.color}>
54
+ {item.value}
55
+ </Text>
56
+ <Text dimColor>{separator}</Text>
57
+ </Box>
58
+ ))}
59
+ {lastUpdated && (
60
+ <Box>
61
+ <Text dimColor>Updated: </Text>
62
+ <Text color="gray">{formatRelativeTime(lastUpdated)}</Text>
63
+ </Box>
64
+ )}
65
+ </Box>
66
+ );
67
+ });
@@ -0,0 +1,116 @@
1
+ import React, { useState, useEffect } 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 { getAllTools } from '../../../../config/tools.js';
8
+ import type { AIToolConfig } from '../../../../types/tools.js';
9
+
10
+ export type AITool = string;
11
+
12
+ export interface AIToolItem {
13
+ label: string;
14
+ value: AITool;
15
+ description: string;
16
+ }
17
+
18
+ export interface AIToolSelectorScreenProps {
19
+ onBack: () => void;
20
+ onSelect: (tool: AITool) => void;
21
+ version?: string | null;
22
+ }
23
+
24
+ /**
25
+ * AIToolSelectorScreen - Screen for selecting AI tool (Claude Code, Codex CLI, or custom tools)
26
+ * Layout: Header + Tool Selection + Footer
27
+ *
28
+ * This screen dynamically loads available tools from the configuration (builtin + custom).
29
+ */
30
+ export function AIToolSelectorScreen({ onBack, onSelect, version }: AIToolSelectorScreenProps) {
31
+ const { rows } = useTerminalSize();
32
+ const [toolItems, setToolItems] = useState<AIToolItem[]>([]);
33
+ const [isLoading, setIsLoading] = useState(true);
34
+
35
+ // Load tools from getAllTools()
36
+ useEffect(() => {
37
+ const loadTools = async () => {
38
+ try {
39
+ const tools = await getAllTools();
40
+
41
+ // Convert AIToolConfig[] to AIToolItem[]
42
+ const items: AIToolItem[] = tools.map((tool: AIToolConfig) => {
43
+ // Generate description based on whether it's builtin or custom
44
+ const description = tool.isBuiltin
45
+ ? `Official ${tool.displayName} tool`
46
+ : `Custom AI tool`;
47
+
48
+ // Add icon to label if present
49
+ const label = tool.icon
50
+ ? `${tool.icon} ${tool.displayName}`
51
+ : tool.displayName;
52
+
53
+ return {
54
+ label,
55
+ value: tool.id,
56
+ description,
57
+ };
58
+ });
59
+
60
+ setToolItems(items);
61
+ } catch (error) {
62
+ // If loading fails, show error in console but don't crash
63
+ console.error('Failed to load tools:', error);
64
+ // Fall back to empty array
65
+ setToolItems([]);
66
+ } finally {
67
+ setIsLoading(false);
68
+ }
69
+ };
70
+
71
+ loadTools();
72
+ }, []);
73
+
74
+ // Handle keyboard input
75
+ // Note: Select component handles Enter and arrow keys
76
+ useInput((input, key) => {
77
+ if (key.escape) {
78
+ onBack();
79
+ }
80
+ });
81
+
82
+ // Handle tool selection
83
+ const handleSelect = (item: AIToolItem) => {
84
+ onSelect(item.value);
85
+ };
86
+
87
+ // Footer actions
88
+ const footerActions = [
89
+ { key: 'enter', description: 'Select' },
90
+ { key: 'esc', description: 'Back' },
91
+ ];
92
+
93
+ return (
94
+ <Box flexDirection="column" height={rows}>
95
+ {/* Header */}
96
+ <Header title="AI Tool Selection" titleColor="blue" version={version} />
97
+
98
+ {/* Content */}
99
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
100
+ <Box marginBottom={1}>
101
+ <Text>Select AI tool to use:</Text>
102
+ </Box>
103
+ {isLoading ? (
104
+ <Text>Loading tools...</Text>
105
+ ) : toolItems.length === 0 ? (
106
+ <Text color="yellow">No tools available. Please check your configuration.</Text>
107
+ ) : (
108
+ <Select items={toolItems} onSelect={handleSelect} />
109
+ )}
110
+ </Box>
111
+
112
+ {/* Footer */}
113
+ <Footer actions={footerActions} />
114
+ </Box>
115
+ );
116
+ }
@@ -0,0 +1,70 @@
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 { ProgressBar } from "../parts/ProgressBar.js";
6
+ import { MergeStatusList } from "../parts/MergeStatusList.js";
7
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
8
+ import type { BatchMergeProgress, BranchMergeStatus } from "../../types.js";
9
+
10
+ export interface BatchMergeProgressScreenProps {
11
+ progress: BatchMergeProgress;
12
+ statuses: BranchMergeStatus[];
13
+ onCancel?: () => void;
14
+ }
15
+
16
+ /**
17
+ * BatchMergeProgressScreen - Real-time progress display for batch merge
18
+ * Layout: Header + Progress Bar + Status List + Footer
19
+ * @see specs/SPEC-ee33ca26/spec.md - User Story 4
20
+ */
21
+ export function BatchMergeProgressScreen({
22
+ progress,
23
+ statuses,
24
+ onCancel,
25
+ }: BatchMergeProgressScreenProps) {
26
+ const { rows } = useTerminalSize();
27
+
28
+ // Handle keyboard input
29
+ useInput((input, key) => {
30
+ if ((input === "q" || key.escape) && onCancel) {
31
+ onCancel();
32
+ }
33
+ });
34
+
35
+ // Calculate available space for status list
36
+ const headerLines = 1;
37
+ const progressLines = 5; // Progress bar takes ~5 lines
38
+ const footerLines = 1;
39
+ const maxStatusLines = Math.max(
40
+ 5,
41
+ rows - headerLines - progressLines - footerLines - 2,
42
+ );
43
+
44
+ return (
45
+ <Box flexDirection="column" height={rows}>
46
+ <Header title="Batch Merge in Progress" />
47
+
48
+ <Box flexDirection="column" paddingX={2} paddingY={1}>
49
+ <ProgressBar progress={progress} />
50
+
51
+ <Box marginTop={2}>
52
+ <Text bold>Recent Branches:</Text>
53
+ </Box>
54
+
55
+ <Box marginTop={1}>
56
+ <MergeStatusList statuses={statuses} maxVisible={maxStatusLines} />
57
+ </Box>
58
+ </Box>
59
+
60
+ <Footer
61
+ actions={[
62
+ {
63
+ key: "q",
64
+ description: `Cancel | Processing ${progress.currentIndex + 1}/${progress.totalBranches}`,
65
+ },
66
+ ]}
67
+ />
68
+ </Box>
69
+ );
70
+ }
@@ -0,0 +1,104 @@
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 { MergeStatusList } from "../parts/MergeStatusList.js";
6
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
+ import type { BatchMergeResult } from "../../types.js";
8
+
9
+ export interface BatchMergeResultScreenProps {
10
+ result: BatchMergeResult;
11
+ onBack?: () => void;
12
+ onQuit?: () => void;
13
+ }
14
+
15
+ /**
16
+ * BatchMergeResultScreen - Final result summary for batch merge
17
+ * Layout: Header + Summary + Status List + Footer
18
+ * @see specs/SPEC-ee33ca26/spec.md - FR-010
19
+ */
20
+ export function BatchMergeResultScreen({
21
+ result,
22
+ onBack,
23
+ onQuit,
24
+ }: BatchMergeResultScreenProps) {
25
+ const { rows } = useTerminalSize();
26
+
27
+ // Handle keyboard input
28
+ useInput((input, key) => {
29
+ if (input === "q" && onQuit) {
30
+ onQuit();
31
+ } else if (key.escape && onBack) {
32
+ onBack();
33
+ }
34
+ });
35
+
36
+ const { summary, totalDurationSeconds, cancelled } = result;
37
+
38
+ return (
39
+ <Box flexDirection="column" height={rows}>
40
+ <Header
41
+ title={cancelled ? "Batch Merge Cancelled" : "Batch Merge Complete"}
42
+ />
43
+
44
+ <Box flexDirection="column" paddingX={2} paddingY={1}>
45
+ {/* Summary Statistics */}
46
+ <Box flexDirection="column" marginBottom={2}>
47
+ <Text bold>Summary:</Text>
48
+ <Box marginTop={1}>
49
+ <Text color="green">✓ Success: {summary.successCount}</Text>
50
+ <Text dimColor> | </Text>
51
+ <Text color="yellow">⊘ Skipped: {summary.skippedCount}</Text>
52
+ <Text dimColor> | </Text>
53
+ <Text color="red">✗ Failed: {summary.failedCount}</Text>
54
+ <Text dimColor> | </Text>
55
+ <Text dimColor>Total: {totalDurationSeconds.toFixed(1)}s</Text>
56
+ </Box>
57
+
58
+ {summary.pushedCount > 0 && (
59
+ <Box marginTop={1}>
60
+ <Text color="cyan">↑ Pushed: {summary.pushedCount}</Text>
61
+ {summary.pushFailedCount > 0 && (
62
+ <>
63
+ <Text dimColor> | </Text>
64
+ <Text color="red">
65
+ Push Failed: {summary.pushFailedCount}
66
+ </Text>
67
+ </>
68
+ )}
69
+ </Box>
70
+ )}
71
+ </Box>
72
+
73
+ {/* Detailed Status List */}
74
+ <Box flexDirection="column">
75
+ <Text bold>Branch Details:</Text>
76
+ <Box marginTop={1}>
77
+ <MergeStatusList
78
+ statuses={result.statuses}
79
+ maxVisible={rows - 12}
80
+ />
81
+ </Box>
82
+ </Box>
83
+
84
+ {/* Messages for next actions */}
85
+ {summary.skippedCount > 0 && (
86
+ <Box marginTop={2}>
87
+ <Text color="yellow">
88
+ Note: {summary.skippedCount} branch
89
+ {summary.skippedCount > 1 ? "es" : ""} skipped due to conflicts.
90
+ Please resolve manually.
91
+ </Text>
92
+ </Box>
93
+ )}
94
+ </Box>
95
+
96
+ <Footer
97
+ actions={[
98
+ { key: "q", description: "Quit" },
99
+ ...(onBack ? [{ key: "ESC", description: "Back" }] : []),
100
+ ]}
101
+ />
102
+ </Box>
103
+ );
104
+ }