Persist base branch for Paseo worktrees and gate merge action

This commit is contained in:
Mohamed Boudra
2026-01-23 15:27:42 +07:00
parent 23cd462336
commit eab8713c24
15 changed files with 565 additions and 79 deletions
+18 -2
View File
@@ -416,9 +416,12 @@ export default function HomeScreen() {
]);
const baseBranchError = useMemo(() => {
if (!isCreateWorktree || isNonGitDirectory || !baseBranch) {
if (!isCreateWorktree || isNonGitDirectory) {
return null;
}
if (!baseBranch) {
return "Base branch is required";
}
const branches = repoInfo?.branches ?? [];
if (branches.length === 0) {
return null;
@@ -442,6 +445,19 @@ export default function HomeScreen() {
[setWorkingDirFromUser]
);
useEffect(() => {
if (!isCreateWorktree || isNonGitDirectory) {
return;
}
if (baseBranch) {
return;
}
const current = repoInfo?.currentBranch?.trim();
if (current) {
setBaseBranch(current);
}
}, [isCreateWorktree, isNonGitDirectory, baseBranch, repoInfo?.currentBranch]);
useEffect(() => {
if (isNonGitDirectory && worktreeMode !== "none") {
setWorktreeMode("none");
@@ -514,7 +530,7 @@ export default function HomeScreen() {
...(modeId ? { modeId } : {}),
...(trimmedModel ? { model: trimmedModel } : {}),
};
const effectiveBaseBranch = baseBranch.trim() || repoInfo?.currentBranch || undefined;
const effectiveBaseBranch = baseBranch.trim();
const effectiveWorktreeSlug =
isCreateWorktree && !worktreeSlug ? createNameId() : worktreeSlug;
if (isCreateWorktree && !worktreeSlug && effectiveWorktreeSlug) {
+37 -18
View File
@@ -356,12 +356,13 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
const [actionStatus, setActionStatus] = useState<string | null>(null);
const { status, isLoading: isStatusLoading, isFetching: isStatusFetching, isError: isStatusError, error: statusError, refresh: refreshStatus } =
useCheckoutStatusQuery({ serverId, agentId });
const isGit = status?.isGit ?? false;
const notGit = Boolean(status) && !status?.isGit && !status?.error;
const gitStatus = status && status.isGit ? status : null;
const isGit = Boolean(gitStatus);
const notGit = status !== null && !status.isGit && !status.error;
const statusErrorMessage =
status?.error?.message ??
(isStatusError && statusError instanceof Error ? statusError.message : null);
const baseRef = status?.baseRef ?? undefined;
const baseRef = gitStatus?.baseRef ?? undefined;
const {
files,
payloadError: diffPayloadError,
@@ -593,17 +594,22 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
diffPayloadError?.message ??
(isDiffError && diffError instanceof Error ? diffError.message : null);
const prErrorMessage = prPayloadError?.message ?? null;
const branchLabel = status?.currentBranch ?? (notGit ? "Not a git repository" : "Unknown");
const branchLabel =
gitStatus?.currentBranch ?? (notGit ? "Not a git repository" : "Unknown");
const actionsDisabled = !isGit || Boolean(status?.error) || isStatusLoading;
const hasUncommittedChanges = Boolean(status?.isDirty);
const hasUncommittedChanges = Boolean(gitStatus?.isDirty);
const showCommitAction =
!isGit || hasUncommittedChanges || (diffMode === "uncommitted" && hasChanges);
const showCreatePrAction = !isGit || !prStatus;
const showMergeAction =
diffMode === "base" && isGit && (gitStatus?.aheadBehind?.ahead ?? 0) > 0;
const commitDisabled = actionsDisabled || commitMutation.isPending;
const prDisabled = actionsDisabled || prMutation.isPending;
const mergeDisabled = actionsDisabled || mergeMutation.isPending;
const archiveDisabled =
actionsDisabled || archiveMutation.isPending || !status?.isPaseoOwnedWorktree;
actionsDisabled ||
archiveMutation.isPending ||
!gitStatus?.isPaseoOwnedWorktree;
let bodyContent: ReactElement;
@@ -745,18 +751,20 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
)}
</Pressable>
) : null}
<Pressable
testID="changes-action-merge"
style={[styles.actionButton, mergeDisabled && styles.actionButtonDisabled]}
onPress={() => mergeMutation.mutate()}
disabled={mergeDisabled}
>
{mergeMutation.isPending ? (
<ActivityIndicator size="small" color={theme.colors.foreground} />
) : (
<Text style={styles.actionButtonText}>Merge to base</Text>
)}
</Pressable>
{showMergeAction ? (
<Pressable
testID="changes-action-merge"
style={[styles.actionButton, mergeDisabled && styles.actionButtonDisabled]}
onPress={() => mergeMutation.mutate()}
disabled={mergeDisabled}
>
{mergeMutation.isPending ? (
<ActivityIndicator size="small" color={theme.colors.foreground} />
) : (
<Text style={styles.actionButtonText}>Merge to base</Text>
)}
</Pressable>
) : null}
<Pressable
testID="changes-action-archive"
style={[styles.actionButton, archiveDisabled && styles.actionButtonDisabled]}
@@ -771,6 +779,11 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
</Pressable>
</View>
<Text style={styles.debugText} testID="changes-debug-state">
mode={diffMode} files={files.length} isGit={String(isGit)} ahead=
{gitStatus?.aheadBehind ? String(gitStatus.aheadBehind.ahead) : "null"}
</Text>
{actionStatus ? <Text style={styles.actionStatusText}>{actionStatus}</Text> : null}
{actionError ? <Text style={styles.actionErrorText}>{actionError}</Text> : null}
{prStatus ? (
@@ -866,6 +879,12 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground,
fontWeight: theme.fontWeight.medium,
},
debugText: {
paddingHorizontal: theme.spacing[3],
paddingBottom: theme.spacing[1],
fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted,
},
actionStatusText: {
paddingHorizontal: theme.spacing[3],
paddingBottom: theme.spacing[1],
@@ -1,5 +1,8 @@
import { useQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import { UnistylesRuntime } from "react-native-unistyles";
import { useSessionStore } from "@/stores/session-store";
import { usePanelStore } from "@/stores/panel-store";
import type { CheckoutStatusResponse } from "@server/shared/messages";
const CHECKOUT_STATUS_STALE_TIME = 15_000;
@@ -22,6 +25,12 @@ export function useCheckoutStatusQuery({ serverId, agentId }: UseCheckoutStatusQ
const isConnected = useSessionStore(
(state) => state.sessions[serverId]?.connection.isConnected ?? false
);
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const explorerTab = usePanelStore((state) => state.explorerTab);
const isOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
const query = useQuery({
queryKey: checkoutStatusQueryKey(serverId, agentId),
@@ -34,8 +43,18 @@ export function useCheckoutStatusQuery({ serverId, agentId }: UseCheckoutStatusQ
enabled: !!client && isConnected && !!agentId,
staleTime: CHECKOUT_STATUS_STALE_TIME,
refetchInterval: 10_000,
refetchIntervalInBackground: true,
refetchOnMount: "always",
});
// Revalidate when sidebar is open with "changes" tab active.
useEffect(() => {
if (!isOpen || explorerTab !== "changes" || !agentId) {
return;
}
void query.refetch();
}, [isOpen, explorerTab, agentId, query]);
return {
status: query.data ?? null,
isLoading: query.isLoading,