Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/sharedSettingsFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ describe("sharedSettingsFile", () => {
autoShowTerminalPanel: true,
gitReviewMode: "panel",
prCreateMode: "dialog",
commitDefaultAction: "commit-push",
providerConfigs: {},
lastPresentationModeByAgent: {},
lastUsedProjectDirs: {},
Expand Down Expand Up @@ -156,6 +157,7 @@ describe("sharedSettingsFile", () => {
autoShowTerminalPanel: true,
gitReviewMode: "panel",
prCreateMode: "dialog",
commitDefaultAction: "commit-push",
providerConfigs: {},
lastPresentationModeByAgent: {},
lastUsedProjectDirs: {},
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/state/sharedSettingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import type {
GitReviewMode,
AgentInstanceConfig,
CommitDefaultAction,
InstalledAcpRegistryAgent,
NewThreadMode,
NotificationFilter,
Expand Down Expand Up @@ -55,6 +56,7 @@ interface SharedSettingsState extends SharedSettings {
setAutoShowTerminalPanel: (value: boolean) => void;
setGitReviewMode: (value: GitReviewMode) => void;
setPrCreateMode: (value: PrCreateMode) => void;
setCommitDefaultAction: (value: CommitDefaultAction) => void;
setEditorLspEnabled: (value: boolean) => void;
setSearchUseIgnoreFiles: (value: boolean) => void;
setSearchExclude: (value: Record<string, boolean>) => void;
Expand Down Expand Up @@ -318,6 +320,11 @@ export const useSharedSettings = create<SharedSettingsState>()((set, get) => ({
set({ prCreateMode });
persistSettings(selectSharedSettings(get()));
},
setCommitDefaultAction: (commitDefaultAction) => {
if (get().commitDefaultAction === commitDefaultAction) return;
set({ commitDefaultAction });
persistSettings(selectSharedSettings(get()));
},
setEditorLspEnabled: (editorLspEnabled) => {
set({ editorLspEnabled });
persistSettings(selectSharedSettings(get()));
Expand Down Expand Up @@ -557,6 +564,7 @@ function selectSharedSettings(state: SharedSettingsState): SharedSettingsInput {
autoShowTerminalPanel: state.autoShowTerminalPanel,
gitReviewMode: state.gitReviewMode,
prCreateMode: state.prCreateMode,
commitDefaultAction: state.commitDefaultAction,
providerConfigs: state.providerConfigs,
lastPresentationModeByAgent: state.lastPresentationModeByAgent,
lastUsedProjectDirs: state.lastUsedProjectDirs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ vi.mock("@heroui/react", () => {
Label: (props: { children: ReactNode }) => <span>{props.children}</span>,
ListBox,
Select,
Separator: () => <span />,

Surface: Wrapper,
Tooltip,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
PanelLeft,
PanelLeftClose,
} from "lucide-react";
import { Button, ButtonGroup, Dropdown, Label, Modal } from "@heroui/react";
import type { GitBranchInfo, GitStatusResult, Project } from "@/shared/contracts";
import { Button, ButtonGroup, Dropdown, Label, Modal, Separator } from "@heroui/react";
import type { GitBranchInfo, GitStatusResult, PrCreateMode, Project } from "@/shared/contracts";
import { getProjectAgentStatuses } from "@/shared/agentStatus";
import { useAgentStatusesStore } from "@/renderer/state/agentStatusesStore";
import { useGitStore } from "@/renderer/state/gitStore";
Expand Down Expand Up @@ -102,6 +102,9 @@ export function GitReviewSidebar(props: {
isWsl ? s.wslCommitGenProvider : s.commitGenProvider,
);
const prCreateMode = useSharedSettings((s) => s.prCreateMode);
const setPrCreateMode = useSharedSettings((s) => s.setPrCreateMode);
const commitDefaultAction = useSharedSettings((s) => s.commitDefaultAction);
const setCommitDefaultAction = useSharedSettings((s) => s.setCommitDefaultAction);

// Treat "unknown" as "might be GitHub" — covers SSH host aliases where the
// remote URL hostname doesn't contain "github" but resolves to github.com.
Expand Down Expand Up @@ -160,6 +163,7 @@ export function GitReviewSidebar(props: {
handlePullFromSource,
handleAbortMerge,
handleCreatePr,
handleCommitAndCreatePr,
handleMergePr,
handleClosePr,
handleMarkPrReady,
Expand Down Expand Up @@ -208,9 +212,15 @@ export function GitReviewSidebar(props: {
);
const showPullFromSource = Boolean(effectiveBranch && sourceBranch && sourceAhead > 0);
const isPushed = hasTracking && ahead === 0;
const showCreatePrButton = Boolean(
showPrSection && ghAvailable && isPushed && sourceBranch && (!prState || prState === "closed"),
// Shared PR eligibility: a GitHub repo with a target branch and no open PR.
const prEligible = Boolean(
showPrSection && ghAvailable && sourceBranch && (!prState || prState === "closed"),
);
const showCreatePrButton = prEligible && isPushed;
// Whether the one-click "Commit & Create PR" action is offered. Unlike
// showCreatePrButton this does NOT require an already-pushed branch (it only
// needs a remote) — the combined action pushes as part of its flow.
const canCreatePr = prEligible && hasRemote;
const [createPrModalOpen, setCreatePrModalOpen] = useState(false);
// In "auto" mode the Create PR button skips the dialog: it auto-generates the
// title/body and creates the PR in one click (handleCreatePr handles the
Expand All @@ -220,10 +230,19 @@ export function GitReviewSidebar(props: {
// derived once here and shared.
const isAutoPrMode = prCreateMode === "auto";
const createPrPending = isAutoPrMode && prLoading;
const onCreatePrPress = () => {
if (isAutoPrMode) void handleCreatePr(false);
const runPrMode = (prMode: PrCreateMode) => {
if (prMode === "auto") void handleCreatePr(false);
else setCreatePrModalOpen(true);
};
const onCreatePrPress = () => runPrMode(prCreateMode);
// Picking the other mode from the split-button menu both runs it and makes
// it the sticky default (the same field the Git settings select drives).
const selectPrMode = (prMode: PrCreateMode) => {
setPrCreateMode(prMode);
runPrMode(prMode);
};
const altPrMode: PrCreateMode = isAutoPrMode ? "dialog" : "auto";
const altPrModeLabel = isAutoPrMode ? "Create PR…" : "Create PR (Auto)";
const createPrButtonContent = (
<>
{createPrPending ? <PixelLoader size="xs" /> : <GitPullRequest className="size-3.5" />}
Expand Down Expand Up @@ -441,14 +460,19 @@ export function GitReviewSidebar(props: {
setCommitMessage={setCommitMessage}
canCommitStaged={canCommitStaged}
canGenerateMessage={canGenerateMessage}
canCreatePr={canCreatePr}
commitDefaultAction={commitDefaultAction}
setCommitDefaultAction={setCommitDefaultAction}
isCommitting={isCommitting}
isGenerating={isGenerating}
isSyncing={isSyncing}
prLoading={prLoading}
isPullingFromSource={isPullingFromSource}
showPullFromSource={showPullFromSource}
sourceBranch={sourceBranch}
sourceAhead={sourceAhead}
handleCommit={handleCommit}
handleCommitAndCreatePr={handleCommitAndCreatePr}
handleGenerateMessage={handleGenerateMessage}
handleSyncOrPush={handleSyncOrPush}
handleSyncAction={handleSyncAction}
Expand All @@ -472,61 +496,60 @@ export function GitReviewSidebar(props: {

{showCreatePrButton && (
<GitReviewSection>
{showMergeActions ? (
<ButtonGroup className="w-full">
<Button
variant="tertiary"
className="flex-1"
isDisabled={createPrPending}
isPending={createPrPending}
onPress={onCreatePrPress}
>
{createPrButtonContent}
</Button>
<Dropdown>
<Button
isIconOnly
variant="tertiary"
aria-label="More pull request options"
isDisabled={isMerging}
>
<ButtonGroup.Separator />
<ChevronDown className="size-3.5" />
</Button>
<Dropdown.Popover placement="top end">
<Dropdown.Menu
aria-label="Pull request options"
onAction={(key) => {
if (key === "merge-only") void handleMergeOnly();
if (key === "merge-and-remove") void handleMergeAndRemove();
}}
>
<Dropdown.Item id="merge-only" textValue="Merge Worktree">
<GitMerge className="size-3.5" />
<Label>Merge Worktree</Label>
</Dropdown.Item>
<Dropdown.Item
id="merge-and-remove"
textValue="Merge Locally & Remove Worktree"
>
<GitMerge className="size-3.5" />
<Label>Merge Locally & Remove Worktree</Label>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown.Popover>
</Dropdown>
</ButtonGroup>
) : (
<ButtonGroup className="w-full">
<Button
variant="tertiary"
className="w-full"
className="flex-1"
isDisabled={createPrPending}
isPending={createPrPending}
onPress={onCreatePrPress}
>
{createPrButtonContent}
</Button>
)}
<Dropdown>
<Button
isIconOnly
variant="tertiary"
aria-label="More pull request options"
isDisabled={createPrPending || isMerging}
>
<ButtonGroup.Separator />
<ChevronDown className="size-3.5" />
</Button>
<Dropdown.Popover placement="top end">
<Dropdown.Menu
aria-label="Pull request options"
onAction={(key) => {
if (key === "pr-auto") selectPrMode("auto");
else if (key === "pr-dialog") selectPrMode("dialog");
else if (key === "merge-only") void handleMergeOnly();
else if (key === "merge-and-remove") void handleMergeAndRemove();
}}
>
<Dropdown.Item id={`pr-${altPrMode}`} textValue={altPrModeLabel}>
<GitPullRequest className="size-3.5" />
<Label>{altPrModeLabel}</Label>
</Dropdown.Item>
{showMergeActions ? (
<>
<Separator />
<Dropdown.Item id="merge-only" textValue="Merge Worktree">
<GitMerge className="size-3.5" />
<Label>Merge Worktree</Label>
</Dropdown.Item>
<Dropdown.Item
id="merge-and-remove"
textValue="Merge Locally & Remove Worktree"
>
<GitMerge className="size-3.5" />
<Label>Merge Locally & Remove Worktree</Label>
</Dropdown.Item>
</>
) : null}
</Dropdown.Menu>
</Dropdown.Popover>
</Dropdown>
</ButtonGroup>
</GitReviewSection>
)}

Expand Down
Loading