From 52d935fd6dade1db44aa47161ee81ba577337b96 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 1 Apr 2026 04:22:55 -0500 Subject: [PATCH] Unify git action flows and reduce sticky thread errors --- apps/web/src/components/GitActionsControl.tsx | 187 +++++++++++++----- apps/web/src/components/Sidebar.logic.test.ts | 24 ++- apps/web/src/components/Sidebar.logic.ts | 2 +- apps/web/src/store.ts | 2 +- 4 files changed, 164 insertions(+), 51 deletions(-) diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 4aa38ec1e..e9e0d8ac6 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -141,6 +141,8 @@ interface GitActionFailureDialogState { retryInput: RetryableGitActionInput; } +type GitDialogAction = GitStackedAction; + const isGitActionFailure = Schema.is(GitActionFailureSchema); function toRetryableGitActionInput(input: RunGitActionWithToastInput): RetryableGitActionInput { @@ -262,9 +264,62 @@ function getMenuActionDisabledReason({ return "Create PR is currently unavailable."; } -const COMMIT_DIALOG_TITLE = "Commit changes"; -const COMMIT_DIALOG_DESCRIPTION = - "Review and confirm your commit. Leave the message blank to auto-generate one."; +function dialogIncludesCommit(action: GitDialogAction | null, gitStatus: GitStatusResult | null): boolean { + if (!action) return false; + return action === "commit" || !!gitStatus?.hasWorkingTreeChanges; +} + +function resolveDialogCopy(input: { + action: GitDialogAction | null; + includesCommit: boolean; +}): { + title: string; + description: string; + confirmLabel: string; + newBranchLabel: string; +} { + if (input.action === "commit_push_pr") { + if (input.includesCommit) { + return { + title: "Commit, push, and create PR", + description: + "Review the commit details, then continue through the full publish flow with PR creation.", + confirmLabel: "Commit, push & create PR", + newBranchLabel: "Use new branch instead", + }; + } + return { + title: "Create pull request", + description: "Push local commits if needed, then create a pull request for this branch.", + confirmLabel: "Push & create PR", + newBranchLabel: "Use new branch instead", + }; + } + + if (input.action === "commit_push") { + if (input.includesCommit) { + return { + title: "Commit and push changes", + description: "Review the commit details, then publish this branch.", + confirmLabel: "Commit & push", + newBranchLabel: "Use new branch instead", + }; + } + return { + title: "Push branch", + description: "Push local commits on this branch.", + confirmLabel: "Push", + newBranchLabel: "Use new branch instead", + }; + } + + return { + title: "Commit changes", + description: "Review and confirm your commit. Leave the message blank to auto-generate one.", + confirmLabel: "Commit", + newBranchLabel: "Commit on new branch", + }; +} function GitActionItemIcon({ icon }: { icon: GitActionIconName }) { if (icon === "commit") return ; @@ -299,7 +354,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions [activeThreadId], ); const queryClient = useQueryClient(); - const [isCommitDialogOpen, setIsCommitDialogOpen] = useState(false); + const [activeDialogAction, setActiveDialogAction] = useState(null); const [dialogCommitMessage, setDialogCommitMessage] = useState(""); const [excludedFiles, setExcludedFiles] = useState>(new Set()); const [isEditingFiles, setIsEditingFiles] = useState(false); @@ -345,6 +400,11 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions const selectedFiles = allFiles.filter((f) => !excludedFiles.has(f.path)); const allSelected = excludedFiles.size === 0; const noneSelected = selectedFiles.length === 0; + const activeDialogIncludesCommit = dialogIncludesCommit(activeDialogAction, gitStatusForActions); + const activeDialogCopy = resolveDialogCopy({ + action: activeDialogAction, + includesCommit: activeDialogIncludesCommit, + }); const initMutation = useMutation(gitInitMutationOptions({ cwd: gitCwd, queryClient })); @@ -824,22 +884,28 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions }, [pendingDefaultBranchAction]); const runDialogActionOnNewBranch = useCallback(() => { - if (!isCommitDialogOpen) return; + if (!activeDialogAction || !activeDialogIncludesCommit) return; const commitMessage = dialogCommitMessage.trim(); - setIsCommitDialogOpen(false); + setActiveDialogAction(null); setDialogCommitMessage(""); setExcludedFiles(new Set()); setIsEditingFiles(false); void runGitActionWithToast({ - action: "commit", + action: activeDialogAction, ...(commitMessage ? { commitMessage } : {}), ...(!allSelected ? { filePaths: selectedFiles.map((f) => f.path) } : {}), featureBranch: true, skipDefaultBranchPrompt: true, }); - }, [allSelected, isCommitDialogOpen, dialogCommitMessage, selectedFiles]); + }, [ + activeDialogAction, + activeDialogIncludesCommit, + allSelected, + dialogCommitMessage, + selectedFiles, + ]); const conflictedFiles = useMemo( () => gitStatusForActions?.conflictedFiles ?? [], @@ -1002,9 +1068,18 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions return; } if (quickAction.action) { - void runGitActionWithToast({ action: quickAction.action }); + setDialogCommitMessage(""); + setExcludedFiles(new Set()); + setIsEditingFiles(false); + setActiveDialogAction(quickAction.action); } - }, [openConflictedFilesInEditor, openExistingPr, quickAction, runPullWithToast, threadToastData]); + }, [ + openConflictedFilesInEditor, + openExistingPr, + quickAction, + runPullWithToast, + threadToastData, + ]); const openDialogForMenuItem = useCallback( (item: GitActionMenuItem) => { @@ -1013,40 +1088,48 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions void openExistingPr(); return; } + setDialogCommitMessage(""); + setExcludedFiles(new Set()); + setIsEditingFiles(false); if (item.dialogAction === "push") { - void runGitActionWithToast({ action: "commit_push", forcePushOnlyProgress: true }); + setActiveDialogAction("commit_push"); return; } if (item.dialogAction === "create_pr") { - void runGitActionWithToast({ action: "commit_push_pr" }); + setActiveDialogAction("commit_push_pr"); return; } - setExcludedFiles(new Set()); - setIsEditingFiles(false); - setIsCommitDialogOpen(true); + setActiveDialogAction("commit"); }, - [openExistingPr, setIsCommitDialogOpen], + [openExistingPr], ); const runDialogAction = useCallback(() => { - if (!isCommitDialogOpen) return; + if (!activeDialogAction) return; const commitMessage = dialogCommitMessage.trim(); - setIsCommitDialogOpen(false); + const includesCommit = dialogIncludesCommit(activeDialogAction, gitStatusForActions); + setActiveDialogAction(null); setDialogCommitMessage(""); setExcludedFiles(new Set()); setIsEditingFiles(false); void runGitActionWithToast({ - action: "commit", - ...(commitMessage ? { commitMessage } : {}), - ...(!allSelected ? { filePaths: selectedFiles.map((f) => f.path) } : {}), + action: activeDialogAction, + ...(includesCommit && commitMessage ? { commitMessage } : {}), + ...(includesCommit + ? !allSelected + ? { filePaths: selectedFiles.map((f) => f.path) } + : {} + : activeDialogAction !== "commit" + ? { forcePushOnlyProgress: true } + : {}), }); }, [ + activeDialogAction, allSelected, dialogCommitMessage, - isCommitDialogOpen, + gitStatusForActions, selectedFiles, setDialogCommitMessage, - setIsCommitDialogOpen, ]); const openChangedFileInEditor = useCallback( @@ -1309,10 +1392,10 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions )} { if (!open) { - setIsCommitDialogOpen(false); + setActiveDialogAction(null); setDialogCommitMessage(""); setExcludedFiles(new Set()); setIsEditingFiles(false); @@ -1321,8 +1404,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions > - {COMMIT_DIALOG_TITLE} - {COMMIT_DIALOG_DESCRIPTION} + {activeDialogCopy.title} + {activeDialogCopy.description}
@@ -1340,7 +1423,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
- {isEditingFiles && allFiles.length > 0 && ( + {activeDialogIncludesCommit && isEditingFiles && allFiles.length > 0 && ( )} Files - {!allSelected && !isEditingFiles && ( + {activeDialogIncludesCommit && !allSelected && !isEditingFiles && ( ({selectedFiles.length} of {allFiles.length}) )}
- {allFiles.length > 0 && ( + {activeDialogIncludesCommit && allFiles.length > 0 && (
-
-

Commit message (optional)

-