import { useAtomValue } from "@effect/atom-react";import { type ScopedThreadRef } from "@t3tools/contracts";import { isAtomCommandInterrupted, squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime";import type { GitRunStackedActionResult, GitStackedAction, SourceControlCloneProtocol, SourceControlProviderDiscoveryItem, SourceControlProviderKind, SourceControlPublishRepositoryResult, SourceControlRepositoryVisibility, VcsStatusResult,} from "@t3tools/contracts";import { useNavigate } from "@tanstack/react-router";import * as Option from "effect/Option";import { type MouseEvent, useCallback, useEffect, useEffectEvent, useMemo, useRef, useState,} from "react";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 { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "~/components/Icons";import { RadioGroup } from "~/components/ui/radio-group";import { Spinner } from "~/components/ui/spinner";import { toggleVariants } from "~/components/ui/toggle";import { cn } from "~/lib/utils";import { useOpenPrLink } from "~/lib/openPullRequestLink";import { buildMenuItems, formatGitActionElapsed, GIT_ACTION_SUCCESS_VISIBLE_MS, type GitActionProgressPresentation, type GitActionIconName, type GitActionMenuItem, type GitQuickAction, type DefaultBranchConfirmableAction, requiresDefaultBranchConfirmation, resolveDefaultBranchActionDialogCopy, resolveGitActionProgressPresentation, resolveGitActionResultToastTiming, resolveLiveThreadBranchUpdate, resolveThreadBranchMetadataPatch, resolveQuickAction, resolveThreadBranchUpdate,} from "./GitActionsControl.logic";import { AnimatedHeight } from "./AnimatedHeight";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 { Group, GroupSeparator } from "~/components/ui/group";import { Input } from "~/components/ui/input";import { Menu, MenuItem, 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 "~/lib/sourceControlActions";import { useThreadShell } from "~/state/entities";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 } from "~/state/vcs";import { randomUUID } from "~/lib/utils";import { resolvePathLinkTarget } from "~/terminal-links";import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";import { THREAD_DETAILS_PANEL_CHEVRON_CLASS, THREAD_DETAILS_PANEL_ICON_CLASS, THREAD_DETAILS_PANEL_ROW_POPUP_CLASS, THREAD_DETAILS_PANEL_ROW_CLASS, THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS, THREAD_DETAILS_PANEL_SPLIT_PRIMARY_CLASS, THREAD_DETAILS_PANEL_SPLIT_SECONDARY_CLASS, THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS,} from "./chat/threadDetailsPanelStyles";import { getSourceControlPresentation } from "~/sourceControlPresentation";import { useOpenLink } from "~/browser/useOpenLink"; interface GitActionsControlProps { gitCwd: string | null; activeThreadRef: ScopedThreadRef | null; draftId?: DraftId; /** * Opens the thread's own change request beside it. Absent when the thread has no project to * place it against, in which case it still opens in the browser. */ onOpenPullRequest?: ((number: number) => void) | undefined; displayMode?: "toolbar" | "panel"; onOpenChanges?: () => void;} interface PendingDefaultBranchAction { action: DefaultBranchConfirmableAction; branchName: string; includesCommit: boolean; commitMessage?: string; onConfirmed?: () => void; filePaths?: string[];} type PublishProviderKind = Extract< SourceControlProviderKind, "github" | "gitlab" | "bitbucket" | "azure-devops">; type GitActionToastId = ReturnType<typeof toastManager.add>; 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;} const GIT_STATUS_WINDOW_REFRESH_DEBOUNCE_MS = 250; type RefreshVcsStatus = (target: { readonly environmentId: ScopedThreadRef["environmentId"]; readonly input: { readonly cwd: string };}) => Promise<unknown>; 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 = [ { value: "github", label: "GitHub", description: "github.com", host: "github.com", pathPlaceholder: "owner/repo", Icon: GitHubIcon, }, { value: "gitlab", label: "GitLab", description: "gitlab.com", host: "gitlab.com", pathPlaceholder: "group/project", Icon: GitLabIcon, }, { value: "bitbucket", label: "Bitbucket", description: "bitbucket.org", host: "bitbucket.org", pathPlaceholder: "workspace/repository", Icon: BitbucketIcon, }, { value: "azure-devops", label: "Azure DevOps", description: "dev.azure.com", host: "dev.azure.com", pathPlaceholder: "project/repository", Icon: AzureDevOpsIcon, },] as const satisfies ReadonlyArray<{ readonly value: PublishProviderKind; readonly label: string; readonly description: string; readonly host: string; readonly pathPlaceholder: string; readonly Icon: typeof GitHubIcon;}>; function publishProviderOption(provider: PublishProviderKind) { return ( PUBLISH_PROVIDER_OPTIONS.find((option) => option.value === provider) ?? PUBLISH_PROVIDER_OPTIONS[0] );} function isPublishProviderKind( provider: SourceControlProviderKind,): provider is PublishProviderKind { return PUBLISH_PROVIDER_OPTIONS.some((option) => option.value === provider);} function getPublishProviderReadiness(input: { provider: PublishProviderKind; sourceControlProviders: ReadonlyArray<SourceControlProviderDiscoveryItem>;}): { 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: checkout a refName 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: checkout a refName before creating a ${terminology.singular}.`; } if (hasChanges) { return `Commit local changes before creating a ${terminology.singular}.`; } if (!gitStatus.hasUpstream && !hasPrimaryRemote) { return `Add an "origin" remote before creating a ${terminology.singular}.`; } if (!isAhead) { 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<typeof getSourceControlPresentation>["Icon"];}) { if (icon === "commit") return <GitCommitIcon />; if (icon === "push") return <CloudUploadIcon />; return <SourceControlIcon />;} function GitQuickActionIcon({ quickAction, SourceControlIcon, className = "size-3.5",}: { quickAction: GitQuickAction; SourceControlIcon: ReturnType<typeof getSourceControlPresentation>["Icon"]; className?: string;}) { const iconClassName = className; if (quickAction.kind === "open_publish") return <CloudUploadIcon className={iconClassName} />; if (quickAction.kind === "run_pull") return <CloudDownloadIcon className={iconClassName} />; if (quickAction.kind === "run_action") { if (quickAction.action === "commit") return <GitCommitIcon className={iconClassName} />; if (quickAction.action === "push" || quickAction.action === "commit_push") { return <CloudUploadIcon className={iconClassName} />; } return <SourceControlIcon className={iconClassName} />; } if (quickAction.label === "Commit") return <GitCommitIcon className={iconClassName} />; if (quickAction.label === "Push") return <CloudUploadIcon className={iconClassName} />; return <InfoIcon className={iconClassName} />;} function GitActionElapsedTime({ startedAtMs, className,}: { startedAtMs: number | null; className?: string;}) { const [nowMs, setNowMs] = useState(() => Date.now()); useEffect(() => { if (startedAtMs === null) { return; } const intervalId = window.setInterval(() => { setNowMs(Date.now()); }, 1_000); return () => window.clearInterval(intervalId); }, [startedAtMs]); const elapsed = formatGitActionElapsed(startedAtMs, nowMs); if (!elapsed) { return null; } return <p className={className}>{elapsed}</p>;} function GitActionProgressButtonContent({ progress, isPanel,}: { progress: GitActionProgressPresentation; isPanel: boolean;}) { const hasOutput = progress.output !== null; return ( <div aria-atomic="false" aria-live="polite" className={cn( "grid min-w-0 flex-1 items-center", // Pin the title row to the button's minimum content height (min-height // minus vertical padding and border) so revealing the output row // extends the button downward without re-centering — the title must // not shift. No row gap: the collapsed output row must contribute zero // height so the single-line running button matches the static button // exactly. The panel column gap matches the static row's icon-to-label // distance (gap-2.5 plus the label's ml-0.5). In the panel the elapsed // counter renders outside the button (in the menu-chevron slot), so // there is no trailing column. isPanel ? "grid-cols-[auto_minmax(0,1fr)] grid-rows-[1.75rem] gap-x-3" : "grid-cols-[auto_minmax(0,1fr)_auto] grid-rows-[1.25rem] gap-x-2.5 sm:grid-rows-[1rem]", )} role="status" > <Spinner aria-hidden="true" className="row-start-1 -mx-0.5 size-4 shrink-0 text-muted-foreground" /> <p className="row-start-1 min-w-0 truncate text-left">{progress.status}</p> {!isPanel ? ( <GitActionElapsedTime startedAtMs={progress.startedAtMs} className="row-start-1 text-[11px] font-normal tabular-nums text-muted-foreground" /> ) : null} <div className={cn( "col-start-2 grid min-w-0 transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none", !isPanel && "col-span-2", hasOutput ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0", )} > <div className="min-h-0 overflow-hidden"> <Tooltip> <TooltipTrigger render={ <p className="truncate pt-0.5 text-left text-[11px] font-normal text-muted-foreground" /> } > {progress.output} </TooltipTrigger> <TooltipPopup side="bottom" className="max-w-96 break-words"> {progress.output} </TooltipPopup> </Tooltip> </div> </div> </div> );} function GitActionSuccessButtonContent({ success }: { success: InlineGitActionSuccess }) { const hasDescription = success.description !== null; return ( <div aria-live="polite" className="grid min-w-0 flex-1 grid-cols-[auto_minmax(0,1fr)] grid-rows-[1.75rem] items-center gap-x-3" role="status" > <CheckIcon aria-hidden="true" className="size-3.5 shrink-0 text-success" /> <p className="min-w-0 truncate text-left">{success.title}</p> <div className={cn( "col-start-2 grid min-w-0 transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none", hasDescription ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0", )} > <div className="min-h-0 overflow-hidden"> <Tooltip> <TooltipTrigger render={ <p className="truncate pt-0.5 text-left text-[11px] font-normal text-muted-foreground" /> } > {success.description} </TooltipTrigger> <TooltipPopup side="bottom" className="max-w-96 break-words"> {success.description} </TooltipPopup> </Tooltip> </div> </div> </div> );} interface PublishRepositoryDialogProps {Lines 1–500 of 2153. Load more
import { useAtomValue } from "@effect/atom-react"; import { type ScopedThreadRef } from "@t3tools/contracts"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import type { GitRunStackedActionResult, GitStackedAction, SourceControlCloneProtocol, SourceControlProviderDiscoveryItem, SourceControlProviderKind, SourceControlPublishRepositoryResult, SourceControlRepositoryVisibility, VcsStatusResult, } from "@t3tools/contracts"; import { useNavigate } from "@tanstack/react-router"; import * as Option from "effect/Option"; import { type MouseEvent, useCallback, useEffect, useEffectEvent, useMemo, useRef, useState, } from "react"; 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 { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "~/components/Icons"; import { RadioGroup } from "~/components/ui/radio-group"; import { Spinner } from "~/components/ui/spinner"; import { toggleVariants } from "~/components/ui/toggle"; import { cn } from "~/lib/utils"; import { useOpenPrLink } from "~/lib/openPullRequestLink"; import { buildMenuItems, formatGitActionElapsed, GIT_ACTION_SUCCESS_VISIBLE_MS, type GitActionProgressPresentation, type GitActionIconName, type GitActionMenuItem, type GitQuickAction, type DefaultBranchConfirmableAction, requiresDefaultBranchConfirmation, resolveDefaultBranchActionDialogCopy, resolveGitActionProgressPresentation, resolveGitActionResultToastTiming, resolveLiveThreadBranchUpdate, resolveThreadBranchMetadataPatch, resolveQuickAction, resolveThreadBranchUpdate, } from "./GitActionsControl.logic"; import { AnimatedHeight } from "./AnimatedHeight"; 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 { Group, GroupSeparator } from "~/components/ui/group"; import { Input } from "~/components/ui/input"; import { Menu, MenuItem, 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 "~/lib/sourceControlActions"; import { useThreadShell } from "~/state/entities"; 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 } from "~/state/vcs"; import { randomUUID } from "~/lib/utils"; import { resolvePathLinkTarget } from "~/terminal-links"; import { type DraftId, useComposerDraftStore } from "~/composerDraftStore"; import { THREAD_DETAILS_PANEL_CHEVRON_CLASS, THREAD_DETAILS_PANEL_ICON_CLASS, THREAD_DETAILS_PANEL_ROW_POPUP_CLASS, THREAD_DETAILS_PANEL_ROW_CLASS, THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS, THREAD_DETAILS_PANEL_SPLIT_PRIMARY_CLASS, THREAD_DETAILS_PANEL_SPLIT_SECONDARY_CLASS, THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS, } from "./chat/threadDetailsPanelStyles"; import { getSourceControlPresentation } from "~/sourceControlPresentation"; import { useOpenLink } from "~/browser/useOpenLink"; interface GitActionsControlProps { gitCwd: string | null; activeThreadRef: ScopedThreadRef | null; draftId?: DraftId; /** * Opens the thread's own change request beside it. Absent when the thread has no project to * place it against, in which case it still opens in the browser. */ onOpenPullRequest?: ((number: number) => void) | undefined; displayMode?: "toolbar" | "panel"; onOpenChanges?: () => void; } interface PendingDefaultBranchAction { action: DefaultBranchConfirmableAction; branchName: string; includesCommit: boolean; commitMessage?: string; onConfirmed?: () => void; filePaths?: string[]; } type PublishProviderKind = Extract< SourceControlProviderKind, "github" | "gitlab" | "bitbucket" | "azure-devops" >; type GitActionToastId = ReturnType<typeof toastManager.add>; 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; } const GIT_STATUS_WINDOW_REFRESH_DEBOUNCE_MS = 250; type RefreshVcsStatus = (target: { readonly environmentId: ScopedThreadRef["environmentId"]; readonly input: { readonly cwd: string }; }) => Promise<unknown>; 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 = [ { value: "github", label: "GitHub", description: "github.com", host: "github.com", pathPlaceholder: "owner/repo", Icon: GitHubIcon, }, { value: "gitlab", label: "GitLab", description: "gitlab.com", host: "gitlab.com", pathPlaceholder: "group/project", Icon: GitLabIcon, }, { value: "bitbucket", label: "Bitbucket", description: "bitbucket.org", host: "bitbucket.org", pathPlaceholder: "workspace/repository", Icon: BitbucketIcon, }, { value: "azure-devops", label: "Azure DevOps", description: "dev.azure.com", host: "dev.azure.com", pathPlaceholder: "project/repository", Icon: AzureDevOpsIcon, }, ] as const satisfies ReadonlyArray<{ readonly value: PublishProviderKind; readonly label: string; readonly description: string; readonly host: string; readonly pathPlaceholder: string; readonly Icon: typeof GitHubIcon; }>; function publishProviderOption(provider: PublishProviderKind) { return ( PUBLISH_PROVIDER_OPTIONS.find((option) => option.value === provider) ?? PUBLISH_PROVIDER_OPTIONS[0] ); } function isPublishProviderKind( provider: SourceControlProviderKind, ): provider is PublishProviderKind { return PUBLISH_PROVIDER_OPTIONS.some((option) => option.value === provider); } function getPublishProviderReadiness(input: { provider: PublishProviderKind; sourceControlProviders: ReadonlyArray<SourceControlProviderDiscoveryItem>; }): { 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: checkout a refName 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: checkout a refName before creating a ${terminology.singular}.`; } if (hasChanges) { return `Commit local changes before creating a ${terminology.singular}.`; } if (!gitStatus.hasUpstream && !hasPrimaryRemote) { return `Add an "origin" remote before creating a ${terminology.singular}.`; } if (!isAhead) { 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<typeof getSourceControlPresentation>["Icon"]; }) { if (icon === "commit") return <GitCommitIcon />; if (icon === "push") return <CloudUploadIcon />; return <SourceControlIcon />; } function GitQuickActionIcon({ quickAction, SourceControlIcon, className = "size-3.5", }: { quickAction: GitQuickAction; SourceControlIcon: ReturnType<typeof getSourceControlPresentation>["Icon"]; className?: string; }) { const iconClassName = className; if (quickAction.kind === "open_publish") return <CloudUploadIcon className={iconClassName} />; if (quickAction.kind === "run_pull") return <CloudDownloadIcon className={iconClassName} />; if (quickAction.kind === "run_action") { if (quickAction.action === "commit") return <GitCommitIcon className={iconClassName} />; if (quickAction.action === "push" || quickAction.action === "commit_push") { return <CloudUploadIcon className={iconClassName} />; } return <SourceControlIcon className={iconClassName} />; } if (quickAction.label === "Commit") return <GitCommitIcon className={iconClassName} />; if (quickAction.label === "Push") return <CloudUploadIcon className={iconClassName} />; return <InfoIcon className={iconClassName} />; } function GitActionElapsedTime({ startedAtMs, className, }: { startedAtMs: number | null; className?: string; }) { const [nowMs, setNowMs] = useState(() => Date.now()); useEffect(() => { if (startedAtMs === null) { return; } const intervalId = window.setInterval(() => { setNowMs(Date.now()); }, 1_000); return () => window.clearInterval(intervalId); }, [startedAtMs]); const elapsed = formatGitActionElapsed(startedAtMs, nowMs); if (!elapsed) { return null; } return <p className={className}>{elapsed}</p>; } function GitActionProgressButtonContent({ progress, isPanel, }: { progress: GitActionProgressPresentation; isPanel: boolean; }) { const hasOutput = progress.output !== null; return ( <div aria-atomic="false" aria-live="polite" className={cn( "grid min-w-0 flex-1 items-center", // Pin the title row to the button's minimum content height (min-height // minus vertical padding and border) so revealing the output row // extends the button downward without re-centering — the title must // not shift. No row gap: the collapsed output row must contribute zero // height so the single-line running button matches the static button // exactly. The panel column gap matches the static row's icon-to-label // distance (gap-2.5 plus the label's ml-0.5). In the panel the elapsed // counter renders outside the button (in the menu-chevron slot), so // there is no trailing column. isPanel ? "grid-cols-[auto_minmax(0,1fr)] grid-rows-[1.75rem] gap-x-3" : "grid-cols-[auto_minmax(0,1fr)_auto] grid-rows-[1.25rem] gap-x-2.5 sm:grid-rows-[1rem]", )} role="status" > <Spinner aria-hidden="true" className="row-start-1 -mx-0.5 size-4 shrink-0 text-muted-foreground" /> <p className="row-start-1 min-w-0 truncate text-left">{progress.status}</p> {!isPanel ? ( <GitActionElapsedTime startedAtMs={progress.startedAtMs} className="row-start-1 text-[11px] font-normal tabular-nums text-muted-foreground" /> ) : null} <div className={cn( "col-start-2 grid min-w-0 transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none", !isPanel && "col-span-2", hasOutput ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0", )} > <div className="min-h-0 overflow-hidden"> <Tooltip> <TooltipTrigger render={ <p className="truncate pt-0.5 text-left text-[11px] font-normal text-muted-foreground" /> } > {progress.output} </TooltipTrigger> <TooltipPopup side="bottom" className="max-w-96 break-words"> {progress.output} </TooltipPopup> </Tooltip> </div> </div> </div> ); } function GitActionSuccessButtonContent({ success }: { success: InlineGitActionSuccess }) { const hasDescription = success.description !== null; return ( <div aria-live="polite" className="grid min-w-0 flex-1 grid-cols-[auto_minmax(0,1fr)] grid-rows-[1.75rem] items-center gap-x-3" role="status" > <CheckIcon aria-hidden="true" className="size-3.5 shrink-0 text-success" /> <p className="min-w-0 truncate text-left">{success.title}</p> <div className={cn( "col-start-2 grid min-w-0 transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none", hasDescription ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0", )} > <div className="min-h-0 overflow-hidden"> <Tooltip> <TooltipTrigger render={ <p className="truncate pt-0.5 text-left text-[11px] font-normal text-muted-foreground" /> } > {success.description} </TooltipTrigger> <TooltipPopup side="bottom" className="max-w-96 break-words"> {success.description} </TooltipPopup> </Tooltip> </div> </div> </div> ); } interface PublishRepositoryDialogProps {