@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,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
|
+
}
|