authorgravatar for 97427849+flamboh@users.noreply.github.comoliver <97427849+flamboh@users.noreply.github.com> 2026-10-03 18:40:58-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-03 22:40:58-03:00
logf90b77d80971ea4855a42abfc89ce85466e1accb
tree2eeebbb3aedcf0ec56efe898786c718e82be8bc6
parent237d51e6cd2f935d3ddced4efe8fcd4a5d697e43
signature Signed by PGP key B5690EEEBB952194

fix(web): Pull request panel entry works for linked PRs (#15061)


3 files changed, 87 insertions(+), 5 deletions(-)

apps/web/src/components/ChatView.tsx+18-5
......@@ -266,7 +266,10 @@ import {
266266 selectThreadPreviewMiniPlayer,
267267 usePreviewMiniPlayerStore,
268268} from "../previewMiniPlayerStore";
269import { pullRequestPanelContext } from "./pullRequest/pullRequestDetail.logic";
269import {
270 pullRequestPanelContext,
271 threadPullRequestPanelTarget,
272} from "./pullRequest/pullRequestDetail.logic";
270273import { PullRequestDetailPanel } from "./pullRequest/PullRequestDetailPanel";
271274import { PullRequestDetailGhost } from "./pullRequest/PullRequestGhosts";
272275import { PullRequestsUnavailableState } from "./pullRequest/PullRequestsUnavailableState";
......@@ -6776,12 +6779,20 @@ export default function ChatView(props: ChatViewProps) {
67766779 },
67776780 );
67786781 }, [activeThreadReferenceCopyTarget]);
6782 const pullRequestPanelTarget = activeThread
6783 ? threadPullRequestPanelTarget({
6784 projectId: activeThread.projectId,
6785 pullRequests: visiblePullRequests,
6786 linkedPullRequest: linkedThreadPullRequest,
6787 branchPullRequest: activeThreadShell?.branchPullRequest ?? activeThread.branchPullRequest,
6788 })
6789 : null;
67796790 const addPullRequestSurface = useCallback(() => {
6780 if (!supportsPullRequests || activeThreadRef === null || linkedThreadPullRequest === null)
6791 if (!supportsPullRequests || activeThreadRef === null || pullRequestPanelTarget === null)
67816792 return;
6782 useRightPanelStore.getState().openPullRequest(activeThreadRef, linkedThreadPullRequest);
6783 }, [activeThreadRef, linkedThreadPullRequest, supportsPullRequests]);
6784 const pullRequestSurfaceAvailable = supportsPullRequests && linkedThreadPullRequest !== null;
6793 useRightPanelStore.getState().openPullRequest(activeThreadRef, pullRequestPanelTarget);
6794 }, [activeThreadRef, pullRequestPanelTarget, supportsPullRequests]);
6795 const pullRequestSurfaceAvailable = supportsPullRequests && pullRequestPanelTarget !== null;
67856796 const supportsSettlement = serverConfig?.environment.capabilities.threadSettlement === true;
67866797 const supportsSnooze = serverConfig?.environment.capabilities.threadSnooze === true;
67876798 const supportsPinning = serverConfig?.environment.capabilities.threadPinning === true;
......@@ -10535,6 +10546,8 @@ export default function ChatView(props: ChatViewProps) {
1053510546 projectId: activeThread.projectId,
1053610547 pullRequests: visiblePullRequests,
1053710548 linkedPullRequest: linkedThreadPullRequest,
10549 branchPullRequest:
10550 activeThreadShell?.branchPullRequest ?? activeThread.branchPullRequest,
1053810551 },
1053910552 renderedRightPanelSurface,
1054010553 )}
apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts+40
......@@ -35,6 +35,7 @@ import {
3535 isStackedPullRequestBase,
3636 loadingPullRequestCheckoutCommand,
3737 pullRequestPanelContext,
38 threadPullRequestPanelTarget,
3839 latestPullRequestReviewOutcomes,
3940 newestPullRequestCommitAt,
4041 mergePullRequestThreadComments,
......@@ -1535,6 +1536,45 @@ describe("pull request panel context beside a thread", () => {
15351536 );
15361537 expect(pullRequestPanelContext({ projectId: null }, surface(3))).toBe("page");
15371538 });
1539
1540 describe("the Pull request entry's target", () => {
1541 const legacy = (number: number) => ({
1542 projectId: ProjectId.make("proj-a"),
1543 repository: "pingdotgg/t3code",
1544 number,
1545 url: `https://github.com/pingdotgg/t3code/pull/${number}`,
1546 });
1547
1548 it("opens a linked pull request the legacy field never named, ahead of the branch PR", () => {
1549 expect(
1550 threadPullRequestPanelTarget({
1551 projectId: "proj-a",
1552 pullRequests: [link(15046, { source: "agent" })],
1553 linkedPullRequest: null,
1554 branchPullRequest: legacy(30),
1555 }),
1556 ).toEqual({ ...legacy(15046), host: "github.com" });
1557 });
1558
1559 it("reuses the legacy reference only for the same pull request on the same host", () => {
1560 const linkedPullRequest = legacy(15046);
1561 const onHost = (host: string) =>
1562 threadPullRequestPanelTarget({
1563 projectId: "proj-a",
1564 pullRequests: [link(15046, { host, url: `https://${host}/pingdotgg/t3code/pull/15046` })],
1565 linkedPullRequest,
1566 });
1567 expect(onHost("github.com")).toBe(linkedPullRequest);
1568 expect(onHost("github.example.com")).toMatchObject({ host: "github.example.com" });
1569 });
1570
1571 it("falls back to the branch pull request when the thread holds no link", () => {
1572 expect(
1573 threadPullRequestPanelTarget({ projectId: "proj-a", branchPullRequest: legacy(3) }),
1574 ).toEqual(legacy(3));
1575 expect(threadPullRequestPanelTarget({ projectId: "proj-a", pullRequests: [] })).toBeNull();
1576 });
1577 });
15381578});
15391579
15401580describe("which actions need the host read again after they run", () => {
apps/web/src/components/pullRequest/pullRequestDetail.logic.ts+29
......@@ -27,6 +27,8 @@ import {
2727 type VcsRef,
2828} from "@t3tools/contracts";
2929import {
30 legacyThreadPullRequestKey,
31 resolveThreadCurrentPullRequestLink,
3032 threadPullRequestKeysEqual,
3133 visibleThreadPullRequests,
3234} from "@t3tools/shared/threadPullRequests";
......@@ -237,6 +239,33 @@ export function pullRequestPanelContext(
237239 : "page";
238240}
239241
242export function threadPullRequestPanelTarget(thread: {
243 readonly projectId: string;
244 readonly pullRequests?: ReadonlyArray<ThreadPullRequestLink> | undefined;
245 readonly linkedPullRequest?: ThreadLinkedPullRequest | null | undefined;
246 readonly branchPullRequest?: ThreadLinkedPullRequest | null | undefined;
247}) {
248 const current = resolveThreadCurrentPullRequestLink(thread.pullRequests ?? []);
249 const legacy = thread.linkedPullRequest;
250 if (
251 current !== null &&
252 legacy != null &&
253 threadPullRequestKeysEqual(current, legacyThreadPullRequestKey(legacy))
254 ) {
255 return legacy;
256 }
257 if (current !== null) {
258 return {
259 projectId: thread.projectId,
260 host: current.host,
261 repository: current.repository,
262 number: current.number,
263 url: current.url,
264 };
265 }
266 return legacy ?? thread.branchPullRequest ?? null;
267}
268
240269/** Names where a pull-request task will land, without letting each surface guess independently. */
241270export function pullRequestHandoffLabels(inThisThread: boolean) {
242271 return inThisThread