authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-02 11:24:24-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-02 11:24:24-07:00
log9ea443d73afc72cf9fa617bdc1344e67913e4c63
tree6ef41cc03f794a7a26968d9ef8d240d0647a78cb
parent60f7ae86bbeaa95ebdfeb0fd83edc564119258df
signature Signed by PGP key B5690EEEBB952194

Split PR creation into granular progress stages (#1694)


4 files changed, 61 insertions(+), 7 deletions(-)

apps/server/src/git/Layers/GitManager.test.ts+11-1
...@@ -2739,7 +2739,17 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2739,7 +2739,17 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2739 expect.objectContaining({2739 expect.objectContaining({
2740 kind: "phase_started",2740 kind: "phase_started",
2741 phase: "pr",2741 phase: "pr",
2742 label: "Creating PR...",2742 label: "Preparing PR...",
2743 }),
2744 expect.objectContaining({
2745 kind: "phase_started",
2746 phase: "pr",
2747 label: "Generating PR content...",
2748 }),
2749 expect.objectContaining({
2750 kind: "phase_started",
2751 phase: "pr",
2752 label: "Creating GitHub pull request...",
2743 }),2753 }),
2744 ]);2754 ]);
2745 }),2755 }),
apps/server/src/git/Layers/GitManager.ts+16-2
...@@ -37,6 +37,7 @@ const STATUS_RESULT_CACHE_TTL = Duration.seconds(1);...@@ -37,6 +37,7 @@ const STATUS_RESULT_CACHE_TTL = Duration.seconds(1);
37const STATUS_RESULT_CACHE_CAPACITY = 2_048;37const STATUS_RESULT_CACHE_CAPACITY = 2_048;
38type StripProgressContext<T> = T extends any ? Omit<T, "actionId" | "cwd" | "action"> : never;38type StripProgressContext<T> = T extends any ? Omit<T, "actionId" | "cwd" | "action"> : never;
39type GitActionProgressPayload = StripProgressContext<GitActionProgressEvent>;39type GitActionProgressPayload = StripProgressContext<GitActionProgressEvent>;
40type GitActionProgressEmitter = (event: GitActionProgressPayload) => Effect.Effect<void, never>;
4041
41interface OpenPrInfo {42interface OpenPrInfo {
42 number: number;43 number: number;
...@@ -1200,6 +1201,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {...@@ -1200,6 +1201,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
1200 modelSelection: ModelSelection,1201 modelSelection: ModelSelection,
1201 cwd: string,1202 cwd: string,
1202 fallbackBranch: string | null,1203 fallbackBranch: string | null,
1204 emit: GitActionProgressEmitter,
1203 ) {1205 ) {
1204 const details = yield* gitCore.statusDetails(cwd);1206 const details = yield* gitCore.statusDetails(cwd);
1205 const branch = details.branch ?? fallbackBranch;1207 const branch = details.branch ?? fallbackBranch;
...@@ -1234,6 +1236,11 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {...@@ -1234,6 +1236,11 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
1234 }1236 }
12351237
1236 const baseBranch = yield* resolveBaseBranch(cwd, branch, details.upstreamRef, headContext);1238 const baseBranch = yield* resolveBaseBranch(cwd, branch, details.upstreamRef, headContext);
1239 yield* emit({
1240 kind: "phase_started",
1241 phase: "pr",
1242 label: "Generating PR content...",
1243 });
1237 const rangeContext = yield* gitCore.readRangeContext(cwd, baseBranch);1244 const rangeContext = yield* gitCore.readRangeContext(cwd, baseBranch);
12381245
1239 const generated = yield* textGeneration.generatePrContent({1246 const generated = yield* textGeneration.generatePrContent({
...@@ -1254,6 +1261,11 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {...@@ -1254,6 +1261,11 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
1254 gitManagerError("runPrStep", "Failed to write pull request body temp file.", cause),1261 gitManagerError("runPrStep", "Failed to write pull request body temp file.", cause),
1255 ),1262 ),
1256 );1263 );
1264 yield* emit({
1265 kind: "phase_started",
1266 phase: "pr",
1267 label: "Creating GitHub pull request...",
1268 });
1257 yield* gitHubCli1269 yield* gitHubCli
1258 .createPullRequest({1270 .createPullRequest({
1259 cwd,1271 cwd,
...@@ -1612,11 +1624,13 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {...@@ -1612,11 +1624,13 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
1612 .emit({1624 .emit({
1613 kind: "phase_started",1625 kind: "phase_started",
1614 phase: "pr",1626 phase: "pr",
1615 label: "Creating PR...",1627 label: "Preparing PR...",
1616 })1628 })
1617 .pipe(1629 .pipe(
1618 Effect.tap(() => Ref.set(currentPhase, Option.some("pr"))),1630 Effect.tap(() => Ref.set(currentPhase, Option.some("pr"))),
1619 Effect.flatMap(() => runPrStep(modelSelection, input.cwd, currentBranch)),1631 Effect.flatMap(() =>
1632 runPrStep(modelSelection, input.cwd, currentBranch, progress.emit),
1633 ),
1620 )1634 )
1621 : { status: "skipped_not_requested" as const };1635 : { status: "skipped_not_requested" as const };
16221636
apps/web/src/components/GitActionsControl.logic.test.ts+27-2
...@@ -877,7 +877,12 @@ describe("buildGitActionProgressStages", () => {...@@ -877,7 +877,12 @@ describe("buildGitActionProgressStages", () => {
877 pushTarget: "origin/feature/test",877 pushTarget: "origin/feature/test",
878 shouldPushBeforePr: true,878 shouldPushBeforePr: true,
879 });879 });
880 assert.deepEqual(stages, ["Pushing to origin/feature/test...", "Creating PR..."]);880 assert.deepEqual(stages, [
881 "Pushing to origin/feature/test...",
882 "Preparing PR...",
883 "Generating PR content...",
884 "Creating GitHub pull request...",
885 ]);
881 });886 });
882887
883 it("shows only PR progress when create-pr can skip the push", () => {888 it("shows only PR progress when create-pr can skip the push", () => {
...@@ -887,7 +892,11 @@ describe("buildGitActionProgressStages", () => {...@@ -887,7 +892,11 @@ describe("buildGitActionProgressStages", () => {
887 hasWorkingTreeChanges: false,892 hasWorkingTreeChanges: false,
888 shouldPushBeforePr: false,893 shouldPushBeforePr: false,
889 });894 });
890 assert.deepEqual(stages, ["Creating PR..."]);895 assert.deepEqual(stages, [
896 "Preparing PR...",
897 "Generating PR content...",
898 "Creating GitHub pull request...",
899 ]);
891 });900 });
892901
893 it("includes commit stages for commit+push when working tree is dirty", () => {902 it("includes commit stages for commit+push when working tree is dirty", () => {
...@@ -903,6 +912,22 @@ describe("buildGitActionProgressStages", () => {...@@ -903,6 +912,22 @@ describe("buildGitActionProgressStages", () => {
903 "Pushing to origin/feature/test...",912 "Pushing to origin/feature/test...",
904 ]);913 ]);
905 });914 });
915
916 it("includes granular PR stages for commit+push+PR actions", () => {
917 const stages = buildGitActionProgressStages({
918 action: "commit_push_pr",
919 hasCustomCommitMessage: true,
920 hasWorkingTreeChanges: true,
921 pushTarget: "origin/feature/test",
922 });
923 assert.deepEqual(stages, [
924 "Committing...",
925 "Pushing to origin/feature/test...",
926 "Preparing PR...",
927 "Generating PR content...",
928 "Creating GitHub pull request...",
929 ]);
930 });
906});931});
907932
908describe("resolveThreadBranchUpdate", () => {933describe("resolveThreadBranchUpdate", () => {
apps/web/src/components/GitActionsControl.logic.ts+7-2
...@@ -47,12 +47,17 @@ export function buildGitActionProgressStages(input: {...@@ -47,12 +47,17 @@ export function buildGitActionProgressStages(input: {
47}): string[] {47}): string[] {
48 const branchStages = input.featureBranch ? ["Preparing feature branch..."] : [];48 const branchStages = input.featureBranch ? ["Preparing feature branch..."] : [];
49 const pushStage = input.pushTarget ? `Pushing to ${input.pushTarget}...` : "Pushing...";49 const pushStage = input.pushTarget ? `Pushing to ${input.pushTarget}...` : "Pushing...";
50 const prStages = [
51 "Preparing PR...",
52 "Generating PR content...",
53 "Creating GitHub pull request...",
54 ];
5055
51 if (input.action === "push") {56 if (input.action === "push") {
52 return [pushStage];57 return [pushStage];
53 }58 }
54 if (input.action === "create_pr") {59 if (input.action === "create_pr") {
55 return input.shouldPushBeforePr ? [pushStage, "Creating PR..."] : ["Creating PR..."];60 return input.shouldPushBeforePr ? [pushStage, ...prStages] : prStages;
56 }61 }
5762
58 const shouldIncludeCommitStages = input.action === "commit" || input.hasWorkingTreeChanges;63 const shouldIncludeCommitStages = input.action === "commit" || input.hasWorkingTreeChanges;
...@@ -67,7 +72,7 @@ export function buildGitActionProgressStages(input: {...@@ -67,7 +72,7 @@ export function buildGitActionProgressStages(input: {
67 if (input.action === "commit_push") {72 if (input.action === "commit_push") {
68 return [...branchStages, ...commitStages, pushStage];73 return [...branchStages, ...commitStages, pushStage];
69 }74 }
70 return [...branchStages, ...commitStages, pushStage, "Creating PR..."];75 return [...branchStages, ...commitStages, pushStage, ...prStages];
71}76}
7277
73export function buildMenuItems(78export function buildMenuItems(