From ed8e259789fcebcc9fced0c25d1a9c267ba76a69 Mon Sep 17 00:00:00 2001 From: badcuban <108198679+badcuban@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:10:17 -0400 Subject: [PATCH 1/3] fix(web): composer pull request row loads whole, closes, and follows the merge queue Four things the docked row got wrong once it was in daily use. It drew in two beats: the number and chip first, then the project, branch and diff stat when the detail read landed seconds later. The listing behind the thread's own pull request already knows the branch and the size, and the project is the thread's own, so the row now draws whole from those and lets the detail sharpen them. It could not be closed. A close control hides the row for that pull request in that thread for the session; a thread moving on to another pull request gets the row back. "Merge when checks pass" took seconds to take a click, because the switch waited for the host to arm the merge and be re-read. It now flips on the click and rolls back only if the host refuses. Once GitHub takes a pull request into its merge queue it drops the auto-merge instruction, so the switch read as off while the chip said Queued, and flipping it re-armed and disarmed the queue entry. The popover now shows "In the merge queue" instead of the switch. The detail read also keeps polling while a pull request is armed with nothing in its way, so the chip sees the queue take it and the merge land instead of waiting on the slow listing. --- apps/web/src/components/ChatView.browser.tsx | 11 +++ .../chat/ComposerPullRequestRow.tsx | 75 +++++++++++++++---- .../chat/composerPullRequest.logic.test.ts | 68 +++++++++++++---- .../chat/composerPullRequest.logic.ts | 66 +++++++++++----- .../chat/composerPullRequestDismissals.ts | 43 +++++++++++ .../pull-requests/pullRequests.logic.test.ts | 18 ++++- .../pull-requests/pullRequests.logic.ts | 29 ++++++- .../components/sidebar/InboxRows.browser.tsx | 2 + .../routes/_chat.$environmentId.$threadId.tsx | 38 +++++++++- 9 files changed, 293 insertions(+), 57 deletions(-) create mode 100644 apps/web/src/components/chat/composerPullRequestDismissals.ts diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index 635727a0..aa05f576 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -3034,6 +3034,17 @@ describe("ChatView timeline estimator parity (full app)", () => { dock!.getBoundingClientRect().bottom - composerSurface!.getBoundingClientRect().top, ), ).toBeLessThan(2); + + // Closing the row takes it off the composer; the notice stays docked. + row.querySelector('button[aria-label^="Hide pull request"]')!.click(); + await waitForElement( + () => + document.querySelector('[data-composer-pull-request-row="true"]') === null + ? document.querySelector('[data-composer-notice-dock="true"]') + : null, + "The pull request row did not leave the composer.", + ); + expect(document.querySelector("[data-composer-notice-severity]")).toBeTruthy(); } finally { await mounted.cleanup(); } diff --git a/apps/web/src/components/chat/ComposerPullRequestRow.tsx b/apps/web/src/components/chat/ComposerPullRequestRow.tsx index 745939ed..4f4699f9 100644 --- a/apps/web/src/components/chat/ComposerPullRequestRow.tsx +++ b/apps/web/src/components/chat/ComposerPullRequestRow.tsx @@ -18,13 +18,16 @@ import type { PullRequestRef, } from "@threadlines/contracts"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { ChevronDownIcon, ExternalLinkIcon } from "lucide-react"; +import { ChevronDownIcon, ExternalLinkIcon, XIcon } from "lucide-react"; import { useState } from "react"; import { isElectron } from "../../env"; import { useSettings, updateSettings } from "../../hooks/useSettings"; import { readLocalApi } from "../../localApi"; -import { pullRequestActionMutationOptions } from "../../lib/pullRequestsReactQuery"; +import { + pullRequestActionMutationOptions, + pullRequestQueryKeys, +} from "../../lib/pullRequestsReactQuery"; import { cn } from "../../lib/utils"; import { PullRequestHoverCard, @@ -36,7 +39,7 @@ import { Checkbox } from "../ui/checkbox"; import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; import { DiffStatLabel } from "./DiffStatLabel"; import { - canToggleComposerAutoMerge, + composerAutoMergeControl, composerPullRequestCheckBuckets, composerPullRequestRow, pullRequestChecksUrl, @@ -49,10 +52,14 @@ export interface ComposerPullRequest { readonly reference: PullRequestRef; /** What the sidebar badge and the tab already resolved. */ readonly pullRequest: ThreadPullRequest; + /** The thread's project, which is the pull request's too. */ + readonly projectTitle: string | null; /** The shared read behind the Pull request tab; absent until it lands. */ readonly detail: PullRequestDetail | undefined; /** Opens the Pull request tab, the same place the sidebar badge goes. */ readonly onOpen: () => void; + /** Closes the row for this pull request in this thread. */ + readonly onDismiss: () => void; } const CHIP_TONE_CLASS: Readonly< @@ -78,6 +85,7 @@ export function ComposerPullRequestRow({ }) { const row = composerPullRequestRow({ pullRequest: pullRequest.pullRequest, + projectTitle: pullRequest.projectTitle, detail: pullRequest.detail, }); const tone = pullRequestBadgeTone(row.state, row.isDraft, row.autoMergeEnabled); @@ -132,6 +140,14 @@ export function ComposerPullRequestRow({ chip={row.chip} checksUrl={pullRequestChecksUrl(row.url)} /> + ); } @@ -212,15 +228,39 @@ function ComposerPullRequestChecksPopover({ const detail = pullRequest.detail; const buckets = composerPullRequestCheckBuckets(detail?.checks ?? []); const wrapUpOnSettled = useSettings((settings) => settings.wrapUpThreadsOnPullRequestSettled); - const autoMergeEnabled = detail?.autoMergeEnabled === true; - const canToggleAutoMerge = canToggleComposerAutoMerge(detail); - const action = useMutation( - pullRequestActionMutationOptions({ - environmentId: pullRequest.environmentId, - reference: pullRequest.reference, - queryClient, - }), + const autoMergeControl = composerAutoMergeControl(detail); + const detailQueryKey = pullRequestQueryKeys.detail( + pullRequest.environmentId, + pullRequest.reference.projectId, + pullRequest.reference.number, ); + const actionOptions = pullRequestActionMutationOptions({ + environmentId: pullRequest.environmentId, + reference: pullRequest.reference, + queryClient, + }); + const action = useMutation({ + ...actionOptions, + // The switch flips the moment it is clicked. The host takes seconds to arm + // the merge and seconds more to be re-read, and a switch that waits for + // both reads as one that did not take the click. If the host refuses, the + // detail it was read from comes back. + onMutate: (variables) => { + const previous = queryClient.getQueryData(detailQueryKey); + if (previous && variables.action.endsWith("auto-merge")) { + queryClient.setQueryData(detailQueryKey, { + ...previous, + autoMergeEnabled: variables.action === "enable-auto-merge", + }); + } + return { previous }; + }, + onError: (_error, _variables, context) => { + if (context?.previous) { + queryClient.setQueryData(detailQueryKey, context.previous); + } + }, + }); return (
@@ -270,12 +310,11 @@ function ComposerPullRequestChecksPopover({ }) )}
- {canToggleAutoMerge ? ( + {autoMergeControl.kind === "toggle" ? ( ) : null} + {autoMergeControl.kind === "queued" ? ( + // The host has taken it: there is no instruction left to switch off, + // and the queue lands it on its own. +

+ + In the merge queue +

+ ) : null}