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<ReturnType<typeof setTimeout> | 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>
This commit is contained in:
Anastasiia Solop
2026-01-12 15:13:57 +01:00
committed by GitHub
parent db2df1540c
commit eff14ef094
12 changed files with 240 additions and 3 deletions

View File

@@ -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,
]);
}