import { ThreadDetailsControl } from "./chat/ThreadDetailsControl"; import { useAtomMount, useAtomValue } from "@effect/atom-react"; import { AuthOrchestrationOperateScope, AuthSourceControlWriteScope, SourceControlProviderKind, type ScopedThreadRef, } from "@t3tools/contracts"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import type { GitRunStackedActionResult, GitStackedAction, SourceControlCloneProtocol, SourceControlProviderDiscoveryItem, SourceControlPublishRepositoryResult, SourceControlRepositoryVisibility, VcsStatusResult, } from "@t3tools/contracts"; import { useNavigate } from "@tanstack/react-router"; import * as Option from "effect/Option"; import { useEffect, useEffectEvent, useRef, useState } from "react"; import { useDeadlinePassed } from "../hooks/useDeadline"; import { useNowSecond } from "../hooks/useNowSecond"; import { flushSync } from "react-dom"; import { CheckIcon, ChevronDownIcon, CloudDownloadIcon, CloudUploadIcon, FileDiffIcon, GitBranchPlusIcon, GitCommitIcon, InfoIcon, LockIcon, GlobeIcon, } from "lucide-react"; import { Radio as RadioPrimitive } from "@base-ui/react/radio"; import { RadioGroup } from "~/components/ui/radio-group"; import { Spinner } from "~/components/ui/spinner"; import { Toggle, ToggleGroup } from "~/components/ui/toggle-group"; import { cn } from "~/lib/utils"; import { buildMenuItems, formatGitActionElapsed, GIT_ACTION_SUCCESS_VISIBLE_MS, type GitActionProgressPresentation, type GitActionIconName, type GitActionMenuItem, type GitQuickAction, type DefaultBranchConfirmableAction, requiresDefaultBranchConfirmation, resolveChangesTotals, resolveDefaultBranchActionDialogCopy, resolveFeatureBranchUnavailableReason, resolveGitActionProgressPresentation, resolveGitActionResultToastTiming, resolveLiveThreadBranchUpdate, resolveThreadBranchMetadataPatch, resolveQuickAction, resolveThreadBranchUpdate, } from "./GitActionsControl.logic"; import { WizardPopup, WizardHeader, WizardSteps, WizardPanel, WizardFooter } from "./ui/wizard"; import { StartTruncatedPath } from "./StartTruncatedPath"; import { Button } from "~/components/ui/button"; import { Checkbox } from "~/components/ui/checkbox"; import { Dialog, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle, } from "~/components/ui/dialog"; import { Input } from "~/components/ui/input"; import { Menu, MenuItem, MenuItemLabel, MenuPopup, MenuTrigger } from "~/components/ui/menu"; import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; import { ScrollArea } from "~/components/ui/scroll-area"; import { Textarea } from "~/components/ui/textarea"; import { stackedThreadToast, toastManager } from "~/components/ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; import { useOpenInPreferredEditor } from "~/editorPreferences"; import { useGitStackedAction, useSourceControlActionRunning, useSourceControlPublishRepositoryAction, useVcsInitAction, useVcsPullAction, } from "../state/sourceControlActions"; import { useQuietThreadProjection, useQuietThreadShell } from "~/state/entities"; import { readEnvironmentScope, useEnvironmentScope } from "~/state/session"; import { useEnvironmentQuery } from "~/state/query"; import { serverEnvironment } from "~/state/server"; import { sourceControlEnvironment } from "~/state/sourceControl"; import { threadEnvironment } from "~/state/threads"; import { useAtomCommand } from "~/state/use-atom-command"; import { vcsActionManager, vcsEnvironment, vcsStatusRevalidation } from "~/state/vcs"; import { randomUUID } from "~/lib/utils"; import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; import { type DraftId, useComposerDraftStore } from "~/composerDraftStore"; import { THREAD_DETAILS_PANEL_CHEVRON_CLASS, THREAD_DETAILS_PANEL_ICON_CLASS, THREAD_DETAILS_PANEL_LABEL_CLASS, THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS, THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS, } from "./chat/threadDetailsPanelStyles"; import { getSourceControlPresentation, sourceControlIcon } from "~/sourceControlPresentation"; import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; import { formatDiffCount } from "@t3tools/client-runtime/diff-count"; import { useOpenLink } from "~/browser/useOpenLink"; interface GitActionsControlProps { gitCwd: string | null; activeThreadRef: ScopedThreadRef | null; draftId?: DraftId; compact?: boolean; showActions?: boolean; showChanges?: boolean; onOpenChanges?: () => void; } interface PendingDefaultBranchAction { action: DefaultBranchConfirmableAction; branchName: string; includesCommit: boolean; commitMessage?: string; onConfirmed?: () => void; filePaths?: string[]; } interface RunGitActionWithToastInput { action: GitStackedAction; commitMessage?: string; onConfirmed?: () => void; skipDefaultBranchPrompt?: boolean; statusOverride?: VcsStatusResult | null; featureBranch?: boolean; filePaths?: string[]; } interface InlineGitActionSuccess { readonly title: string; readonly description: string | null; readonly scopeKey: string; readonly expiresAtMs: number; } const inlineSuccessExpiresAt = () => Date.now() + GIT_ACTION_SUCCESS_VISIBLE_MS; type RefreshVcsStatus = (target: { readonly environmentId: ScopedThreadRef["environmentId"]; readonly input: { readonly cwd: string }; }) => Promise; function requestVcsStatusRefresh( refresh: RefreshVcsStatus, environmentId: ScopedThreadRef["environmentId"] | null, cwd: string | null, ): void { if (environmentId === null || cwd === null) { return; } void refresh({ environmentId, input: { cwd } }); } const RUNNING_SOURCE_CONTROL_ACTIONS = ["runStackedAction", "pull", "publishRepository"] as const; const PUBLISH_PROVIDER_OPTIONS = sourceControlClients.definitions.map((definition) => ({ value: definition.kind, label: definition.pickerLabel, description: definition.publishDescription, pathPlaceholder: definition.repositoryPathHint, definition, Icon: sourceControlIcon(definition), })); function publishProviderOption(provider: SourceControlProviderKind) { return ( PUBLISH_PROVIDER_OPTIONS.find((option) => option.value === provider) ?? PUBLISH_PROVIDER_OPTIONS[0]! ); } function getPublishProviderReadiness(input: { provider: SourceControlProviderKind; sourceControlProviders: ReadonlyArray; }): { readonly ready: boolean; readonly hint: string | null } { const discovered = input.sourceControlProviders.find( (provider) => provider.kind === input.provider, ); if (!discovered) { return { ready: false, hint: "Provider status unavailable. Open Settings -> Source control and rescan.", }; } if (discovered.status !== "available") { return { ready: false, hint: discovered.installHint }; } if (discovered.auth.status === "unauthenticated") { return { ready: false, hint: Option.getOrNull(discovered.auth.detail) ?? `${discovered.label} is not authenticated. Open Settings -> Source control for setup guidance.`, }; } return { ready: true, hint: null }; } function getMenuActionDisabledReason({ item, gitStatus, isBusy, hasPrimaryRemote, }: { item: GitActionMenuItem; gitStatus: VcsStatusResult | null; isBusy: boolean; hasPrimaryRemote: boolean; }): string | null { if (!item.disabled) return null; if (isBusy) return "Git action in progress."; if (!gitStatus) return "Git status is unavailable."; const hasBranch = gitStatus.refName !== null; const hasChanges = gitStatus.hasWorkingTreeChanges; const isAhead = gitStatus.aheadCount > 0; const isBehind = gitStatus.behindCount > 0; const terminology = getSourceControlPresentation(gitStatus.sourceControlProvider).terminology; if (item.id === "commit") { if (!hasChanges) { return "Worktree is clean. Make changes before committing."; } return "Commit is currently unavailable."; } if (item.id === "push") { if (!hasBranch) { return "Detached HEAD: check out a branch before pushing."; } if (hasChanges) { return "Commit or stash local changes before pushing."; } if (isBehind) { return "Branch is behind upstream. Pull/rebase before pushing."; } if (!gitStatus.hasUpstream && !hasPrimaryRemote) { return 'Add an "origin" remote before pushing.'; } if (!isAhead) { return "No local commits to push."; } return "Push is currently unavailable."; } if (!hasBranch) { return `Detached HEAD: check out a branch before creating a ${terminology.singular}.`; } if (!gitStatus.hasUpstream && !hasPrimaryRemote) { return `Add an "origin" remote before creating a ${terminology.singular}.`; } if ((gitStatus.aheadOfDefaultCount ?? gitStatus.aheadCount) <= 0) { return `No local commits to include in a ${terminology.singular}.`; } if (isBehind) { return `Branch is behind upstream. Pull/rebase before creating a ${terminology.singular}.`; } return `Create ${terminology.singular} 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 GitActionItemIcon({ icon, SourceControlIcon, }: { icon: GitActionIconName; SourceControlIcon: ReturnType["Icon"]; }) { if (icon === "commit") return ; if (icon === "push") return ; return ; } function GitQuickActionIcon({ quickAction, SourceControlIcon, className = "size-3.5", }: { quickAction: GitQuickAction; className?: string; SourceControlIcon: ReturnType["Icon"]; }) { if (quickAction.kind === "open_publish") return ; if (quickAction.kind === "run_pull") return ; if (quickAction.kind === "run_action") { if (quickAction.action === "commit") return ; if (quickAction.action === "push" || quickAction.action === "commit_push") { return ; } return ; } if (quickAction.label === "Commit") return ; if (quickAction.label === "Push") return ; return ; } function GitActionElapsedTime({ startedAtMs, className, }: { startedAtMs: number | null; className?: string; }) { return startedAtMs === null ? null : ( ); } function RunningGitActionElapsedTime({ startedAtMs, className, }: { startedAtMs: number; className: string | undefined; }) { const nowMs = useNowSecond(); const elapsed = formatGitActionElapsed(startedAtMs, nowMs); if (!elapsed) { return null; } return

