From 0325f32a862eef833fff63c51d45a5845a80321e Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 1 Oct 2026 16:52:52 +0900 Subject: [PATCH 1/5] fix(github): enqueue a merge-queue PR when auto-merge is disabled gh pr merge --auto calls enablePullRequestAutoMerge, which GitHub rejects when a required merge queue also disallows auto-merge. --- .../client-merge-queue-auto-merge.test.ts | 47 ++++++++++++++++++ src/main/github/client/merge/pr-auto-merge.ts | 48 +++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/src/main/github/client-merge-queue-auto-merge.test.ts b/src/main/github/client-merge-queue-auto-merge.test.ts index d099408d022f..333d495a06dc 100644 --- a/src/main/github/client-merge-queue-auto-merge.test.ts +++ b/src/main/github/client-merge-queue-auto-merge.test.ts @@ -310,6 +310,53 @@ describe('GitHub GraphQL rate-limit guard', () => { ).toBe(false) }) + it('enqueues when a required merge queue disallows auto-merge', async () => { + ghExecFileAsyncMock + .mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) }) + .mockResolvedValueOnce({ + stdout: JSON.stringify({ id: 'PR_kwDO123', headRefOid: 'head-oid', baseRefName: 'main' }) + }) + .mockResolvedValueOnce({ + stdout: JSON.stringify({ + data: { repository: { mergeQueue: { id: 'MQ_kw' }, autoMergeAllowed: false } } + }) + }) + .mockResolvedValue({ stdout: '', stderr: '' }) + + await expect( + setPRAutoMerge('/repo-root', 7, true, 'squash', undefined, { + owner: 'stablyai', + repo: 'orca', + host: 'github.com' + }) + ).resolves.toEqual({ ok: true }) + + expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith( + 4, + expect.arrayContaining([ + 'api', + 'graphql', + '-f', + 'pullRequestId=PR_kwDO123', + '-f', + 'expectedHeadOid=head-oid' + ]), + expect.objectContaining({ + cwd: '/repo-root', + env: expect.objectContaining({ GH_PROMPT_DISABLED: '1' }), + host: 'github.com' + }) + ) + const enqueueCall = ghExecFileAsyncMock.mock.calls[3]?.[0] as string[] + expect(enqueueCall.some((arg) => arg.includes('enqueuePullRequest'))).toBe(true) + expect( + ghExecFileAsyncMock.mock.calls.some( + (call) => + call[0][0] === 'pr' && call[0][1] === 'merge' && (call[0] as string[]).includes('--auto') + ) + ).toBe(false) + }) + it('blocks direct merge when GitHub reports required approval', async () => { ghExecFileAsyncMock .mockResolvedValueOnce({ stdout: JSON.stringify({ stack: null }) }) diff --git a/src/main/github/client/merge/pr-auto-merge.ts b/src/main/github/client/merge/pr-auto-merge.ts index 0cc65c5fb039..8f2b87b3b2eb 100644 --- a/src/main/github/client/merge/pr-auto-merge.ts +++ b/src/main/github/client/merge/pr-auto-merge.ts @@ -85,6 +85,47 @@ export async function shouldUseMergeQueueAutoMerge( return mergeMetadata.mergeQueueRequired === true } +async function mergeQueueDisallowsAutoMerge( + pr: PRAutoMergeIdentity, + ownerRepo: GitHubApiRepository | null, + ghOptions: GhExecOptions, + executionScope?: string +): Promise { + if (!ownerRepo || !pr.baseRefName) { + return false + } + const mergeMetadata = await detectRepositoryMergeMetadata( + ownerRepo, + pr.baseRefName, + ghOptions, + executionScope + ) + return mergeMetadata.mergeQueueRequired === true && mergeMetadata.autoMergeAllowed === false +} + +async function enqueuePullRequest( + pr: PRAutoMergeIdentity, + ghOptions: GhExecOptions +): Promise { + const query = `mutation($pullRequestId: ID!, $expectedHeadOid: GitObjectID) { + enqueuePullRequest(input: { + pullRequestId: $pullRequestId, + expectedHeadOid: $expectedHeadOid + }) { + mergeQueueEntry { id } + } + }` + const args = ['api', 'graphql', '-f', `query=${query}`, '-f', `pullRequestId=${pr.id}`] + if (pr.headRefOid) { + args.push('-f', `expectedHeadOid=${pr.headRefOid}`) + } + // Why: `gh pr merge --auto` calls enablePullRequestAutoMerge, which GitHub rejects when the repository disables auto-merge. + await ghExecFileAsync(args, { + ...ghOptions, + env: { ...process.env, GH_PROMPT_DISABLED: '1' } + }) +} + export async function enablePRAutoMerge( prNumber: number, method: GitHubPRMergeMethod, @@ -110,6 +151,13 @@ export async function enablePRAutoMerge( return { ok: false, error: 'Could not resolve GitHub pull request ID' } } const useMergeQueue = await shouldUseMergeQueueAutoMerge(pr, ownerRepo, ghOptions, executionScope) + if ( + useMergeQueue && + (await mergeQueueDisallowsAutoMerge(pr, ownerRepo, ghOptions, executionScope)) + ) { + await enqueuePullRequest(pr, ghOptions) + return { ok: true } + } if (useMergeQueue) { await runPRAutoMergeCommand(prNumber, method, ownerRepo, ghOptions) return { ok: true } From 7eaad3d222434f32b365bb1fa887b55ce8c110d5 Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 1 Oct 2026 18:44:32 +0900 Subject: [PATCH 2/5] fix(github): show a merge-queue enqueue as queued, not auto-merged Enqueue does not set autoMergeRequest, so the button stayed on Merge when ready and the toast said auto-merge was enabled. One metadata read now chooses enqueue, and a queued pull request no longer offers it again. Co-authored-by: Cursor --- .../client-merge-queue-auto-merge.test.ts | 2 +- .../github/client/fetch/work-item-fetch.ts | 6 ++ .../github/client/lookup/pr-branch-lookup.ts | 5 +- .../github/client/lookup/pr-number-lookup.ts | 12 ++-- .../lookup/pr-refresh-outcome-assembly.ts | 1 + .../client/lookup/pull-request-lookup-data.ts | 1 + .../lookup/pull-request-lookup-hydration.ts | 11 +++- .../pull-request-merge-queue-membership.ts | 47 ++++++++++++++++ src/main/github/client/merge/pr-auto-merge.ts | 49 +++++++---------- .../api/gh-bridge-mutations-and-projects.ts | 2 +- src/preload/api/github-pull-request-api.ts | 2 +- .../github-auto-merge-success-toast.ts | 31 +++++++++++ .../land-pull-request/pr-actions-panel.tsx | 7 +-- .../components/github-pr-merge-state.test.ts | 16 ++++++ .../src/components/github-pr-merge-state.ts | 55 +++++++++++-------- .../actions/merge-actions.ts | 7 +-- .../right-sidebar/HostedReviewActions.tsx | 3 +- .../task-page-linear-jira-list-model.tsx | 1 + .../task-page-work-item-signatures.ts | 1 + .../components/task-page/github/MergeCell.tsx | 5 +- .../useTaskPageGitHubWorkItemMutation.ts | 7 ++- src/shared/github/pull-request-types.ts | 2 + src/shared/github/work-item-types.ts | 1 + src/shared/hosted-review-github.ts | 1 + src/shared/hosted-review.ts | 1 + 25 files changed, 195 insertions(+), 81 deletions(-) create mode 100644 src/main/github/client/lookup/pull-request-merge-queue-membership.ts create mode 100644 src/renderer/src/components/github-auto-merge-success-toast.ts diff --git a/src/main/github/client-merge-queue-auto-merge.test.ts b/src/main/github/client-merge-queue-auto-merge.test.ts index 333d495a06dc..761a03f27018 100644 --- a/src/main/github/client-merge-queue-auto-merge.test.ts +++ b/src/main/github/client-merge-queue-auto-merge.test.ts @@ -329,7 +329,7 @@ describe('GitHub GraphQL rate-limit guard', () => { repo: 'orca', host: 'github.com' }) - ).resolves.toEqual({ ok: true }) + ).resolves.toEqual({ ok: true, enqueued: true }) expect(ghExecFileAsyncMock).toHaveBeenNthCalledWith( 4, diff --git a/src/main/github/client/fetch/work-item-fetch.ts b/src/main/github/client/fetch/work-item-fetch.ts index f0c227d05e7f..f0b4a5dea2d1 100644 --- a/src/main/github/client/fetch/work-item-fetch.ts +++ b/src/main/github/client/fetch/work-item-fetch.ts @@ -10,6 +10,7 @@ import { githubHostExecOptions, type GitHubApiRepository } from '../../github-ap import type { GhExecOptions } from './../github-exec-scope' import { resolvePullRequestLookupCandidates } from './../pull-request-lookup-candidates' import { detectRepositoryMergeMetadata } from './../detect/repository-merge-metadata' +import { readPullRequestInMergeQueue } from './../lookup/pull-request-merge-queue-membership' import { WORK_ITEM_PR_DETAIL_JSON_FIELDS, usersFromUnknown, @@ -121,9 +122,14 @@ export async function fetchPullRequestWorkItem( const baseRefName = typeof item.baseRefName === 'string' ? item.baseRefName : undefined try { const mergeMetadata = await detectRepositoryMergeMetadata(ownerRepo, baseRefName, ghOptions) + const inMergeQueue = + mergeMetadata.mergeQueueRequired === true + ? await readPullRequestInMergeQueue(ownerRepo, number, ghOptions) + : undefined return { ...mapped, mergeQueueRequired: mergeMetadata.mergeQueueRequired, + ...(inMergeQueue !== undefined ? { inMergeQueue } : {}), ...(mergeMetadata.autoMergeAllowed !== null ? { autoMergeAllowed: mergeMetadata.autoMergeAllowed } : {}), diff --git a/src/main/github/client/lookup/pr-branch-lookup.ts b/src/main/github/client/lookup/pr-branch-lookup.ts index 2ee667ceacc4..aa06e73c154c 100644 --- a/src/main/github/client/lookup/pr-branch-lookup.ts +++ b/src/main/github/client/lookup/pr-branch-lookup.ts @@ -65,8 +65,9 @@ export async function hydrateBranchLookupWithExactPR( } try { return ( - (await getPRByNumber(ownerRepo, branchData.number, ghOptions, executionScope, branchData)) ?? - branchData + (await getPRByNumber(ownerRepo, branchData.number, ghOptions, executionScope, branchData, { + readQueueMembership: true + })) ?? branchData ) } catch { return branchData diff --git a/src/main/github/client/lookup/pr-number-lookup.ts b/src/main/github/client/lookup/pr-number-lookup.ts index 42a605eb295c..55f14d74ab15 100644 --- a/src/main/github/client/lookup/pr-number-lookup.ts +++ b/src/main/github/client/lookup/pr-number-lookup.ts @@ -52,7 +52,8 @@ export async function getPRByNumber( number: number, ghOptions: ReturnType, executionScope: string, - knownPullRequestData?: PullRequestLookupData | null + knownPullRequestData?: PullRequestLookupData | null, + options?: { readQueueMembership?: boolean } ): Promise { try { const { stdout } = await ghExecFileAsync( @@ -76,7 +77,8 @@ export async function getPRByNumber( ...(knownPullRequestData?.stack ? { stack: knownPullRequestData.stack } : {}) }, ghOptions, - executionScope + executionScope, + options ) } catch (err) { // Why: deleted/edited linked PR metadata falls back to branch discovery; quota/auth/network failures get one cheaper REST exact lookup. @@ -89,7 +91,7 @@ export async function getPRByNumber( ? await getRestPRByNumber(ownerRepo, number, ghOptions) : knownPullRequestData return restData - ? hydratePullRequestLookupData(ownerRepo, restData, ghOptions, executionScope) + ? hydratePullRequestLookupData(ownerRepo, restData, ghOptions, executionScope, options) : null } catch (restErr) { if (isNotFoundGhError(restErr)) { @@ -115,7 +117,9 @@ export async function lookupPRByNumber(args: { candidate, args.number, args.ghOptions, - args.executionScope + args.executionScope, + undefined, + { readQueueMembership: true } ) if (!linkedData) { continue diff --git a/src/main/github/client/lookup/pr-refresh-outcome-assembly.ts b/src/main/github/client/lookup/pr-refresh-outcome-assembly.ts index 1a494c407cc5..9a1ecd095404 100644 --- a/src/main/github/client/lookup/pr-refresh-outcome-assembly.ts +++ b/src/main/github/client/lookup/pr-refresh-outcome-assembly.ts @@ -52,6 +52,7 @@ export function assemblePRRefreshFoundOutcome(args: { : data.mergeQueueRequired } : {}), + ...(data.inMergeQueue !== undefined ? { inMergeQueue: data.inMergeQueue } : {}), ...(data.mergeMethodSettings !== undefined ? { mergeMethodSettings: data.mergeMethodSettings } : {}), diff --git a/src/main/github/client/lookup/pull-request-lookup-data.ts b/src/main/github/client/lookup/pull-request-lookup-data.ts index 14e3d92337eb..cc7174af0e63 100644 --- a/src/main/github/client/lookup/pull-request-lookup-data.ts +++ b/src/main/github/client/lookup/pull-request-lookup-data.ts @@ -31,6 +31,7 @@ export type PullRequestLookupData = { autoMergeEnabled?: boolean autoMergeAllowed?: boolean | null mergeQueueRequired?: boolean | null + inMergeQueue?: boolean mergeMethodSettings?: GitHubPRMergeMethodSettings mergeStateStatus?: string | null baseRefName?: string diff --git a/src/main/github/client/lookup/pull-request-lookup-hydration.ts b/src/main/github/client/lookup/pull-request-lookup-hydration.ts index bf2993a0f6dd..b4b9720b9bc1 100644 --- a/src/main/github/client/lookup/pull-request-lookup-hydration.ts +++ b/src/main/github/client/lookup/pull-request-lookup-hydration.ts @@ -1,6 +1,7 @@ import type { OwnerRepo } from '../../gh-utils' import type { GhExecOptions } from './../github-exec-scope' import { detectRepositoryMergeMetadata } from './../detect/repository-merge-metadata' +import { readPullRequestInMergeQueue } from './pull-request-merge-queue-membership' import { normalizePullRequestLookupData, type PullRequestLookupData @@ -9,7 +10,8 @@ export async function hydratePullRequestLookupData( ownerRepo: OwnerRepo, data: PullRequestLookupData, ghOptions: GhExecOptions, - executionScope: string + executionScope: string, + options?: { readQueueMembership?: boolean } ): Promise { const normalized = normalizePullRequestLookupData(data) const hasRichMergeFields = @@ -22,12 +24,17 @@ export async function hydratePullRequestLookupData( executionScope ) : undefined + const inMergeQueue = + options?.readQueueMembership === true && mergeMetadata?.mergeQueueRequired === true + ? await readPullRequestInMergeQueue(ownerRepo, normalized.number, ghOptions) + : undefined return { ...normalized, ...(mergeMetadata ? { mergeQueueRequired: mergeMetadata.mergeQueueRequired } : {}), ...(mergeMetadata ? { autoMergeAllowed: mergeMetadata.autoMergeAllowed } : {}), ...(mergeMetadata?.mergeMethodSettings ? { mergeMethodSettings: mergeMetadata.mergeMethodSettings } - : {}) + : {}), + ...(inMergeQueue !== undefined ? { inMergeQueue } : {}) } } diff --git a/src/main/github/client/lookup/pull-request-merge-queue-membership.ts b/src/main/github/client/lookup/pull-request-merge-queue-membership.ts new file mode 100644 index 000000000000..92e8cdeee105 --- /dev/null +++ b/src/main/github/client/lookup/pull-request-merge-queue-membership.ts @@ -0,0 +1,47 @@ +import { ghExecFileAsync } from '../../gh-utils' +import { githubHostExecOptions, type GitHubApiRepository } from '../../github-api-repository' +import { noteRepositoryRateLimitSpend, repositoryRateLimitGuard } from '../../rate-limit' +import type { GhExecOptions } from '../github-exec-scope' + +/** True when this pull request already has a merge-queue entry. + * `gh pr view` has no field for that, and enqueue does not set autoMergeRequest. */ +export async function readPullRequestInMergeQueue( + ownerRepo: GitHubApiRepository, + prNumber: number, + ghOptions: GhExecOptions +): Promise { + const guard = repositoryRateLimitGuard(ownerRepo, 'graphql', ghOptions) + if (guard.blocked) { + return undefined + } + const query = `query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { isInMergeQueue } + } + }` + try { + noteRepositoryRateLimitSpend(ownerRepo, 'graphql', 1, ghOptions) + const { stdout } = await ghExecFileAsync( + [ + 'api', + 'graphql', + '-f', + `query=${query}`, + '-f', + `owner=${ownerRepo.owner}`, + '-f', + `repo=${ownerRepo.repo}`, + '-F', + `number=${prNumber}` + ], + { ...ghOptions, ...githubHostExecOptions(ownerRepo) } + ) + const parsed = JSON.parse(stdout) as { + data?: { repository?: { pullRequest?: { isInMergeQueue?: unknown } | null } | null } + } + const value = parsed.data?.repository?.pullRequest?.isInMergeQueue + return typeof value === 'boolean' ? value : undefined + } catch { + return undefined + } +} diff --git a/src/main/github/client/merge/pr-auto-merge.ts b/src/main/github/client/merge/pr-auto-merge.ts index 8f2b87b3b2eb..fb702b187eb4 100644 --- a/src/main/github/client/merge/pr-auto-merge.ts +++ b/src/main/github/client/merge/pr-auto-merge.ts @@ -67,14 +67,14 @@ export async function runPRAutoMergeCommand( }) } -export async function shouldUseMergeQueueAutoMerge( +async function mergeQueueEnqueueDecision( pr: PRAutoMergeIdentity, ownerRepo: GitHubApiRepository | null, ghOptions: GhExecOptions, executionScope?: string -): Promise { +): Promise<'enqueue' | 'auto' | 'skip'> { if (!ownerRepo || !pr.baseRefName) { - return false + return 'skip' } const mergeMetadata = await detectRepositoryMergeMetadata( ownerRepo, @@ -82,25 +82,12 @@ export async function shouldUseMergeQueueAutoMerge( ghOptions, executionScope ) - return mergeMetadata.mergeQueueRequired === true -} - -async function mergeQueueDisallowsAutoMerge( - pr: PRAutoMergeIdentity, - ownerRepo: GitHubApiRepository | null, - ghOptions: GhExecOptions, - executionScope?: string -): Promise { - if (!ownerRepo || !pr.baseRefName) { - return false + if (mergeMetadata.mergeQueueRequired !== true) { + return 'skip' } - const mergeMetadata = await detectRepositoryMergeMetadata( - ownerRepo, - pr.baseRefName, - ghOptions, - executionScope - ) - return mergeMetadata.mergeQueueRequired === true && mergeMetadata.autoMergeAllowed === false + // A required queue that disallows auto-merge must be enqueued. The mutation + // does not set autoMergeRequest, so callers have to say it was queued. + return mergeMetadata.autoMergeAllowed === false ? 'enqueue' : 'auto' } async function enqueuePullRequest( @@ -132,7 +119,7 @@ export async function enablePRAutoMerge( ownerRepo: GitHubApiRepository | null, ghOptions: GhExecOptions, executionScope?: string -): Promise<{ ok: true } | { ok: false; error: string }> { +): Promise<{ ok: true; enqueued?: true } | { ok: false; error: string }> { if (ownerRepo) { try { const restData = await getRestPRByNumber(ownerRepo, prNumber, ghOptions) @@ -150,15 +137,17 @@ export async function enablePRAutoMerge( if (!pr?.id) { return { ok: false, error: 'Could not resolve GitHub pull request ID' } } - const useMergeQueue = await shouldUseMergeQueueAutoMerge(pr, ownerRepo, ghOptions, executionScope) - if ( - useMergeQueue && - (await mergeQueueDisallowsAutoMerge(pr, ownerRepo, ghOptions, executionScope)) - ) { + const mergeQueueDecision = await mergeQueueEnqueueDecision( + pr, + ownerRepo, + ghOptions, + executionScope + ) + if (mergeQueueDecision === 'enqueue') { await enqueuePullRequest(pr, ghOptions) - return { ok: true } + return { ok: true, enqueued: true } } - if (useMergeQueue) { + if (mergeQueueDecision === 'auto') { await runPRAutoMergeCommand(prNumber, method, ownerRepo, ghOptions) return { ok: true } } @@ -200,7 +189,7 @@ export async function setPRAutoMerge( connectionId?: string | null, prRepo?: GitHubApiRepository | null, localGitOptions: LocalGitExecOptions = {} -): Promise<{ ok: true } | { ok: false; error: string }> { +): Promise<{ ok: true; enqueued?: true } | { ok: false; error: string }> { const { ownerRepo, ghOptions } = await resolveGitHubRepoExecution( repoPath, prRepo, diff --git a/src/preload/api/gh-bridge-mutations-and-projects.ts b/src/preload/api/gh-bridge-mutations-and-projects.ts index d02c4b9a50e8..c6d1c375a115 100644 --- a/src/preload/api/gh-bridge-mutations-and-projects.ts +++ b/src/preload/api/gh-bridge-mutations-and-projects.ts @@ -50,7 +50,7 @@ export const ghMutationsAndProjectsApi = { enabled: boolean method?: 'merge' | 'squash' | 'rebase' prRepo?: GitHubOwnerRepo | null - }): Promise<{ ok: true } | { ok: false; error: string }> => + }): Promise<{ ok: true; enqueued?: true } | { ok: false; error: string }> => ipcRenderer.invoke('gh:setPRAutoMerge', args), updatePRState: (args: { repoPath: string diff --git a/src/preload/api/github-pull-request-api.ts b/src/preload/api/github-pull-request-api.ts index 68de9ac2d317..4a1f643688f8 100644 --- a/src/preload/api/github-pull-request-api.ts +++ b/src/preload/api/github-pull-request-api.ts @@ -150,7 +150,7 @@ export type GithubPullRequestApi = { method?: 'merge' | 'squash' | 'rebase' prRepo?: GitHubOwnerRepo | null } - ) => Promise<{ ok: true } | { ok: false; error: string }> + ) => Promise<{ ok: true; enqueued?: true } | { ok: false; error: string }> updatePRState: ( args: GitHubRepoSelectorArgs & { prNumber: number diff --git a/src/renderer/src/components/github-auto-merge-success-toast.ts b/src/renderer/src/components/github-auto-merge-success-toast.ts new file mode 100644 index 000000000000..61c5ded6b078 --- /dev/null +++ b/src/renderer/src/components/github-auto-merge-success-toast.ts @@ -0,0 +1,31 @@ +import { translate } from '@/i18n/i18n' +import type { GitHubPRMergeStatePresentation } from './github-pr-merge-state' + +export function githubAutoMergeSuccessToast( + enabled: boolean, + result: { enqueued?: boolean } | void | null +): string { + if (!enabled) { + return translate('auto.components.PullRequestPage.0f5821b035', 'Auto-merge disabled') + } + if (result && result.enqueued === true) { + return translate( + 'auto.components.PullRequestPage.mergeQueueEnqueued', + 'Added to the merge queue' + ) + } + return translate('auto.components.PullRequestPage.5edbe7eefa', 'Auto-merge enabled') +} + +export function presentPullRequestInMergeQueue(tone: string): GitHubPRMergeStatePresentation { + return { + label: translate('auto.components.github.pr.merge.state.inMergeQueue', 'In merge queue'), + tone, + tooltip: translate( + 'auto.components.github.pr.merge.state.inMergeQueueTooltip', + 'This pull request is already in the GitHub merge queue' + ), + directMergeAvailable: false, + autoMergeAction: null + } +} diff --git a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx index 487411e75e81..8da7077dce82 100644 --- a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx +++ b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx @@ -11,6 +11,7 @@ import { getGitHubMutationRoutingSettings, getGitHubRuntimeRepoId } from '@/lib/github-source-runtime-context' +import { githubAutoMergeSuccessToast } from '@/components/github-auto-merge-success-toast' import { presentGitHubPRMergeState } from '@/components/github-pr-merge-state' import { GITHUB_PR_MERGE_METHOD_LABELS, @@ -310,11 +311,7 @@ export function PRActionsPanel({ ) } useAppStore.getState().recordFeatureInteraction('github-tasks') - toast.success( - enabled - ? translate('auto.components.GitHubItemDialog.a35ea5a0f6', 'Auto-merge enabled') - : translate('auto.components.GitHubItemDialog.4b390bd50d', 'Auto-merge disabled') - ) + toast.success(githubAutoMergeSuccessToast(enabled, result)) onMutated() } catch { toast.error( diff --git a/src/renderer/src/components/github-pr-merge-state.test.ts b/src/renderer/src/components/github-pr-merge-state.test.ts index 036d34c4c0db..1e4219937739 100644 --- a/src/renderer/src/components/github-pr-merge-state.test.ts +++ b/src/renderer/src/components/github-pr-merge-state.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { githubAutoMergeSuccessToast } from './github-auto-merge-success-toast' import { presentGitHubPRMergeState, type GitHubPRMergeStateInput } from './github-pr-merge-state' function pr(overrides: Partial = {}): GitHubPRMergeStateInput { @@ -24,6 +25,21 @@ describe('presentGitHubPRMergeState', () => { }) }) + it('says a disallowed merge queue was enqueued, not auto-merged', () => { + expect(githubAutoMergeSuccessToast(true, { enqueued: true })).toBe('Added to the merge queue') + expect(githubAutoMergeSuccessToast(true, { ok: true })).toBe('Auto-merge enabled') + }) + + it('hides enqueue once the pull request is already in the merge queue', () => { + expect( + presentGitHubPRMergeState(pr({ mergeQueueRequired: true, inMergeQueue: true })) + ).toMatchObject({ + label: 'In merge queue', + directMergeAvailable: false, + autoMergeAction: null + }) + }) + it('uses the merge-queue label for auto-merge when a queue is required', () => { expect(presentGitHubPRMergeState(pr({ mergeQueueRequired: true }))).toMatchObject({ label: 'Merge when ready', diff --git a/src/renderer/src/components/github-pr-merge-state.ts b/src/renderer/src/components/github-pr-merge-state.ts index f26edd5896ba..66288965a944 100644 --- a/src/renderer/src/components/github-pr-merge-state.ts +++ b/src/renderer/src/components/github-pr-merge-state.ts @@ -7,6 +7,7 @@ import type { } from '../../../shared/github/pull-request-types' import { canEnableGitHubPRAutoMerge } from '../../../shared/github/pull-request-auto-merge-availability' import { translate } from '@/i18n/i18n' +import { presentPullRequestInMergeQueue } from '@/components/github-auto-merge-success-toast' export type GitHubPRMergeStateInput = { state: PRState | 'open' | 'closed' | 'merged' | 'draft' @@ -18,6 +19,7 @@ export type GitHubPRMergeStateInput = { autoMergeEnabled?: boolean autoMergeAllowed?: boolean | null mergeQueueRequired?: boolean | null + inMergeQueue?: boolean } export type GitHubPRAutoMergeAction = { @@ -93,43 +95,45 @@ export function presentGitHubPRMergeState( const autoMergeAction = item.state !== 'open' ? null - : item.autoMergeEnabled === true - ? { - kind: 'disable' as const, - label: translate( - 'auto.components.github.pr.merge.state.48d75ae118', - 'Disable auto-merge' - ), - tooltip: translate( - 'auto.components.github.pr.merge.state.62703b1dc4', - 'GitHub auto-merge is enabled for this pull request' - ) - } - : item.mergeQueueRequired === true + : item.inMergeQueue === true + ? null + : item.autoMergeEnabled === true ? { - kind: 'enable' as const, + kind: 'disable' as const, label: translate( - 'auto.components.github.pr.merge.state.b169f943e1', - 'Merge when ready' + 'auto.components.github.pr.merge.state.48d75ae118', + 'Disable auto-merge' ), tooltip: translate( - 'auto.components.github.pr.merge.state.331ebe1170', - 'Add this pull request to the GitHub merge queue' + 'auto.components.github.pr.merge.state.62703b1dc4', + 'GitHub auto-merge is enabled for this pull request' ) } - : canEnableAutoMerge(item) + : item.mergeQueueRequired === true ? { kind: 'enable' as const, label: translate( - 'auto.components.github.pr.merge.state.4ab19a62ef', - 'Enable auto-merge' + 'auto.components.github.pr.merge.state.b169f943e1', + 'Merge when ready' ), tooltip: translate( - 'auto.components.github.pr.merge.state.8f6cb3772f', - 'Merge this pull request automatically once requirements are met' + 'auto.components.github.pr.merge.state.331ebe1170', + 'Add this pull request to the GitHub merge queue' ) } - : null + : canEnableAutoMerge(item) + ? { + kind: 'enable' as const, + label: translate( + 'auto.components.github.pr.merge.state.4ab19a62ef', + 'Enable auto-merge' + ), + tooltip: translate( + 'auto.components.github.pr.merge.state.8f6cb3772f', + 'Merge this pull request automatically once requirements are met' + ) + } + : null if (item.state === 'merged') { return { @@ -191,6 +195,9 @@ export function presentGitHubPRMergeState( autoMergeAction } } + if (item.inMergeQueue === true) { + return presentPullRequestInMergeQueue(WARNING_TONE) + } if (item.mergeQueueRequired === true) { return { label: item.autoMergeEnabled ? 'Auto-merge on' : 'Merge when ready', diff --git a/src/renderer/src/components/pull-request-page/actions/merge-actions.ts b/src/renderer/src/components/pull-request-page/actions/merge-actions.ts index 27bd98d963c1..19dcca94441a 100644 --- a/src/renderer/src/components/pull-request-page/actions/merge-actions.ts +++ b/src/renderer/src/components/pull-request-page/actions/merge-actions.ts @@ -14,6 +14,7 @@ import type { GitHubWorkItem } from '../../../../../shared/github/work-item-type import type { TaskSourceContext } from '../../../../../shared/task-source-context' import type { PullRequestPageProjectOrigin } from '../page-types' import { translate } from '@/i18n/i18n' +import { githubAutoMergeSuccessToast } from '@/components/github-auto-merge-success-toast' import type { GitHubPRMergeStatePresentation } from '@/components/github-pr-merge-state' export async function changePullRequestState(args: { @@ -252,11 +253,7 @@ export async function setPullRequestAutoMerge(args: { { local: false } ) } - toast.success( - enabled - ? translate('auto.components.PullRequestPage.5edbe7eefa', 'Auto-merge enabled') - : translate('auto.components.PullRequestPage.0f5821b035', 'Auto-merge disabled') - ) + toast.success(githubAutoMergeSuccessToast(enabled, result)) args.onMutated() } catch (err) { toast.error( diff --git a/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx b/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx index 4fd819463a0c..26dad9277798 100644 --- a/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx +++ b/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx @@ -92,7 +92,8 @@ export default function HostedReviewActions({ checksStatus: review.status, autoMergeEnabled: review.autoMergeEnabled, autoMergeAllowed: review.autoMergeAllowed, - mergeQueueRequired: review.mergeQueueRequired + mergeQueueRequired: review.mergeQueueRequired, + inMergeQueue: review.inMergeQueue ?? githubPR?.inMergeQueue }) if (!githubPR?.stack || !stackMergeScope) { return presentation diff --git a/src/renderer/src/components/task-page-linear-jira-list-model.tsx b/src/renderer/src/components/task-page-linear-jira-list-model.tsx index 42cff688a42c..e9bd6afa84d6 100644 --- a/src/renderer/src/components/task-page-linear-jira-list-model.tsx +++ b/src/renderer/src/components/task-page-linear-jira-list-model.tsx @@ -184,6 +184,7 @@ export type TaskPageGitHubWorkItemMutationRunner = { } } | void> successToast?: string + successToastFromResult?: (result: { enqueued?: boolean } | void) => string errorToast: string }) => Promise<'confirmed' | 'rolled_back' | 'stale'> isIntentPending: (input: { diff --git a/src/renderer/src/components/task-page-work-item-signatures.ts b/src/renderer/src/components/task-page-work-item-signatures.ts index 8abb2f07a192..73e1f5be0cdb 100644 --- a/src/renderer/src/components/task-page-work-item-signatures.ts +++ b/src/renderer/src/components/task-page-work-item-signatures.ts @@ -38,6 +38,7 @@ export function taskPageWorkItemStatusSignature(item: GitHubWorkItem): string { item.autoMergeEnabled ?? null, item.autoMergeAllowed ?? null, item.mergeQueueRequired ?? null, + item.inMergeQueue ?? null, item.mergeStateStatus ?? null, item.updatedAt ]) diff --git a/src/renderer/src/components/task-page/github/MergeCell.tsx b/src/renderer/src/components/task-page/github/MergeCell.tsx index 1213c8062a72..10246c4c8a98 100644 --- a/src/renderer/src/components/task-page/github/MergeCell.tsx +++ b/src/renderer/src/components/task-page/github/MergeCell.tsx @@ -11,6 +11,7 @@ import { useAppStore } from '@/store' import { useShallow } from 'zustand/react/shallow' import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' import { translate } from '@/i18n/i18n' +import { githubAutoMergeSuccessToast } from '@/components/github-auto-merge-success-toast' import { presentGitHubPRMergeState } from '@/components/github-pr-merge-state' import { resolveGitHubPRMergeMethods, @@ -165,9 +166,7 @@ export function PRMergeCell({ enabled }, sourceContext, - successToast: enabled - ? translate('auto.components.TaskPage.fed317634c', 'Auto-merge enabled') - : translate('auto.components.TaskPage.a5bf86defe', 'Auto-merge disabled'), + successToastFromResult: (result) => githubAutoMergeSuccessToast(enabled, result), errorToast: enabled ? translate('auto.components.TaskPage.a3318684bc', 'Failed to enable auto-merge') : translate('auto.components.TaskPage.1a9ea003dc', 'Failed to disable auto-merge'), diff --git a/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts b/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts index 24b6a969c68d..d7f95ca6d746 100644 --- a/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts +++ b/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts @@ -51,6 +51,7 @@ export function useTaskPageGitHubWorkItemMutation(args: UseTaskPageGitHubWorkIte sourceContext?: TaskSourceContext | null mutate: () => Promise<{ ok?: boolean; error?: string | { message?: string } } | void> successToast?: string + successToastFromResult?: (result: { enqueued?: boolean } | void) => string errorToast: string serverEntityFromResult?: (result: unknown) => Partial | undefined }) => Promise<'confirmed' | 'rolled_back' | 'stale'> @@ -106,6 +107,7 @@ export function useTaskPageGitHubWorkItemMutation(args: UseTaskPageGitHubWorkIte sourceContext?: TaskSourceContext | null mutate: () => Promise<{ ok?: boolean; error?: string | { message?: string } } | void> successToast?: string + successToastFromResult?: (result: { enqueued?: boolean } | void) => string errorToast: string serverEntityFromResult?: (result: unknown) => Partial | undefined }): Promise<'confirmed' | 'rolled_back' | 'stale'> => { @@ -162,8 +164,9 @@ export function useTaskPageGitHubWorkItemMutation(args: UseTaskPageGitHubWorkIte scheduleQuiet: false }) if (confirmed === 'confirmed') { - if (input.successToast && mountedRef.current) { - toast.success(input.successToast) + const successToast = input.successToastFromResult?.(typed) ?? input.successToast + if (successToast && mountedRef.current) { + toast.success(successToast) } useAppStore.getState().recordFeatureInteraction('github-tasks') } diff --git a/src/shared/github/pull-request-types.ts b/src/shared/github/pull-request-types.ts index 6024d702bf3d..25ce0ed067ad 100644 --- a/src/shared/github/pull-request-types.ts +++ b/src/shared/github/pull-request-types.ts @@ -60,6 +60,8 @@ export type PRInfo = { autoMergeEnabled?: boolean autoMergeAllowed?: boolean | null mergeQueueRequired?: boolean | null + /** True once GitHub has a merge-queue entry. Enqueue does not set autoMergeRequest. */ + inMergeQueue?: boolean mergeMethodSettings?: GitHubPRMergeMethodSettings mergeStateStatus?: string | null /** GitHub-registered stack metadata. Absent for ordinary dependent PR chains. */ diff --git a/src/shared/github/work-item-types.ts b/src/shared/github/work-item-types.ts index 437a1af20f05..740f93a4e442 100644 --- a/src/shared/github/work-item-types.ts +++ b/src/shared/github/work-item-types.ts @@ -46,6 +46,7 @@ export type GitHubWorkItem = { autoMergeEnabled?: boolean autoMergeAllowed?: boolean | null mergeQueueRequired?: boolean | null + inMergeQueue?: boolean mergeMethodSettings?: GitHubPRMergeMethodSettings mergeStateStatus?: string | null maintainerCanModify?: boolean diff --git a/src/shared/hosted-review-github.ts b/src/shared/hosted-review-github.ts index 26d866d5c09a..32eba5571c73 100644 --- a/src/shared/hosted-review-github.ts +++ b/src/shared/hosted-review-github.ts @@ -15,6 +15,7 @@ export function hostedReviewInfoFromGitHubPRInfo(pr: PRInfo): HostedReviewInfo { ...(pr.autoMergeEnabled !== undefined ? { autoMergeEnabled: pr.autoMergeEnabled } : {}), ...(pr.autoMergeAllowed !== undefined ? { autoMergeAllowed: pr.autoMergeAllowed } : {}), ...(pr.mergeQueueRequired !== undefined ? { mergeQueueRequired: pr.mergeQueueRequired } : {}), + ...(pr.inMergeQueue !== undefined ? { inMergeQueue: pr.inMergeQueue } : {}), ...(pr.mergeStateStatus !== undefined ? { mergeStateStatus: pr.mergeStateStatus } : {}), ...(pr.headSha ? { headSha: pr.headSha } : {}), ...(pr.prRepo ? { githubRepository: pr.prRepo } : {}), diff --git a/src/shared/hosted-review.ts b/src/shared/hosted-review.ts index 719c6d27fa53..541f94b3e3e8 100644 --- a/src/shared/hosted-review.ts +++ b/src/shared/hosted-review.ts @@ -40,6 +40,7 @@ export type HostedReviewInfo = { autoMergeEnabled?: boolean autoMergeAllowed?: boolean | null mergeQueueRequired?: boolean | null + inMergeQueue?: boolean mergeStateStatus?: string | null headSha?: string /** GitHub repository that owns the PR; absent on older runtimes and other providers. */ From 90234c752ee08d7ba7619f30eed2c679b3c9f8ab Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 1 Oct 2026 19:56:11 +0900 Subject: [PATCH 3/5] fix(github): keep an enqueue result assignable to the success toast The toast parameter is a weak type, so an ok-only object and a cast that dropped enqueued failed the web typecheck. The hosted review reads inMergeQueue from the pull request that already carries it. Co-authored-by: Cursor --- .../components/github-pr-merge-state.test.ts | 2 +- .../right-sidebar/HostedReviewActions.tsx | 3 +-- .../hooks/useTaskPageGitHubWorkItemMutation.ts | 18 +++++++++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/github-pr-merge-state.test.ts b/src/renderer/src/components/github-pr-merge-state.test.ts index 1e4219937739..528d6be1c075 100644 --- a/src/renderer/src/components/github-pr-merge-state.test.ts +++ b/src/renderer/src/components/github-pr-merge-state.test.ts @@ -27,7 +27,7 @@ describe('presentGitHubPRMergeState', () => { it('says a disallowed merge queue was enqueued, not auto-merged', () => { expect(githubAutoMergeSuccessToast(true, { enqueued: true })).toBe('Added to the merge queue') - expect(githubAutoMergeSuccessToast(true, { ok: true })).toBe('Auto-merge enabled') + expect(githubAutoMergeSuccessToast(true, {})).toBe('Auto-merge enabled') }) it('hides enqueue once the pull request is already in the merge queue', () => { diff --git a/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx b/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx index 26dad9277798..4fd819463a0c 100644 --- a/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx +++ b/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx @@ -92,8 +92,7 @@ export default function HostedReviewActions({ checksStatus: review.status, autoMergeEnabled: review.autoMergeEnabled, autoMergeAllowed: review.autoMergeAllowed, - mergeQueueRequired: review.mergeQueueRequired, - inMergeQueue: review.inMergeQueue ?? githubPR?.inMergeQueue + mergeQueueRequired: review.mergeQueueRequired }) if (!githubPR?.stack || !stackMergeScope) { return presentation diff --git a/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts b/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts index d7f95ca6d746..96c01f1923c8 100644 --- a/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts +++ b/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts @@ -49,7 +49,11 @@ export function useTaskPageGitHubWorkItemMutation(args: UseTaskPageGitHubWorkIte item: GitHubWorkItem intent: TaskPageGitHubMutationIntent sourceContext?: TaskSourceContext | null - mutate: () => Promise<{ ok?: boolean; error?: string | { message?: string } } | void> + mutate: () => Promise<{ + ok?: boolean + error?: string | { message?: string } + enqueued?: boolean + } | void> successToast?: string successToastFromResult?: (result: { enqueued?: boolean } | void) => string errorToast: string @@ -105,7 +109,11 @@ export function useTaskPageGitHubWorkItemMutation(args: UseTaskPageGitHubWorkIte item: GitHubWorkItem intent: TaskPageGitHubMutationIntent sourceContext?: TaskSourceContext | null - mutate: () => Promise<{ ok?: boolean; error?: string | { message?: string } } | void> + mutate: () => Promise<{ + ok?: boolean + error?: string | { message?: string } + enqueued?: boolean + } | void> successToast?: string successToastFromResult?: (result: { enqueued?: boolean } | void) => string errorToast: string @@ -129,7 +137,11 @@ export function useTaskPageGitHubWorkItemMutation(args: UseTaskPageGitHubWorkIte try { const result = await input.mutate() const activeQuery = activeQueryRef.current - const typed = result as { ok?: boolean; error?: string | { message?: string } } | void + const typed = result as { + ok?: boolean + error?: string | { message?: string } + enqueued?: boolean + } | void if (typed && typeof typed === 'object' && typed.ok === false) { const rolled = rollbackTaskPageGitHubWorkItemMutation({ key: began.key, From febd87e818e19036083361f9371f2e2ff3cf78f0 Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 1 Oct 2026 20:43:39 +0900 Subject: [PATCH 4/5] fix(github): confirm a merge-queue enqueue as queued, not auto-merge Co-authored-by: Cursor --- ...age-github-work-item-mutation-lifecycle.ts | 3 +++ ...hub-work-item-mutation-regressions.test.ts | 26 +++++++++++++++++++ .../components/task-page/github/MergeCell.tsx | 7 +++++ 3 files changed, 36 insertions(+) diff --git a/src/renderer/src/components/task-page-github-work-item-mutation-lifecycle.ts b/src/renderer/src/components/task-page-github-work-item-mutation-lifecycle.ts index 8a6933816284..115714812b7e 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutation-lifecycle.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutation-lifecycle.ts @@ -55,6 +55,9 @@ function applyServerEntityIfPresent( opts.serverEntity.autoMergeEnabled ) } + if (opts.serverEntity.inMergeQueue !== undefined) { + entityPatch.inMergeQueue = opts.serverEntity.inMergeQueue + } if (opts.serverEntity.assignees) { const users = freezeTaskPageGitHubUsers(opts.serverEntity.assignees) setConfirmedListSnapshot(key.sourceScope, key.repoId, key.itemId, 'assignees', users) diff --git a/src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts b/src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts index a24a20e33b63..ef772026551c 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts @@ -370,4 +370,30 @@ describe('TaskPage GitHub mutation regressions', () => { expect(patches.findLast((patch) => patch.state !== undefined)?.state).toBe('closed') expect(patches.at(-1)?.assignees).toBeUndefined() }) + + it('confirms an enqueue as queued rather than auto-merge enabled', () => { + const patches: Partial[] = [] + const patchWorkItem = (_id: string, patch: Partial): void => { + patches.push(patch) + } + const base = item({ type: 'pr', autoMergeEnabled: false, inMergeQueue: false }) + const began = beginTaskPageGitHubWorkItemMutation({ + item: base, + intent: { type: 'setAutoMerge', enabled: true }, + query: query(), + queryKey: 'q', + viewerLogin: 'me', + patchWorkItem + }) + confirmTaskPageGitHubWorkItemMutation(began.key, began.generation, { + query: query(), + queryKey: 'q', + viewerLogin: 'me', + item: base, + patchWorkItem, + serverEntity: { autoMergeEnabled: false, inMergeQueue: true } + }) + expect(patches.at(-1)).toMatchObject({ autoMergeEnabled: false, inMergeQueue: true }) + expect(getLastConfirmedClientValue(null, base.repoId, base.id, 'autoMerge')).toBe(false) + }) }) diff --git a/src/renderer/src/components/task-page/github/MergeCell.tsx b/src/renderer/src/components/task-page/github/MergeCell.tsx index 10246c4c8a98..508127394079 100644 --- a/src/renderer/src/components/task-page/github/MergeCell.tsx +++ b/src/renderer/src/components/task-page/github/MergeCell.tsx @@ -167,6 +167,13 @@ export function PRMergeCell({ }, sourceContext, successToastFromResult: (result) => githubAutoMergeSuccessToast(enabled, result), + serverEntityFromResult: (result) => { + const typed = result as { enqueued?: boolean } | void + if (!typed || typed.enqueued !== true) { + return undefined + } + return { autoMergeEnabled: false, inMergeQueue: true } + }, errorToast: enabled ? translate('auto.components.TaskPage.a3318684bc', 'Failed to enable auto-merge') : translate('auto.components.TaskPage.1a9ea003dc', 'Failed to disable auto-merge'), From 21bc876eeb94613870e69b9e06a2dffdfb0d062f Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 1 Oct 2026 21:25:02 +0900 Subject: [PATCH 5/5] fix(github): accept the merge-queue entity on the mutation runner MergeCell already returns the queued work item from the enqueue result. The runner type omitted that callback, so the web typecheck rejected the call. --- src/renderer/src/components/task-page-linear-jira-list-model.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/src/components/task-page-linear-jira-list-model.tsx b/src/renderer/src/components/task-page-linear-jira-list-model.tsx index e9bd6afa84d6..0c71669b6452 100644 --- a/src/renderer/src/components/task-page-linear-jira-list-model.tsx +++ b/src/renderer/src/components/task-page-linear-jira-list-model.tsx @@ -185,6 +185,7 @@ export type TaskPageGitHubWorkItemMutationRunner = { } | void> successToast?: string successToastFromResult?: (result: { enqueued?: boolean } | void) => string + serverEntityFromResult?: (result: unknown) => Partial | undefined errorToast: string }) => Promise<'confirmed' | 'rolled_back' | 'stale'> isIntentPending: (input: {