From eff14ef09402bb5a9fa1b44619112765a78a899b Mon Sep 17 00:00:00 2001 From: Anastasiia Solop <35258279+anastasiya1155@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:13:57 +0100 Subject: [PATCH] Add Push button to Git panel when PR is open with unpushed commits (Vibe Kanban) (#1951) * The implementation is complete. Here's a summary of what I changed: ## Summary Added a "Push" button to the workspaces UI Git panel. The changes allow users to push changes when a PR is already open. ### Files Modified: 1. **`frontend/src/components/ui-new/primitives/RepoCard.tsx`** (lines 27-42) - Added `'push'` to the `RepoAction` type union - Added a new option to `repoActionOptions` array with the Push button configuration using `ArrowUpIcon` 2. **`frontend/src/components/ui-new/actions/index.ts`** (lines 33, 691-707) - Imported `ArrowUpIcon` from phosphor-icons - Added `GitPush` action definition that: - Calls `attemptsApi.push()` with the workspace and repo IDs - Handles the `force_push_required` error case - Invalidates workspace queries on success to refresh the UI 3. **`frontend/src/components/ui-new/containers/WorkspacesLayout.tsx`** (line 103) - Added mapping from `'push'` action to `Actions.GitPush` in the action handler The Push button now appears in the Git panel dropdown alongside "Open pull request" and "Merge" options. * The type check passes. Here's a summary of the updated changes: ## Summary The Push button now appears **next to the PR number** when: 1. A PR is already open (`prStatus === 'open'`) 2. There are new commits ahead (`commitsAhead > 0`) ### Changes Made: **`frontend/src/components/ui-new/primitives/RepoCard.tsx`**: - Removed `push` from the `repoActionOptions` dropdown (lines 34-41) - Added a dedicated Push button in the PR status row (lines 213-222) that: - Only appears when `prStatus === 'open'` AND `commitsAhead > 0` - Uses brand primary styling (`bg-brand-primary`) to make it prominent - Displays with an up arrow icon and "Push" label using existing translation - Calls `onActionsClick?.('push')` when clicked The `GitPush` action and handler mapping from the previous changes remain in place to handle the push operation. * Done. The Push button now uses the same styling as the "Open PR" button: `bg-panel text-normal hover:bg-tertiary`. * The type check passes. Here's a summary of the fix: ## Summary The Push button now correctly uses `remoteCommitsAhead` (unpushed commits to remote) instead of `commitsAhead` (total commits ahead of target branch) to determine when to show. ### Changes Made: 1. **`frontend/src/components/ui-new/views/GitPanel.tsx`**: - Added `remoteCommitsAhead: number` to the `RepoInfo` interface - Pass `remoteCommitsAhead` to `RepoCard` 2. **`frontend/src/components/ui-new/primitives/RepoCard.tsx`**: - Added `remoteCommitsAhead?: number` to `RepoCardProps` - Changed the push button condition from `commitsAhead > 0` to `remoteCommitsAhead > 0` 3. **`frontend/src/components/ui-new/containers/WorkspacesLayout.tsx`**: - Added `remoteCommitsAhead: repoStatus?.remote_commits_ahead ?? 0` to the `repoInfos` mapping Now the Push button will: - Only appear when there's an open PR AND there are unpushed commits (`remote_commits_ahead > 0`) - Disappear after a successful push (when `remote_commits_ahead` becomes 0) - Reappear when new commits are made locally that haven't been pushed * The type check passes. Here's a summary of the changes: ## Summary The Push button now hides immediately when clicked and only reappears when new commits are available: ### Changes to `RepoCard.tsx`: - Added `useState` and `useRef` imports - Added `isPushHidden` state to track when the button should be hidden - Added `lastRemoteCommitsAhead` ref to track the previous value - Added `useEffect` that resets `isPushHidden` to `false` when `remoteCommitsAhead` increases (meaning new commits are available) - Added `handlePushClick` function that hides the button immediately and triggers the push action - Updated the button condition to include `!isPushHidden` The button will: 1. Hide immediately when clicked (no double-clicking possible) 2. Stay hidden until `remoteCommitsAhead` increases above the last known value 3. Reappear when new local commits are made that need to be pushed * Both the linter and type check pass. The linter errors are fixed. Here's a summary of the changes: ## Summary of Linter Fixes The linter was flagging that `RepoCard` (a presentational component) should not manage state or have side effects. I refactored the code to lift the state up to the container component: ### Changes: 1. **`RepoCard.tsx`** - Made purely presentational: - Removed `useState`, `useRef`, `useEffect` imports - Removed `remoteCommitsAhead` prop - Added `showPushButton` prop (boolean, controlled by parent) - Added `onPushClick` callback prop - The button now just renders based on `showPushButton` and calls `onPushClick` 2. **`GitPanel.tsx`** - Updated interface and props: - `RepoInfo` now has optional `remoteCommitsAhead` and `showPushButton` fields - Added `onPushClick` callback to `GitPanelProps` - Passes `showPushButton` and `onPushClick` to `RepoCard` 3. **`WorkspacesLayout.tsx`** - State management moved here: - Added `hiddenPushRepos` state (Set of repo IDs where push was clicked) - Added `lastRemoteCommitsAhead` ref to track previous values - Added `useEffect` to reset hidden state when `remoteCommitsAhead` increases - Added `repoInfosWithPushButton` memo that computes `showPushButton` for each repo - Added `handlePushClick` that hides button immediately and executes push action * prettier fix * Done. I've added `GitPush` to the Git actions list in the command bar. It now appears between "Merge" and "Rebase" in the Git Actions menu. * Both type check and lint pass. Done! The Push action in the command bar now has the same visibility conditions as in the Git panel - it only appears when: 1. There's a workspace 2. There are git repos 3. There's an open PR (`hasOpenPR`) 4. There are unpushed commits (`hasUnpushedCommits`) ### Summary of changes: 1. **`frontend/src/components/ui-new/actions/index.ts`**: - Added `hasOpenPR` and `hasUnpushedCommits` to `ActionVisibilityContext` - Updated `GitPush.isVisible` to check these new context properties 2. **`frontend/src/components/ui-new/actions/useActionVisibility.ts`**: - Added `useBranchStatus` hook to get branch status data - Computed `hasOpenPR` by checking if any repo has an open PR in merges - Computed `hasUnpushedCommits` by checking if any repo has `remote_commits_ahead > 0` - Added these values to the returned context * The refactoring is complete. Here's a summary of the changes: ## Summary of Changes The push button logic has been simplified by using React Query's mutation state instead of manually tracking hidden state with refs and effects. ### Key Changes: 1. **WorkspacesLayout.tsx (`GitPanelContainer`)**: - Removed the complex `useEffect` that tracked `lastRemoteCommitsAhead` and managed `hiddenPushRepos` state - Removed `hiddenPushRepos` state and `lastRemoteCommitsAhead` ref - Added `usePush` hook with callbacks that: - On success: awaits query invalidation before clearing the loading state - On error: clears the loading state - Added simple `pushingRepoId` state to track which repo is being pushed - The button now shows loading state during push and automatically disappears after refetch if `remoteCommitsAhead` becomes 0 2. **GitPanel.tsx**: - Added `isPushPending?: boolean` to `RepoInfo` interface - Passes `isPushPending` to `RepoCard` 3. **RepoCard.tsx**: - Added `isPushPending` prop - Button shows while either `showPushButton` or `isPushPending` is true - Button is disabled and shows spinner with "Pushing..." text when pending - Uses existing translation key `git.states.pushing` ### How it works now: 1. User clicks push button 2. Button immediately shows loading state (spinner + "Pushing...") 3. Button is disabled to prevent multiple clicks 4. Push mutation runs 5. On success, query invalidation completes before clearing loading state 6. Once `branchStatus` refetches, if `remoteCommitsAhead` is 0, `showPushButton` becomes false and button disappears 7. On error, loading state clears and button returns to normal This approach is simpler and more predictable than manually tracking previous values. * Done. The push button now shows success feedback like the old UI: ## Summary of Changes The push button now has three states: 1. **Normal**: Shows "Push" with an up arrow icon 2. **Pending**: Shows "Pushing..." with a spinning icon, button is disabled 3. **Success**: Shows "Pushed!" with a green checkmark icon and green background for 2 seconds The flow is: 1. User clicks "Push" 2. Button immediately shows "Pushing..." with spinner 3. Push completes, queries are invalidated and refetched 4. Button shows green "Pushed!" for 2 seconds 5. Button disappears (if no more commits to push) or returns to normal state * All code review issues have been addressed: ## Summary of Changes 1. **Unified push logic** - Now uses `executeAction(Actions.GitPush, ...)` instead of calling `usePush` hook directly. This ensures consistent error handling between the button and command bar. 2. **Proper setTimeout cleanup** - Added `successTimeoutRef` with a cleanup effect that clears the timeout when the component unmounts: ```typescript const successTimeoutRef = useRef | null>(null); useEffect(() => { return () => { if (successTimeoutRef.current) { clearTimeout(successTimeoutRef.current); } }; }, []); ``` 3. **Error feedback** - Added error state handling: - New `isPushError` prop flows through GitPanel to RepoCard - Button shows red "Failed" with warning icon when push fails - Error state clears after 3 seconds - Added `git.states.pushFailed` translation key 4. **Simplified state management** - Changed from separate `pushingRepoId` and `pushSuccessRepoId` to a single `pushStates` record with `PushState` type (`'idle' | 'pending' | 'success' | 'error'`) * All i18n checks pass now. * Both code review issues have been addressed: ## Summary of Changes ### 1. Fixed stale closure issue - Added a `pushStatesRef` that mirrors the `pushStates` state - Changed the check from `pushStates[repoId]` to `pushStatesRef.current[repoId]` - Removed `pushStates` from the dependency array ### 2. Added proper error logging and user-facing error message - Modified `ActionsContext.tsx` to re-throw errors after showing the dialog, so callers can handle them - Added `console.error('Push failed:', error)` for debugging purposes - The error dialog is already shown by `executeAction` via `ConfirmDialog` - The button now properly shows the error state when push fails * fmt * The fix is in place. The key change is: - When we're in a "push flow" (state is `pending`, `success`, or `error`), we set `showPushButton: false` but the button remains visible because of the `isPushPending || isPushSuccess || isPushError` condition in RepoCard - This prevents the race condition where the query refetch could hide the button before we set the success state * All checks pass. The changes now: 1. **Reverted ActionsContext** - Removed the re-throw to maintain backwards compatibility 2. **Use `usePush` hook directly** - For proper success/error detection instead of going through `executeAction` 3. **Show error dialog manually** - Using `ConfirmDialog.show()` when push fails 4. **Track current repo with ref** - `currentPushRepoRef` tracks which repo is being pushed so callbacks can update the correct state * All checks pass. Here's a summary of the force push handling: ## Force Push Functionality Yes, force push is fully supported. Here's how it works: **Old UI (`useGitOperations.ts`):** 1. When `usePush` fails with `errorData.type === 'force_push_required'` 2. It automatically shows `ForcePushDialog` 3. The dialog warns the user about the consequences and lets them confirm 4. If confirmed, it calls `useForcePush` to execute the force push **New UI (now updated):** 1. When the push button is clicked and fails with `force_push_required` 2. The push state is reset to `idle` (so the button returns to normal) 3. `ForcePushDialog` is shown with the attemptId and repoId 4. User can confirm or cancel the force push from the dialog The `ForcePushDialog` handles its own loading state, success/error feedback, and API call internally via `useForcePush` hook. * clear pushStates on workspace change Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/components/ui-new/actions/index.ts | 27 ++++ .../src/components/ui-new/actions/pages.ts | 1 + .../ui-new/actions/useActionVisibility.ts | 19 ++- .../ui-new/containers/WorkspacesLayout.tsx | 125 +++++++++++++++++- .../components/ui-new/primitives/RepoCard.tsx | 53 +++++++- .../src/components/ui-new/views/GitPanel.tsx | 12 ++ frontend/src/i18n/locales/en/tasks.json | 1 + frontend/src/i18n/locales/es/tasks.json | 1 + frontend/src/i18n/locales/ja/tasks.json | 1 + frontend/src/i18n/locales/ko/tasks.json | 1 + frontend/src/i18n/locales/zh-Hans/tasks.json | 1 + frontend/src/i18n/locales/zh-Hant/tasks.json | 1 + 12 files changed, 240 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/ui-new/actions/index.ts b/frontend/src/components/ui-new/actions/index.ts index 6234b3a1..56cd8fed 100644 --- a/frontend/src/components/ui-new/actions/index.ts +++ b/frontend/src/components/ui-new/actions/index.ts @@ -30,6 +30,7 @@ import { CrosshairIcon, DesktopIcon, PencilSimpleIcon, + ArrowUpIcon, } from '@phosphor-icons/react'; import { useDiffViewStore } from '@/stores/useDiffViewStore'; import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore'; @@ -99,6 +100,8 @@ export interface ActionVisibilityContext { // Git panel state hasGitRepos: boolean; hasMultipleRepos: boolean; + hasOpenPR: boolean; + hasUnpushedCommits: boolean; } // Base properties shared by all actions @@ -686,6 +689,30 @@ export const Actions = { }); }, }, + + GitPush: { + id: 'git-push', + label: 'Push', + icon: ArrowUpIcon, + requiresTarget: 'git', + isVisible: (ctx) => + ctx.hasWorkspace && + ctx.hasGitRepos && + ctx.hasOpenPR && + ctx.hasUnpushedCommits, + execute: async (ctx, workspaceId, repoId) => { + const result = await attemptsApi.push(workspaceId, { repo_id: repoId }); + if (!result.success) { + if (result.error?.type === 'force_push_required') { + throw new Error( + 'Force push required. The remote branch has diverged.' + ); + } + throw new Error('Failed to push changes'); + } + invalidateWorkspaceQueries(ctx.queryClient, workspaceId); + }, + }, } as const satisfies Record; // Helper to resolve dynamic label diff --git a/frontend/src/components/ui-new/actions/pages.ts b/frontend/src/components/ui-new/actions/pages.ts index 9b7eb637..cde344eb 100644 --- a/frontend/src/components/ui-new/actions/pages.ts +++ b/frontend/src/components/ui-new/actions/pages.ts @@ -166,6 +166,7 @@ export const Pages: Record = { items: [ { type: 'action', action: Actions.GitCreatePR }, { type: 'action', action: Actions.GitMerge }, + { type: 'action', action: Actions.GitPush }, { type: 'action', action: Actions.GitRebase }, { type: 'action', action: Actions.GitChangeTarget }, ], diff --git a/frontend/src/components/ui-new/actions/useActionVisibility.ts b/frontend/src/components/ui-new/actions/useActionVisibility.ts index d839b49a..2fed226d 100644 --- a/frontend/src/components/ui-new/actions/useActionVisibility.ts +++ b/frontend/src/components/ui-new/actions/useActionVisibility.ts @@ -5,7 +5,8 @@ import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore'; import { useWorkspaceContext } from '@/contexts/WorkspaceContext'; import { useUserSystem } from '@/components/ConfigProvider'; import { useDevServer } from '@/hooks/useDevServer'; -import type { Workspace } from 'shared/types'; +import { useBranchStatus } from '@/hooks/useBranchStatus'; +import type { Workspace, Merge } from 'shared/types'; import type { ActionVisibilityContext, ActionDefinition, @@ -29,6 +30,7 @@ export function useActionVisibilityContext(): ActionVisibilityContext { const { config } = useUserSystem(); const { isStarting, isStopping, runningDevServers } = useDevServer(workspaceId); + const { data: branchStatus } = useBranchStatus(workspaceId); return useMemo(() => { // Compute isAllDiffsExpanded @@ -45,6 +47,18 @@ export function useActionVisibilityContext(): ActionVisibilityContext { ? 'running' : 'stopped'; + // Compute git state from branch status + const hasOpenPR = + branchStatus?.some((repo) => + repo.merges?.some( + (m: Merge) => m.type === 'pr' && m.pr_info.status === 'open' + ) + ) ?? false; + + const hasUnpushedCommits = + branchStatus?.some((repo) => (repo.remote_commits_ahead ?? 0) > 0) ?? + false; + return { isChangesMode: layout.isChangesMode, isLogsMode: layout.isLogsMode, @@ -63,6 +77,8 @@ export function useActionVisibilityContext(): ActionVisibilityContext { runningDevServers, hasGitRepos: repos.length > 0, hasMultipleRepos: repos.length > 1, + hasOpenPR, + hasUnpushedCommits, }; }, [ layout.isChangesMode, @@ -81,6 +97,7 @@ export function useActionVisibilityContext(): ActionVisibilityContext { isStarting, isStopping, runningDevServers, + branchStatus, ]); } diff --git a/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx b/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx index d77c2518..99640d98 100644 --- a/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx +++ b/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx @@ -27,7 +27,10 @@ import { NavbarContainer } from '@/components/ui-new/containers/NavbarContainer' import { PreviewBrowserContainer } from '@/components/ui-new/containers/PreviewBrowserContainer'; import { PreviewControlsContainer } from '@/components/ui-new/containers/PreviewControlsContainer'; import { useRenameBranch } from '@/hooks/useRenameBranch'; +import { usePush } from '@/hooks/usePush'; import { repoApi } from '@/lib/api'; +import { ConfirmDialog } from '@/components/ui-new/dialogs/ConfirmDialog'; +import { ForcePushDialog } from '@/components/dialogs/git/ForcePushDialog'; import { useDiffStream } from '@/hooks/useDiffStream'; import { useTask } from '@/hooks/useTask'; import { useAttemptRepo } from '@/hooks/useAttemptRepo'; @@ -53,6 +56,8 @@ interface GitPanelContainerProps { onBranchNameChange: (name: string) => void; } +type PushState = 'idle' | 'pending' | 'success' | 'error'; + function GitPanelContainer({ selectedWorkspace, repos, @@ -61,6 +66,101 @@ function GitPanelContainer({ }: GitPanelContainerProps) { const { executeAction } = useActions(); + // Track push state per repo: idle, pending, success, or error + const [pushStates, setPushStates] = useState>({}); + const pushStatesRef = useRef>({}); + pushStatesRef.current = pushStates; + const successTimeoutRef = useRef | null>(null); + const currentPushRepoRef = useRef(null); + + // Reset push-related state when the selected workspace changes to avoid + // leaking push state across workspaces with repos that share the same ID. + useEffect(() => { + setPushStates({}); + pushStatesRef.current = {}; + currentPushRepoRef.current = null; + + if (successTimeoutRef.current) { + clearTimeout(successTimeoutRef.current); + successTimeoutRef.current = null; + } + }, [selectedWorkspace?.id]); + // Use push hook for direct API access with proper error handling + const pushMutation = usePush( + selectedWorkspace?.id, + // onSuccess + () => { + const repoId = currentPushRepoRef.current; + if (!repoId) return; + setPushStates((prev) => ({ ...prev, [repoId]: 'success' })); + // Clear success state after 2 seconds + successTimeoutRef.current = setTimeout(() => { + setPushStates((prev) => ({ ...prev, [repoId]: 'idle' })); + }, 2000); + }, + // onError + async (err, errorData) => { + const repoId = currentPushRepoRef.current; + if (!repoId) return; + + // Handle force push required - show confirmation dialog + if (errorData?.type === 'force_push_required' && selectedWorkspace?.id) { + setPushStates((prev) => ({ ...prev, [repoId]: 'idle' })); + await ForcePushDialog.show({ + attemptId: selectedWorkspace.id, + repoId, + }); + return; + } + + // Show error state and dialog for other errors + setPushStates((prev) => ({ ...prev, [repoId]: 'error' })); + const message = + err instanceof Error ? err.message : 'Failed to push changes'; + ConfirmDialog.show({ + title: 'Error', + message, + confirmText: 'OK', + showCancelButton: false, + variant: 'destructive', + }); + // Clear error state after 3 seconds + successTimeoutRef.current = setTimeout(() => { + setPushStates((prev) => ({ ...prev, [repoId]: 'idle' })); + }, 3000); + } + ); + + // Clean up timeout on unmount + useEffect(() => { + return () => { + if (successTimeoutRef.current) { + clearTimeout(successTimeoutRef.current); + } + }; + }, []); + + // Compute repoInfos with push button state + const repoInfosWithPushButton = useMemo( + () => + repoInfos.map((repo) => { + const state = pushStates[repo.id] ?? 'idle'; + const hasUnpushedCommits = + repo.prStatus === 'open' && (repo.remoteCommitsAhead ?? 0) > 0; + // Show push button if there are unpushed commits OR if we're in a push flow + // (pending/success/error states keep the button visible for feedback) + const isInPushFlow = state !== 'idle'; + return { + ...repo, + showPushButton: hasUnpushedCommits && !isInPushFlow, + isPushPending: state === 'pending', + isPushSuccess: state === 'success', + isPushError: state === 'error', + }; + }), + [repoInfos, pushStates] + ); + // Handle copying repo path to clipboard const handleCopyPath = useCallback( (repoId: string) => { @@ -100,6 +200,7 @@ function GitPanelContainer({ merge: Actions.GitMerge, rebase: Actions.GitRebase, 'change-target': Actions.GitChangeTarget, + push: Actions.GitPush, }; const actionDef = actionMap[action]; @@ -111,12 +212,33 @@ function GitPanelContainer({ [selectedWorkspace, executeAction] ); + // Handle push button click - use mutation for proper state tracking + const handlePushClick = useCallback( + (repoId: string) => { + // Use ref to check current state to avoid stale closure + if (pushStatesRef.current[repoId] === 'pending') return; + + // Clear any existing timeout + if (successTimeoutRef.current) { + clearTimeout(successTimeoutRef.current); + successTimeoutRef.current = null; + } + + // Track which repo we're pushing + currentPushRepoRef.current = repoId; + setPushStates((prev) => ({ ...prev, [repoId]: 'pending' })); + pushMutation.mutate({ repo_id: repoId }); + }, + [pushMutation] + ); + return ( console.log('Add repo clicked')} @@ -256,6 +378,7 @@ export function WorkspacesLayout() { name: repo.display_name || repo.name, targetBranch: repo.target_branch || 'main', commitsAhead: repoStatus?.commits_ahead ?? 0, + remoteCommitsAhead: repoStatus?.remote_commits_ahead ?? 0, filesChanged: diffStats.filesChanged, linesAdded: diffStats.linesAdded, linesRemoved: diffStats.linesRemoved, diff --git a/frontend/src/components/ui-new/primitives/RepoCard.tsx b/frontend/src/components/ui-new/primitives/RepoCard.tsx index b657dee2..c360f624 100644 --- a/frontend/src/components/ui-new/primitives/RepoCard.tsx +++ b/frontend/src/components/ui-new/primitives/RepoCard.tsx @@ -11,6 +11,8 @@ import { CopyIcon, GitMergeIcon, CheckCircleIcon, + SpinnerGapIcon, + WarningCircleIcon, } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import { @@ -24,7 +26,12 @@ import { CollapsibleSection } from './CollapsibleSection'; import { SplitButton, type SplitButtonOption } from './SplitButton'; import { useRepoAction, PERSIST_KEYS } from '@/stores/useUiPreferencesStore'; -export type RepoAction = 'pull-request' | 'merge' | 'change-target' | 'rebase'; +export type RepoAction = + | 'pull-request' + | 'merge' + | 'change-target' + | 'rebase' + | 'push'; const repoActionOptions: SplitButtonOption[] = [ { @@ -46,10 +53,15 @@ interface RepoCardProps { prNumber?: number; prUrl?: string; prStatus?: 'open' | 'merged' | 'closed' | 'unknown'; + showPushButton?: boolean; + isPushPending?: boolean; + isPushSuccess?: boolean; + isPushError?: boolean; branchDropdownContent?: React.ReactNode; onChangeTarget?: () => void; onRebase?: () => void; onActionsClick?: (action: RepoAction) => void; + onPushClick?: () => void; onOpenInEditor?: () => void; onCopyPath?: () => void; } @@ -65,10 +77,15 @@ export function RepoCard({ prNumber, prUrl, prStatus, + showPushButton = false, + isPushPending = false, + isPushSuccess = false, + isPushError = false, branchDropdownContent, onChangeTarget, onRebase, onActionsClick, + onPushClick, onOpenInEditor, onCopyPath, }: RepoCardProps) { @@ -205,6 +222,40 @@ export function RepoCard({ {t('git.pr.open', { number: prNumber })} )} + {/* Push button - shows loading/success/error state */} + {(showPushButton || + isPushPending || + isPushSuccess || + isPushError) && ( + + )} )} diff --git a/frontend/src/components/ui-new/views/GitPanel.tsx b/frontend/src/components/ui-new/views/GitPanel.tsx index d10bac98..4b9812c8 100644 --- a/frontend/src/components/ui-new/views/GitPanel.tsx +++ b/frontend/src/components/ui-new/views/GitPanel.tsx @@ -16,12 +16,17 @@ export interface RepoInfo { name: string; targetBranch: string; commitsAhead: number; + remoteCommitsAhead?: number; filesChanged: number; linesAdded: number; linesRemoved: number; prNumber?: number; prUrl?: string; prStatus?: 'open' | 'merged' | 'closed' | 'unknown'; + showPushButton?: boolean; + isPushPending?: boolean; + isPushSuccess?: boolean; + isPushError?: boolean; } interface GitPanelProps { @@ -29,6 +34,7 @@ interface GitPanelProps { workingBranchName: string; onWorkingBranchNameChange: (name: string) => void; onActionsClick?: (repoId: string, action: RepoAction) => void; + onPushClick?: (repoId: string) => void; onOpenInEditor?: (repoId: string) => void; onCopyPath?: (repoId: string) => void; onAddRepo?: () => void; @@ -41,6 +47,7 @@ export function GitPanel({ workingBranchName, onWorkingBranchNameChange, onActionsClick, + onPushClick, onOpenInEditor, onCopyPath, className, @@ -75,9 +82,14 @@ export function GitPanel({ prNumber={repo.prNumber} prUrl={repo.prUrl} prStatus={repo.prStatus} + showPushButton={repo.showPushButton} + isPushPending={repo.isPushPending} + isPushSuccess={repo.isPushSuccess} + isPushError={repo.isPushError} onChangeTarget={() => onActionsClick?.(repo.id, 'change-target')} onRebase={() => onActionsClick?.(repo.id, 'rebase')} onActionsClick={(action) => onActionsClick?.(repo.id, action)} + onPushClick={() => onPushClick?.(repo.id)} onOpenInEditor={() => onOpenInEditor?.(repo.id)} onCopyPath={() => onCopyPath?.(repo.id)} /> diff --git a/frontend/src/i18n/locales/en/tasks.json b/frontend/src/i18n/locales/en/tasks.json index f35b550b..73c2679f 100644 --- a/frontend/src/i18n/locales/en/tasks.json +++ b/frontend/src/i18n/locales/en/tasks.json @@ -352,6 +352,7 @@ "pushed": "Pushed!", "pushing": "Pushing...", "push": "Push", + "pushFailed": "Failed", "forcePush": "Force Push", "forcePushing": "Force Pushing...", "creating": "Creating...", diff --git a/frontend/src/i18n/locales/es/tasks.json b/frontend/src/i18n/locales/es/tasks.json index 30d62623..2087a854 100644 --- a/frontend/src/i18n/locales/es/tasks.json +++ b/frontend/src/i18n/locales/es/tasks.json @@ -309,6 +309,7 @@ "push": "Enviar", "pushed": "¡Enviado!", "pushing": "Enviando...", + "pushFailed": "Falló", "rebase": "Rebase", "rebasing": "Rebaseando..." }, diff --git a/frontend/src/i18n/locales/ja/tasks.json b/frontend/src/i18n/locales/ja/tasks.json index 8b0eb401..c0b0d133 100644 --- a/frontend/src/i18n/locales/ja/tasks.json +++ b/frontend/src/i18n/locales/ja/tasks.json @@ -307,6 +307,7 @@ "push": "プッシュ", "pushed": "プッシュ完了!", "pushing": "プッシュ中...", + "pushFailed": "失敗", "forcePush": "強制プッシュ", "forcePushing": "強制プッシュ中...", "rebase": "リベース", diff --git a/frontend/src/i18n/locales/ko/tasks.json b/frontend/src/i18n/locales/ko/tasks.json index bb81da1b..4f7cd622 100644 --- a/frontend/src/i18n/locales/ko/tasks.json +++ b/frontend/src/i18n/locales/ko/tasks.json @@ -263,6 +263,7 @@ "pushed": "푸시됨!", "pushing": "푸시 중...", "push": "푸시", + "pushFailed": "실패", "forcePush": "강제 푸시", "forcePushing": "강제 푸시 중...", "creating": "생성 중...", diff --git a/frontend/src/i18n/locales/zh-Hans/tasks.json b/frontend/src/i18n/locales/zh-Hans/tasks.json index 9d47627d..dd63d9fd 100644 --- a/frontend/src/i18n/locales/zh-Hans/tasks.json +++ b/frontend/src/i18n/locales/zh-Hans/tasks.json @@ -298,6 +298,7 @@ "pushed": "已推送!", "pushing": "推送中...", "push": "推送", + "pushFailed": "失败", "forcePush": "强制推送", "forcePushing": "强制推送中...", "creating": "创建中...", diff --git a/frontend/src/i18n/locales/zh-Hant/tasks.json b/frontend/src/i18n/locales/zh-Hant/tasks.json index d20a956f..5f14612b 100644 --- a/frontend/src/i18n/locales/zh-Hant/tasks.json +++ b/frontend/src/i18n/locales/zh-Hant/tasks.json @@ -298,6 +298,7 @@ "pushed": "已推送!", "pushing": "推送中...", "push": "推送", + "pushFailed": "失敗", "forcePush": "強制推送", "forcePushing": "強制推送中...", "creating": "建立中...",