{elapsed}

; } function GitActionProgressButtonContent({ progress }: { progress: GitActionProgressPresentation }) { const hasOutput = progress.output !== null; return (
); } function GitActionSuccessButtonContent({ success }: { success: InlineGitActionSuccess }) { const hasDescription = success.description !== null; return (
); } interface PublishRepositoryDialogProps { readonly open: boolean; readonly onOpenChange: (open: boolean) => void; readonly environmentId: ScopedThreadRef["environmentId"] | null; readonly threadRef: ScopedThreadRef | null; readonly gitCwd: string; } function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { const openLink = useOpenLink(props.threadRef); const navigate = useNavigate(); const sourceControlDiscovery = useEnvironmentQuery( props.environmentId === null ? null : sourceControlEnvironment.discovery({ environmentId: props.environmentId, input: {}, }), ); const [selectedPublishProvider, setSelectedPublishProvider] = useState(null); const [publishRepositoryOverride, setPublishRepositoryOverride] = useState(null); const [publishVisibility, setPublishVisibility] = useState("private"); const [publishRemoteName, setPublishRemoteName] = useState("origin"); const [publishProtocol, setPublishProtocol] = useState("ssh"); const [publishWizardStep, setPublishWizardStep] = useState(0); const [publishAdvancedOpen, setPublishAdvancedOpen] = useState(false); const [publishError, setPublishError] = useState(null); const [publishResult, setPublishResult] = useState( null, ); const sourceControlScope = { environmentId: props.environmentId, cwd: props.gitCwd, }; const publishRepositoryAction = useSourceControlPublishRepositoryAction(sourceControlScope); const publishAccountByProvider = new Map(); for (const provider of sourceControlDiscovery.data?.sourceControlProviders ?? []) { publishAccountByProvider.set(provider.kind, Option.getOrNull(provider.auth.account)); } const publishSourceControlProviders = sourceControlDiscovery.data?.sourceControlProviders ?? []; const publishReadinessByProvider = new Map( PUBLISH_PROVIDER_OPTIONS.map((option) => [ option.value, getPublishProviderReadiness({ provider: option.value, sourceControlProviders: publishSourceControlProviders, }), ]), ); const publishProviderReadiness = (provider: SourceControlProviderKind) => publishReadinessByProvider.get(provider) ?? { ready: false, hint: null }; const hasReadyPublishProvider = PUBLISH_PROVIDER_OPTIONS.some( (option) => publishProviderReadiness(option.value).ready, ); const sortedPublishProviderOptions = PUBLISH_PROVIDER_OPTIONS.toSorted((left, right) => { const leftReady = publishProviderReadiness(left.value).ready; const rightReady = publishProviderReadiness(right.value).ready; if (leftReady !== rightReady) { return leftReady ? -1 : 1; } return left.label.localeCompare(right.label); }); const firstReadyPublishProvider = sortedPublishProviderOptions.find( (option) => publishProviderReadiness(option.value).ready, )?.value; const publishProvider = selectedPublishProvider !== null && publishProviderReadiness(selectedPublishProvider).ready ? selectedPublishProvider : (firstReadyPublishProvider ?? selectedPublishProvider ?? sourceControlClients.get(undefined).kind); const selectedPublishProviderReadiness = publishProviderReadiness(publishProvider); const publishAccount = publishAccountByProvider.get(publishProvider) ?? null; const publishRepositoryPrefill = publishAccount ? `${publishAccount}/` : ""; const publishRepository = publishRepositoryOverride ?? publishRepositoryPrefill; const currentPublishProvider = publishProviderOption(publishProvider); const publishHost = currentPublishProvider.definition.publishHost( Option.getOrNull( sourceControlDiscovery.data?.sourceControlProviders.find( (provider) => provider.kind === publishProvider, )?.auth.host ?? Option.none(), ), ); const publishPathPlaceholder = currentPublishProvider.pathPlaceholder; const publishProviderLabel = currentPublishProvider.label; const publishWizardSteps = ["Provider", "Repository", "Summary"] as const; const publishWizardStepSummaries = [ publishProviderLabel, publishResult?.repository.nameWithOwner ?? null, null, ] as const; const publishRepositoryParts = publishRepository.trim().split("/"); const publishRepositoryOwner = publishRepositoryParts[0]?.trim() ?? ""; const publishRepositoryName = publishRepositoryParts.slice(1).join("/").trim(); const canSubmitPublishRepository = publishRepositoryAction.isAllowed && selectedPublishProviderReadiness.ready && !publishRepositoryAction.isPending && publishRepositoryOwner.length > 0 && publishRepositoryName.length > 0; const submitPublishRepository = () => { if (!canSubmitPublishRepository) { return; } setPublishError(null); void (async () => { const result = await publishRepositoryAction.run({ provider: publishProvider, repository: publishRepository.trim(), visibility: publishVisibility, remoteName: publishRemoteName.trim() || "origin", protocol: publishProtocol, }); if (result._tag === "Failure") { if (!isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); setPublishError(error instanceof Error ? error.message : "An error occurred."); } return; } flushSync(() => { setPublishResult(result.value); setPublishWizardStep(2); }); })(); }; const resetState = () => { setPublishRemoteName("origin"); setPublishRepositoryOverride(null); setPublishWizardStep(0); setPublishAdvancedOpen(false); setPublishError(null); setPublishResult(null); }; const handleOpenChange = (open: boolean) => { props.onOpenChange(open); if (!open) { resetState(); } }; const openSourceControlSettings = () => { handleOpenChange(false); void navigate({ to: "/settings/source-control" }); }; return ( publishWizardStep === 2 || index >= publishWizardSteps.length - 1 || index > publishWizardStep } onStepChange={setPublishWizardStep} />
Provider { setSelectedPublishProvider(value as SourceControlProviderKind); setPublishRepositoryOverride(null); }} aria-labelledby="publish-provider-cards-label" className="grid grid-cols-2" > {sortedPublishProviderOptions.map((option) => { const readiness = publishProviderReadiness(option.value); const isSelected = publishProvider === option.value && readiness.ready; if (!readiness.ready) { return (
{option.label} { event.preventDefault(); event.stopPropagation(); openSourceControlSettings(); }} > Setup Required } /> {readiness.hint ?? "Open Settings -> Source control to configure this provider."}
); } return ( {option.label} ); })}
{publishHost}/ { setPublishRepositoryOverride(event.target.value); }} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); submitPublishRepository(); } }} placeholder={publishPathPlaceholder} disabled={publishRepositoryAction.isPending} className="w-full bg-transparent px-3 py-2 font-mono text-sm placeholder:text-muted-foreground/60 focus:outline-none" />
Visibility setPublishVisibility(value as SourceControlRepositoryVisibility) } aria-labelledby="publish-visibility-cards-label" disabled={publishRepositoryAction.isPending} className="grid grid-cols-2" > {[ { value: "private" as const, label: "Private", description: "Only invited people", Icon: LockIcon, }, { value: "public" as const, label: "Public", description: "Anyone on the web", Icon: GlobeIcon, }, ].map((option) => { const isSelected = publishVisibility === option.value; return ( {option.label} {option.description} ); })}
{publishAdvancedOpen ? (
Protocol { const protocol = next[0]; if (protocol === "ssh" || protocol === "https") { setPublishProtocol(protocol); } }} aria-labelledby="publish-protocol-label" disabled={publishRepositoryAction.isPending} > SSH HTTPS
) : null}
{publishRepositoryAction.isPending ? (
Publishing repository to {publishProviderLabel}...
) : null} {publishError && !publishRepositoryAction.isPending ? (

Publish failed

{publishError}

) : null}
{publishResult ? ( <>

{publishResult.status === "pushed" ? "Repository published" : "Repository created"}

{publishResult.status === "pushed" ? `${publishResult.branch} is now live on ${publishProviderLabel}.` : `Remote "${publishResult.remoteName}" is set up. Make a commit and push it to share your code.`}

{publishResult.repository.nameWithOwner}
) : (
Publish result unavailable.
)}
{publishWizardStep === 2 ? ( ) : ( <> {publishWizardStep < 1 ? ( ) : ( )} )}
); } export default function GitActionsControl({ gitCwd, activeThreadRef, draftId, compact = false, showActions = true, showChanges = true, onOpenChanges, }: GitActionsControlProps) { const panelAnchorRef = useRef(null); const updateThreadMetadata = useAtomCommand( threadEnvironment.updateMetadata, "thread branch metadata update", ); const activeEnvironmentId = activeThreadRef?.environmentId ?? null; const successScopeKey = `${activeEnvironmentId ?? ""}\u0000${gitCwd ?? ""}`; const canWriteSourceControl = useEnvironmentScope( activeEnvironmentId, AuthSourceControlWriteScope, ); const canOperateThread = useEnvironmentScope(activeEnvironmentId, AuthOrchestrationOperateScope); const serverConfig = useAtomValue(serverEnvironment.configValueAtom(activeEnvironmentId)); const openInPreferredEditor = useOpenInPreferredEditor( activeEnvironmentId, serverConfig?.availableEditors ?? [], ); const threadToastData = activeThreadRef ? { threadRef: activeThreadRef } : undefined; const activeServerThreadShell = useQuietThreadShell(activeThreadRef); const activeDraftThread = useComposerDraftStore((store) => draftId ? store.getDraftSession(draftId) : activeThreadRef ? store.getDraftThreadByRef(activeThreadRef) : null, ); const activeServerThreadProjection = useQuietThreadProjection( activeDraftThread !== null && activeServerThreadShell === null ? null : activeThreadRef, ); const activeServerThread = activeServerThreadShell ?? activeServerThreadProjection?.projection.thread ?? null; const isLocalDraftThread = activeDraftThread !== null && activeServerThread === null; const canChangeThreadBranch = canWriteSourceControl && (isLocalDraftThread || canOperateThread); const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); const [isCommitDialogOpen, setIsCommitDialogOpen] = useState(false); const [dialogCommitMessage, setDialogCommitMessage] = useState(""); const [excludedFiles, setExcludedFiles] = useState>(new Set()); const [isEditingFiles, setIsEditingFiles] = useState(false); const [isPublishDialogOpen, setIsPublishDialogOpen] = useState(false); const [inlineSuccess, setInlineSuccess] = useState(null); const [pendingDefaultBranchAction, setPendingDefaultBranchAction] = useState(null); const sourceControlScope = { environmentId: activeEnvironmentId, cwd: gitCwd }; const vcsActionState = useAtomValue(vcsActionManager.stateAtom(sourceControlScope)); const inlineSuccessExpired = useDeadlinePassed(inlineSuccess?.expiresAtMs ?? null); const visibleInlineSuccess = inlineSuccess?.scopeKey === successScopeKey && !inlineSuccessExpired ? inlineSuccess : null; const persistThreadBranchSync = (branch: string | null, manualSelection = false) => { if (!activeThreadRef) { return; } if (activeServerThread) { if ( !readEnvironmentScope(activeThreadRef.environmentId, AuthOrchestrationOperateScope) || activeServerThread.branch === branch ) { return; } void updateThreadMetadata({ environmentId: activeThreadRef.environmentId, input: { threadId: activeThreadRef.threadId, ...resolveThreadBranchMetadataPatch(branch, activeServerThread.branch), }, }); return; } if (!activeDraftThread || activeDraftThread.branch === branch) { return; } setDraftThreadContext(draftId ?? activeThreadRef, { branch, worktreePath: activeDraftThread.worktreePath, environmentSelection: manualSelection ? "manual" : (activeDraftThread.environmentSelection ?? (activeDraftThread.branch ? "manual" : "auto")), }); }; const persistLiveThreadBranch = useEffectEvent((branch: string | null) => { persistThreadBranchSync(branch); }); const syncThreadBranchAfterGitAction = (result: GitRunStackedActionResult) => { const branchUpdate = resolveThreadBranchUpdate(result); if (!branchUpdate) { return; } persistThreadBranchSync(branchUpdate.branch, true); }; const gitStatusQuery = useEnvironmentQuery( activeEnvironmentId !== null && gitCwd !== null ? vcsEnvironment.status({ environmentId: activeEnvironmentId, input: { cwd: gitCwd }, }) : null, ); const refreshVcsStatus = useAtomCommand(vcsEnvironment.refreshStatus, { reportFailure: false, }); const { data: gitStatus, error: gitStatusError } = gitStatusQuery; const sourceControlPresentation = getSourceControlPresentation(gitStatus?.sourceControlProvider); const changeRequestTerminology = sourceControlPresentation.terminology; const SourceControlIcon = sourceControlPresentation.Icon; const isRepo = gitStatus?.isRepo ?? true; const hasPrimaryRemote = gitStatus?.hasPrimaryRemote ?? false; const gitStatusForActions = gitStatus; const changesTotals = resolveChangesTotals(gitStatusForActions) ?? { baseRef: null, insertions: 0, deletions: 0, }; const allFiles = gitStatusForActions?.workingTree.files ?? []; // Changed-file paths are repository-relative, so they must resolve against // the worktree root from the same status snapshot, not the project cwd. const workingTreeRoot = gitStatusForActions?.workingTree.rootPath ?? null; const selectedFiles = allFiles.filter((f) => !excludedFiles.has(f.path)); const allSelected = excludedFiles.size === 0; const noneSelected = selectedFiles.length === 0; const initAction = useVcsInitAction(sourceControlScope); const runImmediateGitAction = useGitStackedAction(sourceControlScope); const pullAction = useVcsPullAction(sourceControlScope); const isGitActionRunning = useSourceControlActionRunning( sourceControlScope, RUNNING_SOURCE_CONTROL_ACTIONS, ); const isSelectingWorktreeBase = !activeServerThread && activeDraftThread?.envMode === "worktree" && activeDraftThread.worktreePath === null; // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- persists the draft thread branch when the live git status subscription reports a different branch useEffect(() => { if (isGitActionRunning || isSelectingWorktreeBase || activeServerThread) { return; } const branchUpdate = resolveLiveThreadBranchUpdate({ threadBranch: activeDraftThread?.branch ?? null, gitStatus: gitStatusForActions, }); if (!branchUpdate) { return; } persistLiveThreadBranch(branchUpdate.branch); }, [ activeServerThread, activeDraftThread?.branch, gitStatusForActions, isGitActionRunning, isSelectingWorktreeBase, ]); const isDefaultRef = gitStatusForActions?.isDefaultRef ?? false; const gitActionMenuItems = buildMenuItems( gitStatusForActions, isGitActionRunning, hasPrimaryRemote, ); const quickAction = resolveQuickAction( gitStatusForActions, isGitActionRunning, isDefaultRef, hasPrimaryRemote, ); const quickActionDisabledReason = !canWriteSourceControl ? "This connection cannot change source control." : quickAction.disabled ? (quickAction.hint ?? "This action is currently unavailable.") : null; const gitActionProgress = resolveGitActionProgressPresentation(vcsActionState); // A fresh draft or a thread with no turns hides commit, push, and diff rows until there is work. const quietEmptyDraft = (draftId !== undefined || activeServerThreadShell?.latestRun === null) && gitActionProgress === null && !gitStatusError && quickAction.disabled && !changesTotals.insertions && !changesTotals.deletions; const pendingDefaultBranchActionCopy = pendingDefaultBranchAction ? resolveDefaultBranchActionDialogCopy({ action: pendingDefaultBranchAction.action, branchName: pendingDefaultBranchAction.branchName, includesCommit: pendingDefaultBranchAction.includesCommit, terminology: changeRequestTerminology, }) : null; // Keep enabled until live status loads; the server rejects stale requests. const featureBranchUnavailableReason = pendingDefaultBranchAction ? resolveFeatureBranchUnavailableReason({ branchName: pendingDefaultBranchAction.branchName, hasWorkingTreeChanges: gitStatus?.hasWorkingTreeChanges ?? true, }) : null; useAtomMount( vcsStatusRevalidation( activeEnvironmentId !== null && gitCwd !== null ? { environmentId: activeEnvironmentId, cwd: gitCwd } : null, ), ); async function runGitActionWithToast({ action, commitMessage, onConfirmed, skipDefaultBranchPrompt = false, statusOverride, featureBranch = false, filePaths, }: RunGitActionWithToastInput) { if ( activeEnvironmentId === null || !readEnvironmentScope(activeEnvironmentId, AuthSourceControlWriteScope) || (featureBranch && !isLocalDraftThread && !readEnvironmentScope(activeEnvironmentId, AuthOrchestrationOperateScope)) ) return; const actionStatus = statusOverride ?? gitStatusForActions; const actionBranch = actionStatus?.refName ?? null; const actionIsDefaultBranch = featureBranch ? false : isDefaultRef; const actionCanCommit = action === "commit" || action === "commit_push" || action === "commit_push_pr"; const includesCommit = actionCanCommit && (action === "commit" || !!actionStatus?.hasWorkingTreeChanges || featureBranch); if ( !skipDefaultBranchPrompt && requiresDefaultBranchConfirmation(action, actionIsDefaultBranch) && actionBranch ) { if ( action !== "push" && action !== "create_pr" && action !== "commit_push" && action !== "commit_push_pr" ) { return; } setPendingDefaultBranchAction({ action, branchName: actionBranch, includesCommit, ...(commitMessage ? { commitMessage } : {}), ...(onConfirmed ? { onConfirmed } : {}), ...(filePaths ? { filePaths } : {}), }); return; } onConfirmed?.(); setInlineSuccess(null); const scopedToastData = threadToastData ? { ...threadToastData } : undefined; const actionId = randomUUID(); const result = await runImmediateGitAction.run({ actionId, action, ...(commitMessage ? { commitMessage } : {}), ...(featureBranch ? { featureBranch } : {}), ...(filePaths ? { filePaths } : {}), ...(activeServerThread ? { threadId: activeServerThread.id } : {}), ...(activeDraftThread ? { projectId: activeDraftThread.projectId } : {}), }); if (result._tag === "Failure") { if (isAtomCommandInterrupted(result)) { return; } const error = squashAtomCommandFailure(result); const errorToastTiming = resolveGitActionResultToastTiming("error"); toastManager.add( stackedThreadToast({ type: "error", title: "Action failed", description: error instanceof Error ? error.message : "An error occurred.", timeout: errorToastTiming.timeout, ...(scopedToastData !== undefined ? { data: scopedToastData } : {}), }), ); return; } const actionResult = result.value; syncThreadBranchAfterGitAction(actionResult); const createdPullRequest = actionResult.pr.status === "created"; setInlineSuccess({ title: createdPullRequest ? `Created ${changeRequestTerminology.shortLabel}` : actionResult.toast.title, description: createdPullRequest ? null : (actionResult.toast.description ?? null), scopeKey: successScopeKey, expiresAtMs: inlineSuccessExpiresAt(), }); } const continuePendingDefaultBranchAction = (featureBranch: boolean) => { if (!pendingDefaultBranchAction || !canWriteSourceControl) return; if (featureBranch && (!canChangeThreadBranch || featureBranchUnavailableReason !== null)) return; const { action, commitMessage, onConfirmed, filePaths } = pendingDefaultBranchAction; setPendingDefaultBranchAction(null); void runGitActionWithToast({ action, ...(commitMessage ? { commitMessage } : {}), ...(onConfirmed ? { onConfirmed } : {}), ...(filePaths ? { filePaths } : {}), featureBranch, skipDefaultBranchPrompt: true, }); }; const runDialogActionOnNewBranch = () => { if (!isCommitDialogOpen) return; const commitMessage = dialogCommitMessage.trim(); setIsCommitDialogOpen(false); setDialogCommitMessage(""); setExcludedFiles(new Set()); setIsEditingFiles(false); void runGitActionWithToast({ action: "commit", ...(commitMessage ? { commitMessage } : {}), ...(!allSelected ? { filePaths: selectedFiles.map((f) => f.path) } : {}), featureBranch: true, skipDefaultBranchPrompt: true, }); }; const runQuickAction = () => { if (quickAction.kind === "open_publish") { setIsPublishDialogOpen(true); return; } if (quickAction.kind === "run_pull") { void (async () => { setInlineSuccess(null); const result = await pullAction.run(); if (result._tag === "Failure") { if (isAtomCommandInterrupted(result)) { return; } const error = squashAtomCommandFailure(result); const errorToastTiming = resolveGitActionResultToastTiming("error"); toastManager.add( stackedThreadToast({ type: "error", title: "Pull failed", description: error instanceof Error ? error.message : "An error occurred.", timeout: errorToastTiming.timeout, ...(threadToastData !== undefined ? { data: threadToastData } : {}), }), ); return; } const pullResult = result.value; const title = pullResult.status === "pulled" ? "Pulled" : "Already up to date"; const description = pullResult.status === "pulled" ? `Updated ${pullResult.refName} from ${pullResult.upstreamRef ?? "upstream"}` : `${pullResult.refName} is already synchronized.`; setInlineSuccess({ title, description, scopeKey: successScopeKey, expiresAtMs: inlineSuccessExpiresAt(), }); })(); return; } if (quickAction.kind === "show_hint") { toastManager.add({ type: "info", title: quickAction.label, description: quickAction.hint, data: threadToastData, }); return; } if (quickAction.action) { void runGitActionWithToast({ action: quickAction.action }); } }; const openDialogForMenuItem = (item: GitActionMenuItem) => { if (item.disabled) return; if (item.dialogAction === "push") { void runGitActionWithToast({ action: "push" }); return; } if (item.dialogAction === "create_pr") { void runGitActionWithToast({ action: "create_pr" }); return; } setExcludedFiles(new Set()); setIsEditingFiles(false); setIsCommitDialogOpen(true); }; const runDialogAction = () => { if (!isCommitDialogOpen) return; const commitMessage = dialogCommitMessage.trim(); setIsCommitDialogOpen(false); setDialogCommitMessage(""); setExcludedFiles(new Set()); setIsEditingFiles(false); void runGitActionWithToast({ action: "commit", ...(commitMessage ? { commitMessage } : {}), ...(!allSelected ? { filePaths: selectedFiles.map((f) => f.path) } : {}), }); }; const openChangedFileInEditor = (filePath: string) => { if (workingTreeRoot === null) { toastManager.add({ type: "error", title: "Editor opening is unavailable.", data: threadToastData, }); return; } const target = resolvePathLinkTarget(filePath, workingTreeRoot); void (async () => { const result = await openInPreferredEditor(target); if (result._tag === "Success" || isAtomCommandInterrupted(result)) { return; } const error = squashAtomCommandFailure(result); toastManager.add( stackedThreadToast({ type: "error", title: "Unable to open file", description: error instanceof Error ? error.message : "An error occurred.", ...(threadToastData !== undefined ? { data: threadToastData } : {}), }), ); })(); }; const canPublishRepository = canWriteSourceControl && isRepo && gitStatusForActions !== null && !hasPrimaryRemote; const initializeGit = () => { void (async () => { const result = await initAction.run(); if (result._tag === "Success" || isAtomCommandInterrupted(result)) { return; } const error = squashAtomCommandFailure(result); toastManager.add( stackedThreadToast({ type: "error", title: "Git initialization failed", description: error instanceof Error ? error.message : "An error occurred.", ...(threadToastData !== undefined ? { data: threadToastData } : {}), }), ); })(); }; const gitItems = ( <> {gitActionMenuItems.map((item) => { const disabledReason = getMenuActionDisabledReason({ item, gitStatus: gitStatusForActions, isBusy: isGitActionRunning, hasPrimaryRemote, }); if (item.disabled && disabledReason) { return ( } > {item.label} {disabledReason} ); } return ( { openDialogForMenuItem(item); }} > {item.label} ); })} {canPublishRepository ? ( { setIsPublishDialogOpen(true); }} > Publish repository... ) : null} {gitStatusForActions?.refName === null && (

