feat(pulls,automations,settings): expand draft controls and rerun automation
Brought to you by:
thebguy
Originally created by: theBGuy
Originally owned by: theBGuy
Expand pull-request draft workflows across GitHub, GitLab, and Bitbucket while making stopped automated reviews recoverable in-app. The changes reduce repetitive setup for draft PRs and preserve failed or cancelled automation runs so users can retry the exact review mode.
createPrsAsDraft to src/lib/settings/api.ts and exposes the Create pull requests as drafts toggle in src/features/settings/GeneralSection.tsx.src/features/pulls/CreatePrDialog.tsx, while retaining per-PR override support.forge_pr_set_draft in src-tauri/src/forge/mod.rs to support both draft directions across GitHub, GitLab, and Bitbucket.set_mr_draft in src-tauri/src/forge/gitlab.rs.gh pr ready --undo support through gh_pr_set_ready in src-tauri/src/github/pr.rs, including fork-aware lens handling.src-tauri/src/forge/model.rs, src/lib/git/types.ts, and src/lib/git/queries.ts for the shared draft toggle.src/features/pulls/RemotePrView.tsx and src/lib/hotkeys/registry.ts for Ready for review and Convert to draft.set_pull_request_draft tool in src-tauri/src/mcp_server/write_forge.rs and its API wrapper in src/lib/git/api.ts.useSetPrDraft within src/lib/git/queries.ts.src/features/activity/ActivityDock.tsx.src/lib/stores/reviews.ts, limits retained stopped rows, and preserves manual run behavior.src/lib/automations/runner.ts so retrying a stopped row re-fires only that automation mode.src/lib/automations/dismissals.ts, preventing cancelled runs from being skipped by the existing watermark.src/lib/automations/runner.ts.src/lib/automations/runner.ts when a remote pull-request head is not available locally.README.md.src/features/help/content.ts.changelog.d/added-automation-rerun.md, changelog.d/added-default-draft-setting.md, and changelog.d/changed-draft-toggle-all-providers.md.
Originally posted by: cloudflare-workers-and-pages[bot]
Deploying gitdesktop with
Cloudflare Pages
4a96b97View logs
Originally posted by: theBGuy
🗒️ Notes for reviewers
Recorded decisions for this three-feature batch (stopped-automation re-run · all-provider draft toggle · default-draft setting), so deliberate choices read as such:
cancelReview'sauto:arm now patches tocancelledinstead of removing — deliberately inverting the old comment's rationale: the stopped row (with Re-run/Dismiss) IS the cancel feedback now. The runner's cancel arms intentionally do NOT settle; control cleanup is split (cancelReview deletes the control;fail/settleguard on own-control identity).rerunclosure presence is the dock discriminator — manual panel runs also reach cancelled/error phases but never carryrerun, so they stay out of the Stopped group by construction.run(event, only)), clearing that (target, mode) dismissed-head watermark first; claim files (released on stop, kept on deliver) make re-fires per-mode safe. A rule disabled since the stop toasts instead of no-oping silently.forgePrDifffallback — remote pr-sync already used the forge diff ("head may not be local"); pr-open kept the local branch diff, which broke for catch-up/ready-flip events on unfetched heads (observed live:git diff main...<head>fatals when the head was never fetched). Fallback validated end-to-end on a ref-only fixture — both automated reviews delivered through it.errorMessage(e)in the runner's failure path — the prior raw interpolation rendered Tauri rejections as "[object Object]" (observed live); the normalized message also feeds the stopped row's tooltip.review-failedinbox notification gated onnotifications.automations— parity with manual runs' notifyReviewDone. Canceled runs deliberately don't (user-initiated; the stopped row is the feedback).gh pr ready [--undo], not the HTTP forge —mr_draft_toggleis deliberately false for GitHub (per-provider Implemented convention); the footer pair's GitHub gate reduces to the oldcanWrite, keeping the Ready path zero-diff (same variant/copy/position,fireReadyReviewpreserved on button + palette).gh_pr_readystays registered though the frontend no longer imports it — it's the shared impl behind the forge arm's GitHub dispatch, not dead code.useSetPrDraftis optimistic (field-scoped isDraft patch + rollback, mirroring useSetPrAssignees) with an explicit repo-wide invalidate on settle — reproducing useRepoMutation's default deliberately, since draft state feeds the merge gate and PR lists, broader than the assignees mirror's PR-scoped invalidate.busy(not justsetDraft.isPending) — thebusydeclaration was hoisted above the palette block (TDZ), expression unchanged.createPrsAsDraftseeds at defaultValues + the seedOnOpen reset (fresh read per open via useEffectEvent); it deliberately does NOT retro-flip an already-open dialog, and?? falsekeeps pre-field settings stores byte-identical.set_pull_request_draft: description/doc text only — tool count stays 119; the forge call site gained theNonelens arg with the new signature.Validation run before this PR:
pnpm buildgreen ·cargo test787 passed + clippy clean · biome check-only clean on all touched files · live E2E on scratch PRs (draft both directions via palette AND footer verified againstgh pr view, Stopped/Re-run/Dismiss exercised on real failures, inbox parity, dialog draft seeding). Not live-tested: the dock cancel arm (fixture reviews completed too fast to cancel) and the GitLab glab arm (both flags CLI-verified; no GitLab scratch repo).Posted by GitDesktop — automated agent comment, verify before acting on it.
Originally posted by: theBGuy
🤖 GitDesktop AI review ·
opus· automatedThis PR adds three coherent features — re-runnable stopped automations, an all-provider draft toggle, and a default-draft setting — and the implementation is careful and well-documented; the Rust arms, optimistic mutation, and gating all line up correctly. I found one should-fix in the Re-run flow and a minor doc inaccuracy; nothing else blocking.
Correctness
src/features/activity/ActivityDock.tsxStoppedTaskRow(Re-run onClick) +src/lib/automations/runner.tsrerunAutomation/runclaim gate. The Re-run handler callsresetReview(task.key)(synchronously removing the row) before firingtask.rerun?.(), and the re-run is async and can silently do nothing. Concrete cases whererun(event, only)returnsattempted === 0even though the rule is still enabled:cancelReviewsynchronously patches the row to"cancelled"and exposes Re-run immediately, but the cancelled run'sreleaseClaim()only runs once the abort/subprocess-kill unwindsgenerateReviewText(100ms–1s+ for a CLI provider). Clicking Re-run in that window makesclaim_automation_runreturnfalse(the original claim is still held) →if (!won) continue→attempted === 0.pr-syncsameSha(prior.headSha, headSha)gate skips.In all of these the row is already gone (via
resetReview) and the user gets the misleading toastAutomated review for this pull request is turned off.— with no way to retry. Fix: don't remove the stopped row up-front; only remove it oncerun()confirms it registered a new run (e.g. havererunAutomationresolve a "started" signal and remove/keep the row based on it), and narrow theattempted === 0toast so a held-claim/already-covered outcome isn't reported as "turned off."Docs
src/features/help/content.ts(Activity & notifications section). The new sentence says the failed-automated-run inbox row matches "a manual run (both gated on the automations notification preference)." The manual path (notifyReviewDoneinstores/reviews.ts) gates its inbox row onnotifications.reviews, notautomations— only the automated run is gated onautomations. Reword to avoid claiming the manual run rides the automations pref. (The changelog fragment's "when automation notifications are on" is accurate.)Acknowledged deliberate decisions (not re-flagged)
gh_pr_readykept registered though the frontend no longer imports it (note [#11]) — recorded; note that the forge GitHub arm actually callsgh_pr_set_readydirectly, sogh_pr_readyis now only reachable via its Tauri registration, which matches the stated backward-compat intent.The optimistic
useSetPrDraft(key["repo", repo, "pr", lens, number]matchesusePrDetails), the!draft→gh pr ready [--undo]mapping, themr_draft_toggleflag wiring across all constructors/tests, thebusyhoist (no duplicate declaration), and the removal ofuseReadyPr/ghPrReadywith no dangling callers all check out.Posted by GitDesktop — AI output, verify before acting on it.
Related
Tickets:
#11Tickets:
#17Originally posted by: theBGuy
🤖 GitDesktop AI security audit ·
opus· automatedI verified the only injection-capable sinks in this diff:
set_mr_draft/gh_pr_set_readyshellglab/ghviaCommand::new(...).args(&[...])(confirmed inglab.rs/runner.rs) — argument arrays, no shell interpolation. Their dynamic inputs arenumber: u64(digits only) and fixed literal flags (--draft/--ready/--undo), so no argument injection is possible.lensflows intogh_lens_slug→lens_remote, which strictly whitelistsNone/"origin"/"upstream"and rejects anything else withInvalidArgumentbefore any spawn, so the newOption<String>lens parameter cannot smuggle attacker-controlled tokens into the command.set_pull_request_drafttool still callsensure_remote_write()and passesNonefor the lens, so authorization is unchanged.The rest of the diff is frontend state management (Zustand review store, react-query optimistic patch, settings field, UI rows, help/README/changelog text) with no unsafe sinks (no
dangerouslySetInnerHTML, no secrets, no new network host control). The automation runner embedding diffs into review prompts is the product working as designed.No genuine security issues in these changes.
Posted by GitDesktop — AI output, verify before acting on it.
Originally posted by: theBGuy
Round-1 disposition (fixes verified locally, landing in the next push):
run(event, only, replacesKey)), so a re-fire that can't start keeps the row as a retry target.runnow returns{matched, attempted}andrerunAutomationdistinguishes the three outcomes: rule genuinely gone → "turned off" toast; rule applies but a claim/watermark blocked it (your fast-click-after-Cancel and already-covered cases) → a retryable "another run already covers this head… the row is kept" toast; registered → no toast, the fresh Running row is the feedback. Verified against both concrete races in the finding: the claim gate sits upstream of the removal, so a Re-run racing the canceled run'sreleaseClaimunwind keeps the row; double-click is safe (second fire loses the claim,removeno-ops on the absent key)..catch(() => undefined)) — FIXED by the same change. The whole-body swallow is gone: a throw before/outside the loop (e.g.loadAutomationsI/O) now surfacesCouldn't re-run the …: <message>and the row stays; only the best-effortclearDismissedHeadkeeps its own catch. Replied + resolved on the thread.ensure_remote_write).Verification after the fixes:
pnpm buildgreen, biome check clean on the three touched files.Posted by GitDesktop — automated agent comment, verify before acting on it.
Originally posted by: theBGuy
🤖 GitDesktop AI review ·
opus· automatedI've verified the key claims against the actual code. Here's my review.
The change cleanly adds three features — re-runnable stopped automations, an all-provider draft toggle, and a default-draft setting. The Rust dispatch arms, the optimistic
useSetPrDraftmutation, the capability flag wiring, and the re-run flow all line up correctly. The revision since the last review addressed the one real should-fix well. No blocking issues; this looks sound to merge.Resolved since last review
StoppedTaskRow's Re-run now calls onlytask.rerun?.()(no up-frontresetReview), and the stale row is removed insiderun()at line ~348 after the replacementregisterAutomationRunsucceeds and past the claim gate (if (!won) continueat line 297 sits beforeattempted++at line 323).runreturns{matched, attempted}, andrerunAutomationnow distinguishes rule-gone (matched 0 → "turned off"), blocked-but-retryable (matched>0/attempted 0 → "row kept, try again"), and started (no toast). The fast-click-after-Cancel and already-covered cases from the prior finding now keep the row. Verified end-to-end in the current diff..catch(() => undefined)inrerunAutomation(Copilot) — resolved. The swallow is gone; a throw before/outside the per-rule loop now surfacesCouldn't re-run the …: <message>and keeps the row, while only the best-effortclearDismissedHeadretains its own scoped catch.notifications.reviewsvianotifyReviewDone).Verified correct
useSetPrDraft's optimistic key["repo", repo, "pr", lens, args.number]matchesprDetailsOptions'queryKey(queries.ts:950); the field-scoped rollback and repo-wideonSettledinvalidate mirroruseSetPrAssigneeswith the documented broader-invalidate deviation, and both footer buttons + palette twins passonErrorso failures still toast.review-failed/tone: "danger"push notification uses a validNotificationTone, and itstarget.kind = event.target.typeis"remote" | "local"as theNotificationTargetrequires.run()is accounted for after removing the oldfinally { settle() }: success/skip →settle, failure →fail, both cancel arms intentionally leave the "cancelled" row; nocontinueafter registration leaves an orphaned "running" row.canWrite && pr.isDraft(sincecanToggleDraftis false for GitHub), and backend dispatch is provider-detected server-side, so a momentarily-undefinedprovideron GitLab/Bitbucket can't misroute throughgh pr ready.!draft → readymapping intogh_pr_set_ready(--undowhen!ready), the GitLab--draft|--readyflags, and themr_draft_toggleflag (github false / gitlab+bitbucket true) with matching tests.Acknowledged deliberate decisions
gh_pr_readykept registered though the frontend droppedghPrReady(note [#11]) — recorded; it's now only reachable via its Tauri registration, the forge arm callinggh_pr_set_readydirectly.onSettledinvalidate and stopped-cap eviction at 8 (notes [#12], [#3]) — recorded and consistent with the code.Posted by GitDesktop — AI output, verify before acting on it.
Related
Tickets:
#11Tickets:
#12Tickets:
#17Tickets:
#3Ticket changed by: theBGuy