authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-02 20:17:47-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-02 20:46:05-08:00
logebe44d03a5fa6c34fdae2a67d53ef2ed6ba32737
treed97a56df23267689fba8634e231f913b06a3bfac
parentdcd2d50768363e61fdf556e81abada8aa54996de

Revert git action button logic toward main behavior

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

3 files changed, 46 insertions(+), 69 deletions(-)

apps/web/src/components/GitActionsControl.logic.test.ts+7-24
...@@ -250,20 +250,15 @@ describe("when: branch is clean, ahead, and has no open PR", () => {...@@ -250,20 +250,15 @@ 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 offers create PR", () => {253 it("resolveQuickAction returns disabled no-action state", () => {
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, {258 assert.deepInclude(quick, { kind: "show_hint", label: "Commit", disabled: true });
259 kind: "run_action",
260 action: "commit_push_pr",
261 label: "Create PR",
262 disabled: false,
263 });
264 });259 });
265260
266 it("buildMenuItems enables create PR and keeps commit/push disabled", () => {261 it("buildMenuItems disables commit, push, and create PR", () => {
267 const items = buildMenuItems(status({ aheadCount: 0, behindCount: 0, pr: null }), false);262 const items = buildMenuItems(status({ aheadCount: 0, behindCount: 0, pr: null }), false);
268 assert.deepEqual(items, [263 assert.deepEqual(items, [
269 {264 {
...@@ -285,25 +280,13 @@ describe("when: branch is clean, up to date, and has no open PR", () => {...@@ -285,25 +280,13 @@ describe("when: branch is clean, up to date, and has no open PR", () => {
285 {280 {
286 id: "pr",281 id: "pr",
287 label: "Create PR",282 label: "Create PR",
288 disabled: false,283 disabled: true,
289 icon: "pr",284 icon: "pr",
290 kind: "open_dialog",285 kind: "open_dialog",
291 dialogAction: "create_pr",286 dialogAction: "create_pr",
292 },287 },
293 ]);288 ]);
294 });289 });
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 });
307});290});
308291
309describe("when: branch is behind upstream", () => {292describe("when: branch is behind upstream", () => {
...@@ -365,7 +348,7 @@ describe("when: working tree has local changes", () => {...@@ -365,7 +348,7 @@ describe("when: working tree has local changes", () => {
365 });348 });
366 });349 });
367350
368 it("resolveQuickAction keeps full commit/push/pr flow on non-default branches even when PR exists", () => {351 it("resolveQuickAction returns commit and push when open PR exists", () => {
369 const quick = resolveQuickAction(352 const quick = resolveQuickAction(
370 status({353 status({
371 hasWorkingTreeChanges: true,354 hasWorkingTreeChanges: true,
...@@ -382,8 +365,8 @@ describe("when: working tree has local changes", () => {...@@ -382,8 +365,8 @@ describe("when: working tree has local changes", () => {
382 );365 );
383 assert.deepInclude(quick, {366 assert.deepInclude(quick, {
384 kind: "run_action",367 kind: "run_action",
385 action: "commit_push_pr",368 action: "commit_push",
386 label: "Commit, push & create PR",369 label: "Commit & push",
387 });370 });
388 });371 });
389372
apps/web/src/components/GitActionsControl.logic.ts+3-14
...@@ -113,7 +113,6 @@ export function summarizeGitResult(result: GitRunStackedActionResult): {...@@ -113,7 +113,6 @@ 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,
117): GitActionMenuItem[] {116): GitActionMenuItem[] {
118 if (!gitStatus) return [];117 if (!gitStatus) return [];
119118
...@@ -128,9 +127,8 @@ export function buildMenuItems(...@@ -128,9 +127,8 @@ export function buildMenuItems(
128 hasBranch &&127 hasBranch &&
129 !hasChanges &&128 !hasChanges &&
130 !hasOpenPr &&129 !hasOpenPr &&
131 !isBehind &&130 gitStatus.aheadCount > 0 &&
132 !isDefaultBranch &&131 !isBehind;
133 (gitStatus.aheadCount > 0 || gitStatus.hasUpstream);
134 const canOpenPr = !isBusy && hasOpenPr;132 const canOpenPr = !isBusy && hasOpenPr;
135133
136 return [134 return [
...@@ -204,7 +202,7 @@ export function resolveQuickAction(...@@ -204,7 +202,7 @@ export function resolveQuickAction(
204 }202 }
205203
206 if (hasChanges) {204 if (hasChanges) {
207 if (isDefaultBranch) {205 if (hasOpenPr || isDefaultBranch) {
208 return { label: "Commit & push", disabled: false, kind: "run_action", action: "commit_push" };206 return { label: "Commit & push", disabled: false, kind: "run_action", action: "commit_push" };
209 }207 }
210 return {208 return {
...@@ -267,15 +265,6 @@ export function resolveQuickAction(...@@ -267,15 +265,6 @@ export function resolveQuickAction(
267 };265 };
268 }266 }
269267
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
279 if (hasOpenPr && gitStatus.hasUpstream) {268 if (hasOpenPr && gitStatus.hasUpstream) {
280 return { label: "Open PR", disabled: false, kind: "open_pr" };269 return { label: "Open PR", disabled: false, kind: "open_pr" };
281 }270 }
apps/web/src/components/GitActionsControl.tsx+36-31
...@@ -57,6 +57,8 @@ interface PendingDefaultBranchAction {...@@ -57,6 +57,8 @@ interface PendingDefaultBranchAction {
57 onConfirmed?: () => void;57 onConfirmed?: () => void;
58}58}
5959
60type GitActionToastId = ReturnType<typeof toastManager.add>;
61
60function getMenuActionDisabledReason(62function getMenuActionDisabledReason(
61 item: GitActionMenuItem,63 item: GitActionMenuItem,
62 gitStatus: GitStatusResult | null,64 gitStatus: GitStatusResult | null,
...@@ -180,19 +182,10 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -180,19 +182,10 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
180 const current = branchList?.branches.find((branch) => branch.name === branchName);182 const current = branchList?.branches.find((branch) => branch.name === branchName);
181 return current?.isDefault ?? (branchName === "main" || branchName === "master");183 return current?.isDefault ?? (branchName === "main" || branchName === "master");
182 }, [branchList?.branches, gitStatusForActions?.branch]);184 }, [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 );
192185
193 const gitActionMenuItems = useMemo(186 const gitActionMenuItems = useMemo(
194 () => buildMenuItems(gitStatusForActions, isGitActionRunning, isDefaultBranch),187 () => buildMenuItems(gitStatusForActions, isGitActionRunning),
195 [gitStatusForActions, isGitActionRunning, isDefaultBranch],188 [gitStatusForActions, isGitActionRunning],
196 );189 );
197 const quickAction = useMemo(190 const quickAction = useMemo(
198 () => resolveQuickAction(gitStatusForActions, isGitActionRunning, isDefaultBranch),191 () => resolveQuickAction(gitStatusForActions, isGitActionRunning, isDefaultBranch),
...@@ -245,26 +238,28 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -245,26 +238,28 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
245 forcePushOnlyProgress = false,238 forcePushOnlyProgress = false,
246 onConfirmed,239 onConfirmed,
247 skipDefaultBranchPrompt = false,240 skipDefaultBranchPrompt = false,
241 statusOverride,
248 featureBranch = false,242 featureBranch = false,
249 isDefaultBranchOverride,243 isDefaultBranchOverride,
244 progressToastId,
250 }: {245 }: {
251 action: GitStackedAction;246 action: GitStackedAction;
252 commitMessage?: string;247 commitMessage?: string;
253 forcePushOnlyProgress?: boolean;248 forcePushOnlyProgress?: boolean;
254 onConfirmed?: () => void;249 onConfirmed?: () => void;
255 skipDefaultBranchPrompt?: boolean;250 skipDefaultBranchPrompt?: boolean;
251 statusOverride?: GitStatusResult | null;
256 featureBranch?: boolean;252 featureBranch?: boolean;
257 isDefaultBranchOverride?: boolean;253 isDefaultBranchOverride?: boolean;
254 progressToastId?: GitActionToastId;
258 }) => {255 }) => {
259 const actionStatus = gitStatusForActions;256 const actionStatus = statusOverride ?? gitStatusForActions;
260 const actionBranch = actionStatus?.branch ?? null;257 const actionBranch = actionStatus?.branch ?? null;
261 const actionIsDefaultBranch =258 const actionIsDefaultBranch = isDefaultBranchOverride ?? (featureBranch ? false : isDefaultBranch);
262 isDefaultBranchOverride ?? (featureBranch ? false : isDefaultBranchName(actionBranch));
263 const includesCommit =259 const includesCommit =
264 !forcePushOnlyProgress && (action === "commit" || !!actionStatus?.hasWorkingTreeChanges);260 !forcePushOnlyProgress && (action === "commit" || !!actionStatus?.hasWorkingTreeChanges);
265 if (261 if (
266 !skipDefaultBranchPrompt &&262 !skipDefaultBranchPrompt &&
267 !featureBranch &&
268 requiresDefaultBranchConfirmation(action, actionIsDefaultBranch) &&263 requiresDefaultBranchConfirmation(action, actionIsDefaultBranch) &&
269 actionBranch264 actionBranch
270 ) {265 ) {
...@@ -292,17 +287,28 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -292,17 +287,28 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
292 featureBranch,287 featureBranch,
293 ...(pushTarget ? { pushTarget } : {}),288 ...(pushTarget ? { pushTarget } : {}),
294 });289 });
295 const progressToastId = toastManager.add({290 const resolvedProgressToastId =
296 type: "loading",291 progressToastId ??
297 title: progressStages[0] ?? "Running git action...",292 toastManager.add({
298 timeout: 0,293 type: "loading",
299 data: threadToastData,294 title: progressStages[0] ?? "Running git action...",
300 });295 timeout: 0,
296 data: threadToastData,
297 });
298
299 if (progressToastId) {
300 toastManager.update(progressToastId, {
301 type: "loading",
302 title: progressStages[0] ?? "Running git action...",
303 timeout: 0,
304 data: threadToastData,
305 });
306 }
301307
302 let stageIndex = 0;308 let stageIndex = 0;
303 const stageInterval = setInterval(() => {309 const stageInterval = setInterval(() => {
304 stageIndex = Math.min(stageIndex + 1, progressStages.length - 1);310 stageIndex = Math.min(stageIndex + 1, progressStages.length - 1);
305 toastManager.update(progressToastId, {311 toastManager.update(resolvedProgressToastId, {
306 title: progressStages[stageIndex] ?? "Running git action...",312 title: progressStages[stageIndex] ?? "Running git action...",
307 type: "loading",313 type: "loading",
308 timeout: 0,314 timeout: 0,
...@@ -338,10 +344,10 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -338,10 +344,10 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
338 result.push.status === "pushed" &&344 result.push.status === "pushed" &&
339 !actionIsDefaultBranch;345 !actionIsDefaultBranch;
340 const closeResultToast = () => {346 const closeResultToast = () => {
341 toastManager.close(progressToastId);347 toastManager.close(resolvedProgressToastId);
342 };348 };
343349
344 toastManager.update(progressToastId, {350 toastManager.update(resolvedProgressToastId, {
345 type: "success",351 type: "success",
346 title: resultToast.title,352 title: resultToast.title,
347 description: resultToast.description,353 description: resultToast.description,
...@@ -359,8 +365,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -359,8 +365,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
359 action: "commit_push",365 action: "commit_push",
360 forcePushOnlyProgress: true,366 forcePushOnlyProgress: true,
361 onConfirmed: closeResultToast,367 onConfirmed: closeResultToast,
362 ...(featureBranch ? { skipDefaultBranchPrompt: true } : {}),368 statusOverride: actionStatus,
363 ...(featureBranch ? { isDefaultBranchOverride: false } : {}),369 isDefaultBranchOverride: actionIsDefaultBranch,
364 });370 });
365 },371 },
366 },372 },
...@@ -386,8 +392,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -386,8 +392,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
386 void runGitActionWithToast({392 void runGitActionWithToast({
387 action: "commit_push_pr",393 action: "commit_push_pr",
388 forcePushOnlyProgress: true,394 forcePushOnlyProgress: true,
389 ...(featureBranch ? { skipDefaultBranchPrompt: true } : {}),395 statusOverride: actionStatus,
390 ...(featureBranch ? { isDefaultBranchOverride: false } : {}),396 isDefaultBranchOverride: actionIsDefaultBranch,
391 });397 });
392 },398 },
393 },399 },
...@@ -396,7 +402,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -396,7 +402,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
396 });402 });
397 } catch (err) {403 } catch (err) {
398 stopProgressUpdates();404 stopProgressUpdates();
399 toastManager.update(progressToastId, {405 toastManager.update(resolvedProgressToastId, {
400 type: "error",406 type: "error",
401 title: "Action failed",407 title: "Action failed",
402 description: err instanceof Error ? err.message : "An error occurred.",408 description: err instanceof Error ? err.message : "An error occurred.",
...@@ -406,7 +412,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -406,7 +412,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
406 },412 },
407413
408 [414 [
409 isDefaultBranchName,415 isDefaultBranch,
410 runImmediateGitActionMutation,416 runImmediateGitActionMutation,
411 setPendingDefaultBranchAction,417 setPendingDefaultBranchAction,
412 threadToastData,418 threadToastData,
...@@ -424,7 +430,6 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -424,7 +430,6 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
424 forcePushOnlyProgress,430 forcePushOnlyProgress,
425 ...(onConfirmed ? { onConfirmed } : {}),431 ...(onConfirmed ? { onConfirmed } : {}),
426 skipDefaultBranchPrompt: true,432 skipDefaultBranchPrompt: true,
427 isDefaultBranchOverride: true,
428 });433 });
429 }, [pendingDefaultBranchAction, runGitActionWithToast]);434 }, [pendingDefaultBranchAction, runGitActionWithToast]);
430435