Detached HEAD: create and check out a branch to enable push and{" "} {changeRequestTerminology.singular} actions.

)} {gitStatusForActions && gitStatusForActions.refName !== null && !gitStatusForActions.hasWorkingTreeChanges && gitStatusForActions.behindCount > 0 && gitStatusForActions.aheadCount === 0 && (

Behind upstream. Pull/rebase first.

)} {gitStatusError &&

{gitStatusError}

} ); if (!gitCwd) return null; return ( <> {!showActions ? null : !isRepo ? ( {initAction.isPending ? "Initializing..." : "Initialize Git"} ) : quietEmptyDraft || (compact && !gitActionProgress && !visibleInlineSuccess) ? null : (
{gitActionProgress ? ( ) : visibleInlineSuccess ? ( ) : quickActionDisabledReason ? ( } > {quickAction.label} {quickActionDisabledReason} ) : ( {quickAction.label} )} {gitActionProgress ? ( ) : ( <>
)} {showChanges && isRepo && !quietEmptyDraft ? ( } > Changes {changesTotals.baseRef ? `vs ${changesTotals.baseRef}` : "Uncommitted"} +{formatDiffCount(changesTotals.insertions)} -{formatDiffCount(changesTotals.deletions)} {changesTotals.baseRef ? `Changes compared with ${changesTotals.baseRef}` : "Uncommitted changes"} ) : null} { if (!open) { setIsCommitDialogOpen(false); setDialogCommitMessage(""); setExcludedFiles(new Set()); setIsEditingFiles(false); } }} > {COMMIT_DIALOG_TITLE} {COMMIT_DIALOG_DESCRIPTION}
Branch {gitStatusForActions?.refName ?? "(detached HEAD)"} {isDefaultRef && Default branch}
{isEditingFiles && allFiles.length > 0 && ( { setExcludedFiles( allSelected ? new Set(allFiles.map((f) => f.path)) : new Set(), ); }} /> )} Files {!allSelected && !isEditingFiles && ( ({selectedFiles.length} of {allFiles.length}) )}
{allFiles.length > 0 && ( )}
{!gitStatusForActions || allFiles.length === 0 ? (

none

) : (
{allFiles.map((file) => { const isExcluded = excludedFiles.has(file.path); return (
{isEditingFiles && ( { setExcludedFiles((prev) => { const next = new Set(prev); if (next.has(file.path)) { next.delete(file.path); } else { next.add(file.path); } return next; }); }} /> )}
); })}
+{formatDiffCount(selectedFiles.reduce((sum, f) => sum + f.insertions, 0))} / -{formatDiffCount(selectedFiles.reduce((sum, f) => sum + f.deletions, 0))}
)}

Commit message (optional)