authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-02 20:04:54-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-02 20:46:05-08:00
logf11764c517732b1c02057f28ff1a10f03f8157da
treec75cf72a7089bf042f345cdf584dacef408b85d7
parent77c81b7468003e7b7d56fd4dd123c499fdf3e073

Fix PR CTAs and quick action after push

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 53 insertions(+), 10 deletions(-)

apps/web/src/components/GitActionsControl.logic.test.ts+21-4
...@@ -250,15 +250,20 @@ describe("when: branch is clean, ahead, and has no open PR", () => {...@@ -250,15 +250,20 @@ describe("when: branch is clean, ahead, and has no open PR", () => {
250});250});
251251
252describe("when: branch is clean, up to date, and has no open PR", () => {252describe("when: branch is clean, up to date, and has no open PR", () => {
253 it("resolveQuickAction returns disabled no-action state", () => {253 it("resolveQuickAction offers create PR", () => {
254 const quick = resolveQuickAction(254 const quick = resolveQuickAction(
255 status({ aheadCount: 0, behindCount: 0, hasWorkingTreeChanges: false, pr: null }),255 status({ aheadCount: 0, behindCount: 0, hasWorkingTreeChanges: false, pr: null }),
256 false,256 false,
257 );257 );
258 assert.deepInclude(quick, { kind: "show_hint", label: "Commit", disabled: true });258 assert.deepInclude(quick, {
259 kind: "run_action",
260 action: "commit_push_pr",
261 label: "Create PR",
262 disabled: false,
263 });
259 });264 });
260265
261 it("buildMenuItems disables commit, push, and create PR", () => {266 it("buildMenuItems enables create PR and keeps commit/push disabled", () => {
262 const items = buildMenuItems(status({ aheadCount: 0, behindCount: 0, pr: null }), false);267 const items = buildMenuItems(status({ aheadCount: 0, behindCount: 0, pr: null }), false);
263 assert.deepEqual(items, [268 assert.deepEqual(items, [
264 {269 {
...@@ -280,13 +285,25 @@ describe("when: branch is clean, up to date, and has no open PR", () => {...@@ -280,13 +285,25 @@ describe("when: branch is clean, up to date, and has no open PR", () => {
280 {285 {
281 id: "pr",286 id: "pr",
282 label: "Create PR",287 label: "Create PR",
283 disabled: true,288 disabled: false,
284 icon: "pr",289 icon: "pr",
285 kind: "open_dialog",290 kind: "open_dialog",
286 dialogAction: "create_pr",291 dialogAction: "create_pr",
287 },292 },
288 ]);293 ]);
289 });294 });
295
296 it("buildMenuItems keeps create PR disabled on default branch", () => {
297 const items = buildMenuItems(status({ branch: "main", aheadCount: 0, behindCount: 0 }), false, true);
298 assert.deepEqual(items[2], {
299 id: "pr",
300 label: "Create PR",
301 disabled: true,
302 icon: "pr",
303 kind: "open_dialog",
304 dialogAction: "create_pr",
305 });
306 });
290});307});
291308
292describe("when: branch is behind upstream", () => {309describe("when: branch is behind upstream", () => {
apps/web/src/components/GitActionsControl.logic.ts+13-2
...@@ -113,6 +113,7 @@ export function summarizeGitResult(result: GitRunStackedActionResult): {...@@ -113,6 +113,7 @@ export function summarizeGitResult(result: GitRunStackedActionResult): {
113export function buildMenuItems(113export function buildMenuItems(
114 gitStatus: GitStatusResult | null,114 gitStatus: GitStatusResult | null,
115 isBusy: boolean,115 isBusy: boolean,
116 isDefaultBranch = false,
116): GitActionMenuItem[] {117): GitActionMenuItem[] {
117 if (!gitStatus) return [];118 if (!gitStatus) return [];
118119
...@@ -127,8 +128,9 @@ export function buildMenuItems(...@@ -127,8 +128,9 @@ export function buildMenuItems(
127 hasBranch &&128 hasBranch &&
128 !hasChanges &&129 !hasChanges &&
129 !hasOpenPr &&130 !hasOpenPr &&
130 gitStatus.aheadCount > 0 &&131 !isBehind &&
131 !isBehind;132 !isDefaultBranch &&
133 (gitStatus.aheadCount > 0 || gitStatus.hasUpstream);
132 const canOpenPr = !isBusy && hasOpenPr;134 const canOpenPr = !isBusy && hasOpenPr;
133135
134 return [136 return [
...@@ -265,6 +267,15 @@ export function resolveQuickAction(...@@ -265,6 +267,15 @@ export function resolveQuickAction(
265 };267 };
266 }268 }
267269
270 if (!hasOpenPr && !isDefaultBranch && gitStatus.hasUpstream) {
271 return {
272 label: "Create PR",
273 disabled: false,
274 kind: "run_action",
275 action: "commit_push_pr",
276 };
277 }
278
268 if (hasOpenPr && gitStatus.hasUpstream) {279 if (hasOpenPr && gitStatus.hasUpstream) {
269 return { label: "Open PR", disabled: false, kind: "open_pr" };280 return { label: "Open PR", disabled: false, kind: "open_pr" };
270 }281 }
apps/web/src/components/GitActionsControl.tsx+19-4
...@@ -180,10 +180,19 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -180,10 +180,19 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
180 const current = branchList?.branches.find((branch) => branch.name === branchName);180 const current = branchList?.branches.find((branch) => branch.name === branchName);
181 return current?.isDefault ?? (branchName === "main" || branchName === "master");181 return current?.isDefault ?? (branchName === "main" || branchName === "master");
182 }, [branchList?.branches, gitStatusForActions?.branch]);182 }, [branchList?.branches, gitStatusForActions?.branch]);
183 const isDefaultBranchName = useCallback(
184 (branchName: string | null | undefined) => {
185 if (!branchName) return false;
186 const defaultBranchName = branchList?.branches.find((branch) => branch.isDefault)?.name;
187 if (defaultBranchName) return branchName === defaultBranchName;
188 return branchName === "main" || branchName === "master";
189 },
190 [branchList?.branches],
191 );
183192
184 const gitActionMenuItems = useMemo(193 const gitActionMenuItems = useMemo(
185 () => buildMenuItems(gitStatusForActions, isGitActionRunning),194 () => buildMenuItems(gitStatusForActions, isGitActionRunning, isDefaultBranch),
186 [gitStatusForActions, isGitActionRunning],195 [gitStatusForActions, isGitActionRunning, isDefaultBranch],
187 );196 );
188 const quickAction = useMemo(197 const quickAction = useMemo(
189 () => resolveQuickAction(gitStatusForActions, isGitActionRunning, isDefaultBranch),198 () => resolveQuickAction(gitStatusForActions, isGitActionRunning, isDefaultBranch),
...@@ -237,6 +246,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -237,6 +246,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
237 onConfirmed,246 onConfirmed,
238 skipDefaultBranchPrompt = false,247 skipDefaultBranchPrompt = false,
239 featureBranch = false,248 featureBranch = false,
249 isDefaultBranchOverride,
240 }: {250 }: {
241 action: GitStackedAction;251 action: GitStackedAction;
242 commitMessage?: string;252 commitMessage?: string;
...@@ -244,10 +254,12 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -244,10 +254,12 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
244 onConfirmed?: () => void;254 onConfirmed?: () => void;
245 skipDefaultBranchPrompt?: boolean;255 skipDefaultBranchPrompt?: boolean;
246 featureBranch?: boolean;256 featureBranch?: boolean;
257 isDefaultBranchOverride?: boolean;
247 }) => {258 }) => {
248 const actionStatus = gitStatusForActions;259 const actionStatus = gitStatusForActions;
249 const actionIsDefaultBranch = featureBranch ? false : isDefaultBranch;
250 const actionBranch = actionStatus?.branch ?? null;260 const actionBranch = actionStatus?.branch ?? null;
261 const actionIsDefaultBranch =
262 isDefaultBranchOverride ?? (featureBranch ? false : isDefaultBranchName(actionBranch));
251 const includesCommit =263 const includesCommit =
252 !forcePushOnlyProgress && (action === "commit" || !!actionStatus?.hasWorkingTreeChanges);264 !forcePushOnlyProgress && (action === "commit" || !!actionStatus?.hasWorkingTreeChanges);
253 if (265 if (
...@@ -348,6 +360,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -348,6 +360,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
348 forcePushOnlyProgress: true,360 forcePushOnlyProgress: true,
349 onConfirmed: closeResultToast,361 onConfirmed: closeResultToast,
350 ...(featureBranch ? { skipDefaultBranchPrompt: true } : {}),362 ...(featureBranch ? { skipDefaultBranchPrompt: true } : {}),
363 ...(featureBranch ? { isDefaultBranchOverride: false } : {}),
351 });364 });
352 },365 },
353 },366 },
...@@ -374,6 +387,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -374,6 +387,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
374 action: "commit_push_pr",387 action: "commit_push_pr",
375 forcePushOnlyProgress: true,388 forcePushOnlyProgress: true,
376 ...(featureBranch ? { skipDefaultBranchPrompt: true } : {}),389 ...(featureBranch ? { skipDefaultBranchPrompt: true } : {}),
390 ...(featureBranch ? { isDefaultBranchOverride: false } : {}),
377 });391 });
378 },392 },
379 },393 },
...@@ -392,7 +406,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -392,7 +406,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
392 },406 },
393407
394 [408 [
395 isDefaultBranch,409 isDefaultBranchName,
396 runImmediateGitActionMutation,410 runImmediateGitActionMutation,
397 setPendingDefaultBranchAction,411 setPendingDefaultBranchAction,
398 threadToastData,412 threadToastData,
...@@ -410,6 +424,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -410,6 +424,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
410 forcePushOnlyProgress,424 forcePushOnlyProgress,
411 ...(onConfirmed ? { onConfirmed } : {}),425 ...(onConfirmed ? { onConfirmed } : {}),
412 skipDefaultBranchPrompt: true,426 skipDefaultBranchPrompt: true,
427 isDefaultBranchOverride: true,
413 });428 });
414 }, [pendingDefaultBranchAction, runGitActionWithToast]);429 }, [pendingDefaultBranchAction, runGitActionWithToast]);
415430