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

@@ -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<string, ActionDefinition>;
// Helper to resolve dynamic label

View File

@@ -166,6 +166,7 @@ export const Pages: Record<StaticPageId, CommandBarPage> = {
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 },
],

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

View File

@@ -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<Record<string, PushState>>({});
const pushStatesRef = useRef<Record<string, PushState>>({});
pushStatesRef.current = pushStates;
const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const currentPushRepoRef = useRef<string | null>(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 (
<GitPanel
repos={repoInfos}
repos={repoInfosWithPushButton}
workingBranchName={selectedWorkspace?.branch ?? ''}
onWorkingBranchNameChange={onBranchNameChange}
onActionsClick={handleActionsClick}
onPushClick={handlePushClick}
onOpenInEditor={handleOpenInEditor}
onCopyPath={handleCopyPath}
onAddRepo={() => 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,

View File

@@ -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<RepoAction>[] = [
{
@@ -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 })}
</span>
)}
{/* Push button - shows loading/success/error state */}
{(showPushButton ||
isPushPending ||
isPushSuccess ||
isPushError) && (
<button
onClick={onPushClick}
disabled={isPushPending || isPushSuccess || isPushError}
className={`inline-flex items-center gap-half px-base py-half rounded-sm text-sm font-medium transition-colors disabled:cursor-not-allowed ${
isPushSuccess
? 'bg-success/20 text-success'
: isPushError
? 'bg-error/20 text-error'
: 'bg-panel text-normal hover:bg-tertiary disabled:opacity-50'
}`}
>
{isPushPending ? (
<SpinnerGapIcon className="size-icon-xs animate-spin" />
) : isPushSuccess ? (
<CheckCircleIcon className="size-icon-xs" weight="fill" />
) : isPushError ? (
<WarningCircleIcon className="size-icon-xs" weight="fill" />
) : (
<ArrowUpIcon className="size-icon-xs" weight="bold" />
)}
{isPushPending
? t('git.states.pushing')
: isPushSuccess
? t('git.states.pushed')
: isPushError
? t('git.states.pushFailed')
: t('git.states.push')}
</button>
)}
</div>
)}

View File

@@ -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)}
/>

View File

@@ -352,6 +352,7 @@
"pushed": "Pushed!",
"pushing": "Pushing...",
"push": "Push",
"pushFailed": "Failed",
"forcePush": "Force Push",
"forcePushing": "Force Pushing...",
"creating": "Creating...",

View File

@@ -309,6 +309,7 @@
"push": "Enviar",
"pushed": "¡Enviado!",
"pushing": "Enviando...",
"pushFailed": "Falló",
"rebase": "Rebase",
"rebasing": "Rebaseando..."
},

View File

@@ -307,6 +307,7 @@
"push": "プッシュ",
"pushed": "プッシュ完了!",
"pushing": "プッシュ中...",
"pushFailed": "失敗",
"forcePush": "強制プッシュ",
"forcePushing": "強制プッシュ中...",
"rebase": "リベース",

View File

@@ -263,6 +263,7 @@
"pushed": "푸시됨!",
"pushing": "푸시 중...",
"push": "푸시",
"pushFailed": "실패",
"forcePush": "강제 푸시",
"forcePushing": "강제 푸시 중...",
"creating": "생성 중...",

View File

@@ -298,6 +298,7 @@
"pushed": "已推送!",
"pushing": "推送中...",
"push": "推送",
"pushFailed": "失败",
"forcePush": "强制推送",
"forcePushing": "强制推送中...",
"creating": "创建中...",

View File

@@ -298,6 +298,7 @@
"pushed": "已推送!",
"pushing": "推送中...",
"push": "推送",
"pushFailed": "失敗",
"forcePush": "強制推送",
"forcePushing": "強制推送中...",
"creating": "建立中...",