@@ -11,6 +11,7 @@ import { useIsMutating, useMutation, useQuery, useQueryClient } from "@tanstack/
1111import { Schema } from "effect" ;
1212import { useCallback , useEffect , useEffectEvent , useMemo , useRef , useState } from "react" ;
1313import {
14+ ArrowUpDownIcon ,
1415 ChevronDownIcon ,
1516 CircleAlertIcon ,
1617 CloudUploadIcon ,
@@ -28,8 +29,9 @@ import {
2829 type DefaultBranchConfirmableAction ,
2930 requiresDefaultBranchConfirmation ,
3031 resolveDefaultBranchActionDialogCopy ,
31- resolveQuickAction ,
3232 resolveGitFailureRetryLabel ,
33+ resolveQuickAction ,
34+ resolveSyncAction ,
3335 summarizeGitFailure ,
3436 summarizeGitResult ,
3537} from "./GitActionsControl.logic" ;
@@ -279,6 +281,10 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {
279281 return < InfoIcon className = { iconClassName } /> ;
280282}
281283
284+ function GitSyncActionIcon ( ) {
285+ return < ArrowUpDownIcon className = "size-3.5" /> ;
286+ }
287+
282288export default function GitActionsControl ( { gitCwd, activeThreadId } : GitActionsControlProps ) {
283289 const { settings } = useAppSettings ( ) ;
284290 const threadToastData = useMemo (
@@ -363,9 +369,16 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
363369 resolveQuickAction ( gitStatusForActions , isGitActionRunning , isDefaultBranch , hasOriginRemote ) ,
364370 [ gitStatusForActions , hasOriginRemote , isDefaultBranch , isGitActionRunning ] ,
365371 ) ;
372+ const syncAction = useMemo (
373+ ( ) => resolveSyncAction ( gitStatusForActions , isGitActionRunning ) ,
374+ [ gitStatusForActions , isGitActionRunning ] ,
375+ ) ;
366376 const quickActionDisabledReason = quickAction . disabled
367377 ? ( quickAction . hint ?? "This action is currently unavailable." )
368378 : null ;
379+ const syncActionDisabledReason = syncAction ?. disabled
380+ ? ( syncAction . hint ?? "This action is currently unavailable." )
381+ : null ;
369382 const pendingDefaultBranchActionCopy = pendingDefaultBranchAction
370383 ? resolveDefaultBranchActionDialogCopy ( {
371384 action : pendingDefaultBranchAction . action ,
@@ -838,30 +851,63 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
838851 void openPromise . catch ( ( ) => undefined ) ;
839852 } , [ conflictedFiles , gitCwd , threadToastData ] ) ;
840853
841- const runQuickAction = useCallback ( ( ) => {
842- if ( quickAction . kind === "open_pr" ) {
843- void openExistingPr ( ) ;
844- return ;
845- }
846- if ( quickAction . kind === "run_pull" ) {
854+ const runPullWithToast = useCallback (
855+ ( messages ?: {
856+ loadingTitle ?: string ;
857+ pulledTitle ?: string ;
858+ skippedTitle ?: string ;
859+ errorTitle ?: string ;
860+ } ) => {
847861 const promise = pullMutation . mutateAsync ( ) ;
848862 toastManager . promise ( promise , {
849- loading : { title : "Pulling..." , data : threadToastData } ,
863+ loading : { title : messages ?. loadingTitle ?? "Pulling..." , data : threadToastData } ,
850864 success : ( result ) => ( {
851- title : result . status === "pulled" ? "Pulled" : "Already up to date" ,
865+ title :
866+ result . status === "pulled"
867+ ? ( messages ?. pulledTitle ?? "Pulled" )
868+ : ( messages ?. skippedTitle ?? "Already up to date" ) ,
852869 description :
853870 result . status === "pulled"
854871 ? `Updated ${ result . branch } from ${ result . upstreamBranch ?? "upstream" } `
855872 : `${ result . branch } is already synchronized.` ,
856873 data : threadToastData ,
857874 } ) ,
858875 error : ( err ) => ( {
859- title : "Pull failed" ,
876+ title : messages ?. errorTitle ?? "Pull failed" ,
860877 description : err instanceof Error ? err . message : "An error occurred." ,
861878 data : threadToastData ,
862879 } ) ,
863880 } ) ;
864881 void promise . catch ( ( ) => undefined ) ;
882+ } ,
883+ [ pullMutation , threadToastData ] ,
884+ ) ;
885+
886+ const runSyncAction = useCallback ( ( ) => {
887+ if ( ! syncAction || syncAction . disabled ) {
888+ return ;
889+ }
890+ if ( syncAction . kind === "run_pull" ) {
891+ runPullWithToast ( {
892+ loadingTitle : "Syncing..." ,
893+ pulledTitle : "Synced branch" ,
894+ skippedTitle : "Already up to date" ,
895+ errorTitle : "Sync failed" ,
896+ } ) ;
897+ return ;
898+ }
899+ if ( syncAction . kind === "run_action" && syncAction . action === "commit_push" ) {
900+ void runGitActionWithToast ( { action : "commit_push" , forcePushOnlyProgress : true } ) ;
901+ }
902+ } , [ runPullWithToast , syncAction ] ) ;
903+
904+ const runQuickAction = useCallback ( ( ) => {
905+ if ( quickAction . kind === "open_pr" ) {
906+ void openExistingPr ( ) ;
907+ return ;
908+ }
909+ if ( quickAction . kind === "run_pull" ) {
910+ runPullWithToast ( ) ;
865911 return ;
866912 }
867913 if ( quickAction . kind === "resolve_conflicts" ) {
@@ -880,7 +926,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
880926 if ( quickAction . action ) {
881927 void runGitActionWithToast ( { action : quickAction . action } ) ;
882928 }
883- } , [ openConflictedFilesInEditor , openExistingPr , pullMutation , quickAction , threadToastData ] ) ;
929+ } , [ openConflictedFilesInEditor , openExistingPr , quickAction , runPullWithToast , threadToastData ] ) ;
884930
885931 const openDialogForMenuItem = useCallback (
886932 ( item : GitActionMenuItem ) => {
@@ -964,6 +1010,41 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
9641010 </ Button >
9651011 ) : (
9661012 < Group aria-label = "Git actions" >
1013+ { syncAction ? (
1014+ < >
1015+ { syncActionDisabledReason ? (
1016+ < Popover >
1017+ < PopoverTrigger
1018+ openOnHover
1019+ render = {
1020+ < Button
1021+ aria-disabled = "true"
1022+ className = "cursor-not-allowed opacity-64"
1023+ size = "icon-xs"
1024+ variant = "outline"
1025+ />
1026+ }
1027+ >
1028+ < GitSyncActionIcon />
1029+ </ PopoverTrigger >
1030+ < PopoverPopup tooltipStyle side = "bottom" align = "start" >
1031+ { syncActionDisabledReason }
1032+ </ PopoverPopup >
1033+ </ Popover >
1034+ ) : (
1035+ < Button
1036+ aria-label = { syncAction . label }
1037+ title = { syncAction . label }
1038+ size = "icon-xs"
1039+ variant = "outline"
1040+ onClick = { runSyncAction }
1041+ >
1042+ < GitSyncActionIcon />
1043+ </ Button >
1044+ ) }
1045+ < GroupSeparator className = "hidden @sm/header-actions:block" />
1046+ </ >
1047+ ) : null }
9671048 { quickActionDisabledReason ? (
9681049 < Popover >
9691050 < PopoverTrigger
0 commit comments