diff --git a/apps/server/src/gitManager.test.ts b/apps/server/src/gitManager.test.ts index 172e85368a5d4cb68f15ce60ea08727e46304136..13d5baafdeb060894cc2be34b16ed1de19391008 100644 --- a/apps/server/src/gitManager.test.ts +++ b/apps/server/src/gitManager.test.ts @@ -343,7 +343,7 @@ describe("GitManager", () => { expect(result.push.status).toBe("skipped_up_to_date"); }); - it("opens existing PR for commit/push/pr action", async () => { + it("returns existing PR metadata for commit/push/pr action", async () => { const repoDir = makeTempDir("t3code-git-manager-"); await initRepo(repoDir); await runGit(repoDir, ["checkout", "-b", "feature/existing-pr"]); @@ -376,9 +376,7 @@ describe("GitManager", () => { expect(result.pr.status).toBe("opened_existing"); expect(result.pr.number).toBe(42); - expect( - ghCalls.some((call) => call.startsWith("pr view feature/existing-pr --web")), - ).toBe(true); + expect(ghCalls.some((call) => call.startsWith("pr view "))).toBe(false); }); it("creates PR when one does not already exist", async () => { @@ -422,6 +420,7 @@ describe("GitManager", () => { expect( ghCalls.some((call) => call.includes("pr create --base main --head feature-create-pr")), ).toBe(true); + expect(ghCalls.some((call) => call.startsWith("pr view "))).toBe(false); }); it("rejects push/pr actions from detached HEAD", async () => { diff --git a/apps/server/src/gitManager.ts b/apps/server/src/gitManager.ts index 32bbc4bf3bc04f8020d3f66a176aabcd9a33b705..4bc511761ef441c6ab79b83b81612b8ea066004a 100644 --- a/apps/server/src/gitManager.ts +++ b/apps/server/src/gitManager.ts @@ -325,7 +325,6 @@ export class GitManager { const existing = await this.findOpenPr(cwd, branch); if (existing) { - await this.openPrInBrowser(cwd, branch); return { status: "opened_existing", url: existing.url, @@ -381,7 +380,6 @@ export class GitManager { } const created = await this.findOpenPr(cwd, branch); - await this.openPrInBrowser(cwd, branch); if (!created) { return { @@ -402,14 +400,6 @@ export class GitManager { }; } - private async openPrInBrowser(cwd: string, branch: string): Promise { - try { - await this.runGh(cwd, ["pr", "view", branch, "--web"]); - } catch { - // Opening the browser is best-effort. - } - } - private async findOpenPr( cwd: string, branch: string, diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 696f4307df61bf22f4c846a880367aecf5bcde99..28ed5eeb5d8ba1371f8c0b25476135e20bcd9d8a 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -64,6 +64,22 @@ function GitActionIcon(props: { icon: GitActionIconName; disabled: boolean }) { return ; } +function GitProgressStatusIcon(props: { status: GitProgressStepStatus }) { + if (props.status === "running") { + return ; + } + if (props.status === "completed") { + return ; + } + if (props.status === "skipped") { + return ; + } + if (props.status === "failed") { + return ; + } + return ; +} + function gitActionModalTitle(): string { return "Commit your changes"; } @@ -249,6 +265,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp const [gitModalProgress, setGitModalProgress] = useState([]); const [gitModalError, setGitModalError] = useState(null); const [gitModalResult, setGitModalResult] = useState(null); + const [isGitProgressToastVisible, setIsGitProgressToastVisible] = useState(false); const gitMenuRef = useRef(null); const { data: gitStatus = null, error: gitStatusError } = useQuery( @@ -291,10 +308,13 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp [gitModalActionOptions, gitModalSelectedAction], ); const visibleGitActionError = gitActionError ?? gitStatusError?.message; - const gitModalOpenPrUrl = gitModalResult?.pr.url ?? gitStatus?.openPr?.url ?? null; - const gitModalHasProgress = gitModalProgress.length > 0; - const gitModalSelectionMode = !gitModalHasProgress && gitModalResult === null; - const gitModalSteps = gitModalProgress; + const gitProgressToastOpenPrUrl = gitModalResult?.pr.url ?? gitStatus?.openPr?.url ?? null; + const showGitProgressToast = + isGitProgressToastVisible && + (isGitModalActionRunning || + gitModalProgress.length > 0 || + gitModalError !== null || + gitModalResult !== null); const refreshGitStatus = useCallback(async () => { if (!api || !gitCwd) return; @@ -307,6 +327,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp setIsGitModalOpen(true); setGitModalSelectedAction("commit"); setGitModalCommitMessage(""); + setIsGitProgressToastVisible(false); setGitModalProgress([]); setGitModalError(null); setGitModalResult(null); @@ -318,9 +339,6 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp setIsGitModalOpen(false); setGitModalSelectedAction("commit"); setGitModalCommitMessage(""); - setGitModalProgress([]); - setGitModalError(null); - setGitModalResult(null); }, [isGitActionRunning]); const runGitActionImmediately = useCallback( @@ -349,28 +367,39 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp }); }, [api, gitStatus?.openPr?.url]); - const openPrFromModal = useCallback(() => { + const openPrFromToast = useCallback(() => { if (!api) { setGitModalError("Link opening is unavailable."); return; } - const prUrl = gitModalOpenPrUrl; + const prUrl = gitProgressToastOpenPrUrl; if (!prUrl) return; void api.shell.openExternal(prUrl).catch((error) => { setGitModalError(error instanceof Error ? error.message : "Unable to open PR link."); }); - }, [api, gitModalOpenPrUrl]); + }, [api, gitProgressToastOpenPrUrl]); + + const dismissGitProgressToast = useCallback(() => { + if (isGitModalActionRunning) return; + setIsGitProgressToastVisible(false); + setGitModalProgress([]); + setGitModalError(null); + setGitModalResult(null); + }, [isGitModalActionRunning]); const runGitAction = useCallback(async () => { - if (!api || !gitCwd || !isGitModalOpen) return; + if (!api || !gitCwd) return; if (!selectedGitModalActionOption || selectedGitModalActionOption.disabled) return; const actionCwd = gitCwd; const action = selectedGitModalActionOption.action; const commitMessage = gitModalCommitMessage.trim(); const includeGeneratedCommitMessage = commitMessage.length === 0; + setIsGitMenuOpen(false); + setIsGitModalOpen(false); + setIsGitProgressToastVisible(true); setIsGitModalActionRunning(true); setGitModalError(null); setGitActionError(null); @@ -488,7 +517,6 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp api, gitCwd, gitModalCommitMessage, - isGitModalOpen, refreshGitStatus, selectedGitModalActionOption, ]); @@ -497,6 +525,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp setGitActionError(null); setGitModalError(null); setIsGitModalOpen(false); + setIsGitProgressToastVisible(false); setGitModalSelectedAction("commit"); setGitModalCommitMessage(""); setGitModalProgress([]); @@ -690,92 +719,57 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp placeholder="Leave blank to autogenerate a commit message" value={gitModalCommitMessage} onChange={(event) => setGitModalCommitMessage(event.target.value)} - disabled={!gitModalSelectionMode || isGitActionRunning} + disabled={isGitActionRunning} />

Next steps

- {gitModalSelectionMode && - gitModalActionOptions.map((option, index) => { - const borderClass = - index < gitModalActionOptions.length - 1 ? "border-b border-border/70" : ""; - const selected = option.action === gitModalSelectedAction && !option.disabled; - const disabled = option.disabled || isGitActionRunning; + {gitModalActionOptions.map((option, index) => { + const borderClass = + index < gitModalActionOptions.length - 1 ? "border-b border-border/70" : ""; + const selected = option.action === gitModalSelectedAction && !option.disabled; + const disabled = option.disabled || isGitActionRunning; - return ( - - ); - })} - {!gitModalSelectionMode && - gitModalSteps.map((step, index) => { - const borderClass = - index < gitModalSteps.length - 1 ? "border-b border-border/70" : ""; - - return ( -
- - {step.status === "running" ? ( - - ) : step.status === "completed" ? ( - - ) : step.status === "skipped" ? ( - - ) : step.status === "failed" ? ( - - ) : ( - - )} - -
-

{step.label}

- {step.status === "failed" && step.detail && ( -

- {step.detail} -

- )} -
+ )}
- ); - })} + {selected && !disabled ? ( + + ) : null} + + ); + })}
{(gitModalError ?? visibleGitActionError) && ( @@ -784,44 +778,116 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp )}
- {gitModalResult && gitModalOpenPrUrl && ( + + +
+ + + )} + + {showGitProgressToast && ( +
+
+
+
+

+ Git actions +

+

+ {isGitModalActionRunning + ? "Running action..." + : gitModalError + ? "Action failed" + : "Action complete"} +

+
+ +
+ {gitModalProgress.length > 0 && ( +
+ {gitModalProgress.map((step, index) => { + const borderClass = + index < gitModalProgress.length - 1 ? "border-b border-border/70" : ""; + + return ( +
+ + + +
+

{step.label}

+ {step.detail && ( +

+ {step.detail} +

+ )} +
+
+ ); + })} +
+ )} + {gitModalError && ( +
+ {gitModalError} +
+ )} +
+ {!isGitModalActionRunning && gitModalResult && gitProgressToastOpenPrUrl && ( )} - {!gitModalResult && ( - - )}