authorgravatar for 97427849+flamboh@users.noreply.github.comoliver <97427849+flamboh@users.noreply.github.com> 2026-09-02 15:38:47-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 18:38:47-04:00
log1eb36b45ef7aa5c028380afb7266bcabf258a55e
tree366ddc5395c84f2108158abadb61fe63435f5c65
parentba3cb0773859334fe9f75016295bb3893c9e6044
signature Signed by PGP key B5690EEEBB952194

fix(web): show pull request state icons in tabs (#9112)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

7 files changed, 86 insertions(+), 134 deletions(-)

apps/web/src/components/ChatView.tsx+2-20
...@@ -150,7 +150,6 @@ import {...@@ -150,7 +150,6 @@ import {
150 selectActiveRightPanelSurface,150 selectActiveRightPanelSurface,
151 selectThreadRightPanelState,151 selectThreadRightPanelState,
152 type RightPanelSurface,152 type RightPanelSurface,
153 updatePullRequestTabStatus,
154 useRightPanelStore,153 useRightPanelStore,
155} from "../rightPanelStore";154} from "../rightPanelStore";
156import {155import {
...@@ -1786,21 +1785,6 @@ function ChatViewContent(props: ChatViewProps) {...@@ -1786,21 +1785,6 @@ function ChatViewContent(props: ChatViewProps) {
1786 const activeRightPanelSurface = useRightPanelStore((state) =>1785 const activeRightPanelSurface = useRightPanelStore((state) =>
1787 selectActiveRightPanelSurface(state.byThreadKey, activeThreadRef),1786 selectActiveRightPanelSurface(state.byThreadKey, activeThreadRef),
1788 );1787 );
1789 const [pullRequestTabStatuses, setPullRequestTabStatuses] = useState<
1790 Record<string, PullRequestTabStatus>
1791 >({});
1792 // Keyed by the surface the panel is showing rather than by a key rebuilt from the status, so
1793 // the tab is found again whether or not that surface was opened with an environment on it.
1794 const activePullRequestSurfaceId =
1795 activeRightPanelSurface?.kind === "pull-request" ? activeRightPanelSurface.id : undefined;
1796 const updatePullRequestTabStatusFromPanel = useCallback(
1797 (status: PullRequestTabStatus) => {
1798 const id = activePullRequestSurfaceId;
1799 if (id === undefined) return;
1800 setPullRequestTabStatuses((current) => updatePullRequestTabStatus(current, id, status));
1801 },
1802 [activePullRequestSurfaceId],
1803 );
1804 const refreshVcsStatus = useAtomCommand(vcsEnvironment.refreshStatus, { reportFailure: false });1788 const refreshVcsStatus = useAtomCommand(vcsEnvironment.refreshStatus, { reportFailure: false });
1805 const sidebarPrRefreshKeyRef = useRef<string | null>(null);1789 const sidebarPrRefreshKeyRef = useRef<string | null>(null);
1806 const activePreviewState = useThreadPreviewState(activeThreadRef);1790 const activePreviewState = useThreadPreviewState(activeThreadRef);
...@@ -4683,7 +4667,6 @@ function ChatViewContent(props: ChatViewProps) {...@@ -4683,7 +4667,6 @@ function ChatViewContent(props: ChatViewProps) {
4683 });4667 });
4684 const handlePullRequestTabStatusChange = useCallback(4668 const handlePullRequestTabStatusChange = useCallback(
4685 (status: PullRequestTabStatus) => {4669 (status: PullRequestTabStatus) => {
4686 updatePullRequestTabStatusFromPanel(status);
4687 const source = threadPullRequestRefreshSource({4670 const source = threadPullRequestRefreshSource({
4688 panel: status,4671 panel: status,
4689 thread: {4672 thread: {
...@@ -4734,7 +4717,6 @@ function ChatViewContent(props: ChatViewProps) {...@@ -4734,7 +4717,6 @@ function ChatViewContent(props: ChatViewProps) {
4734 linkedThreadPullRequest,4717 linkedThreadPullRequest,
4735 refreshVcsStatus,4718 refreshVcsStatus,
4736 threadRepository,4719 threadRepository,
4737 updatePullRequestTabStatusFromPanel,
4738 ],4720 ],
4739 );4721 );
4740 const activeThreadReferenceCopyTarget = useMemo(4722 const activeThreadReferenceCopyTarget = useMemo(
...@@ -7719,6 +7701,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -7719,6 +7701,7 @@ function ChatViewContent(props: ChatViewProps) {
7719 open={rightPanelOpen}7701 open={rightPanelOpen}
7720 maximized={rightPanelMaximized}7702 maximized={rightPanelMaximized}
7721 surfaces={renderedRightPanelSurfaces}7703 surfaces={renderedRightPanelSurfaces}
7704 environmentId={activeThreadRef.environmentId}
7722 activeSurfaceId={renderedRightPanelSurface?.id ?? null}7705 activeSurfaceId={renderedRightPanelSurface?.id ?? null}
7723 pendingSurfaceIds={pendingFileSurfaceIds}7706 pendingSurfaceIds={pendingFileSurfaceIds}
7724 previewSessions={activePreviewState.sessions}7707 previewSessions={activePreviewState.sessions}
...@@ -7744,7 +7727,6 @@ function ChatViewContent(props: ChatViewProps) {...@@ -7744,7 +7727,6 @@ function ChatViewContent(props: ChatViewProps) {
7744 filesAvailable={activeProject !== null}7727 filesAvailable={activeProject !== null}
7745 pullRequestAvailable={pullRequestSurfaceAvailable}7728 pullRequestAvailable={pullRequestSurfaceAvailable}
7746 agentsAvailable7729 agentsAvailable
7747 pullRequestStatuses={pullRequestTabStatuses}
7748 liveAgentCount={agentPanelModel.liveCount}7730 liveAgentCount={agentPanelModel.liveCount}
7749 >7731 >
7750 {rightPanelContent}7732 {rightPanelContent}
...@@ -7768,6 +7750,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -7768,6 +7750,7 @@ function ChatViewContent(props: ChatViewProps) {
7768 ) : null7750 ) : null
7769 }7751 }
7770 surfaces={renderedRightPanelSurfaces}7752 surfaces={renderedRightPanelSurfaces}
7753 environmentId={activeThreadRef.environmentId}
7771 activeSurfaceId={renderedRightPanelSurface?.id ?? null}7754 activeSurfaceId={renderedRightPanelSurface?.id ?? null}
7772 pendingSurfaceIds={pendingFileSurfaceIds}7755 pendingSurfaceIds={pendingFileSurfaceIds}
7773 previewSessions={activePreviewState.sessions}7756 previewSessions={activePreviewState.sessions}
...@@ -7793,7 +7776,6 @@ function ChatViewContent(props: ChatViewProps) {...@@ -7793,7 +7776,6 @@ function ChatViewContent(props: ChatViewProps) {
7793 filesAvailable={activeProject !== null}7776 filesAvailable={activeProject !== null}
7794 pullRequestAvailable={pullRequestSurfaceAvailable}7777 pullRequestAvailable={pullRequestSurfaceAvailable}
7795 agentsAvailable7778 agentsAvailable
7796 pullRequestStatuses={pullRequestTabStatuses}
7797 liveAgentCount={agentPanelModel.liveCount}7779 liveAgentCount={agentPanelModel.liveCount}
7798 >7780 >
7799 {rightPanelContent}7781 {rightPanelContent}
apps/web/src/components/RightPanelTabs.test.tsx+1
...@@ -97,6 +97,7 @@ function renderTabs(...@@ -97,6 +97,7 @@ function renderTabs(
97 <RightPanelTabs97 <RightPanelTabs
98 mode="inline"98 mode="inline"
99 surfaces={second ? [previewSurface, secondSurface] : [previewSurface]}99 surfaces={second ? [previewSurface, secondSurface] : [previewSurface]}
100 environmentId={null}
100 activeSurfaceId={previewSurface.id}101 activeSurfaceId={previewSurface.id}
101 pendingSurfaceIds={new Set()}102 pendingSurfaceIds={new Set()}
102 previewSessions={sessions}103 previewSessions={sessions}
apps/web/src/components/RightPanelTabs.tsx+63-19
...@@ -1,4 +1,10 @@...@@ -1,4 +1,10 @@
1import type { ContextMenuItem, PreviewSessionSnapshot, PullRequestState } from "@t3tools/contracts";1import type {
2 ContextMenuItem,
3 EnvironmentId,
4 PreviewSessionSnapshot,
5 ProjectId,
6 PullRequestState,
7} from "@t3tools/contracts";
2import { getTerminalLabel } from "@t3tools/shared/terminalLabels";8import { getTerminalLabel } from "@t3tools/shared/terminalLabels";
3import {9import {
4 Bot,10 Bot,
...@@ -45,12 +51,15 @@ import { ScrollArea } from "~/components/ui/scroll-area";...@@ -45,12 +51,15 @@ import { ScrollArea } from "~/components/ui/scroll-area";
45import { PanelTabCloseButton } from "~/components/ui/panel-tab-close-button";51import { PanelTabCloseButton } from "~/components/ui/panel-tab-close-button";
46import { faviconUrlForOrigin } from "~/lib/favicon";52import { faviconUrlForOrigin } from "~/lib/favicon";
47import { useTheme } from "~/hooks/useTheme";53import { useTheme } from "~/hooks/useTheme";
54import { pullRequestEnvironment } from "~/state/pullRequests";
55import { useEnvironmentQuery } from "~/state/query";
48import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";56import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
4957
50import { PreviewPanelShell, type PreviewPanelMode } from "./preview/PreviewPanelShell";58import { PreviewPanelShell, type PreviewPanelMode } from "./preview/PreviewPanelShell";
51import { FaviconImage } from "./preview/PreviewFaviconIcon";59import { FaviconImage } from "./preview/PreviewFaviconIcon";
52import { previewBridge } from "./preview/previewBridge";60import { previewBridge } from "./preview/previewBridge";
53import { PierreEntryIcon } from "./chat/PierreEntryIcon";61import { PierreEntryIcon } from "./chat/PierreEntryIcon";
62import { resolvePullRequestState } from "./pullRequest/pullRequestPresentation";
5463
55interface RightPanelTabsProps {64interface RightPanelTabsProps {
56 mode: PreviewPanelMode;65 mode: PreviewPanelMode;
...@@ -62,6 +71,8 @@ interface RightPanelTabsProps {...@@ -62,6 +71,8 @@ interface RightPanelTabsProps {
62 defaultWidth?: number;71 defaultWidth?: number;
63 layoutControls?: ReactNode;72 layoutControls?: ReactNode;
64 surfaces: readonly RightPanelSurface[];73 surfaces: readonly RightPanelSurface[];
74 /** Fallback environment for surfaces that do not carry their own. */
75 environmentId: EnvironmentId | null;
65 activeSurfaceId: string | null;76 activeSurfaceId: string | null;
66 pendingSurfaceIds: ReadonlySet<string>;77 pendingSurfaceIds: ReadonlySet<string>;
67 previewSessions: Readonly<Record<string, PreviewSessionSnapshot>>;78 previewSessions: Readonly<Record<string, PreviewSessionSnapshot>>;
...@@ -97,7 +108,7 @@ interface RightPanelTabsProps {...@@ -97,7 +108,7 @@ interface RightPanelTabsProps {
97 filesAvailable: boolean;108 filesAvailable: boolean;
98 pullRequestAvailable: boolean;109 pullRequestAvailable: boolean;
99 agentsAvailable: boolean;110 agentsAvailable: boolean;
100 pullRequestStatuses?: Readonly<Record<string, PullRequestTabStatus>>;111 pullRequestStatusSeeds?: Readonly<Record<string, PullRequestTabStatusSeed>>;
101 /** Running + waiting subagents; badges the Agents card in the empty state. */112 /** Running + waiting subagents; badges the Agents card in the empty state. */
102 liveAgentCount: number;113 liveAgentCount: number;
103 children: ReactNode;114 children: ReactNode;
...@@ -111,6 +122,8 @@ export interface PullRequestTabStatus {...@@ -111,6 +122,8 @@ export interface PullRequestTabStatus {
111 isDraft: boolean;122 isDraft: boolean;
112}123}
113124
125export type PullRequestTabStatusSeed = Pick<PullRequestTabStatus, "state" | "isDraft">;
126
114export function shouldOpenDefaultBrowserProfileFromMenuClick(127export function shouldOpenDefaultBrowserProfileFromMenuClick(
115 pointerType: string | undefined,128 pointerType: string | undefined,
116): boolean {129): boolean {
...@@ -570,13 +583,15 @@ function SurfaceIcon({...@@ -570,13 +583,15 @@ function SurfaceIcon({
570 sessions,583 sessions,
571 desktopByTabId,584 desktopByTabId,
572 theme,585 theme,
573 pullRequestStatuses,586 environmentId,
587 pullRequestStatusSeeds,
574}: {588}: {
575 surface: RightPanelSurface;589 surface: RightPanelSurface;
576 sessions: Readonly<Record<string, PreviewSessionSnapshot>>;590 sessions: Readonly<Record<string, PreviewSessionSnapshot>>;
577 desktopByTabId: Readonly<Record<string, DesktopPreviewOverlay>>;591 desktopByTabId: Readonly<Record<string, DesktopPreviewOverlay>>;
578 theme: "light" | "dark";592 theme: "light" | "dark";
579 pullRequestStatuses: Readonly<Record<string, PullRequestTabStatus>> | undefined;593 environmentId: EnvironmentId | null;
594 pullRequestStatusSeeds: Readonly<Record<string, PullRequestTabStatusSeed>> | undefined;
580}) {595}) {
581 switch (surface.kind) {596 switch (surface.kind) {
582 case "preview": {597 case "preview": {
...@@ -602,25 +617,53 @@ function SurfaceIcon({...@@ -602,25 +617,53 @@ function SurfaceIcon({
602 );617 );
603 case "terminal":618 case "terminal":
604 return <TerminalSquare className="size-3 shrink-0" />;619 return <TerminalSquare className="size-3 shrink-0" />;
605 case "pull-request": {620 case "pull-request":
606 const status = pullRequestStatuses?.[surface.id] ?? null;621 return (
607 const toneClassName =622 <PullRequestSurfaceIcon
608 status?.state === "merged"623 surface={surface}
609 ? "text-violet-600 dark:text-violet-300/90"624 environmentId={environmentId}
610 : status?.state === "closed"625 seed={pullRequestStatusSeeds?.[surface.id]}
611 ? "text-red-600 dark:text-red-300/90"626 />
612 : status?.isDraft627 );
613 ? "text-zinc-500 dark:text-zinc-400/80"
614 : status?.state === "open"
615 ? "text-emerald-600 dark:text-emerald-300/90"
616 : "text-muted-foreground";
617 return <GitPullRequest className={cn("size-3 shrink-0", toneClassName)} />;
618 }
619 case "agents":628 case "agents":
620 return <Bot className="size-3 shrink-0" />;629 return <Bot className="size-3 shrink-0" />;
621 }630 }
622}631}
623632
633function PullRequestSurfaceIcon({
634 surface,
635 environmentId,
636 seed,
637}: {
638 surface: Extract<RightPanelSurface, { kind: "pull-request" }>;
639 environmentId: EnvironmentId | null;
640 seed: PullRequestTabStatusSeed | undefined;
641}) {
642 const resolvedEnvironmentId =
643 (surface.environmentId as EnvironmentId | undefined) ?? environmentId;
644 const detail = useEnvironmentQuery(
645 resolvedEnvironmentId === null
646 ? null
647 : pullRequestEnvironment.detail({
648 environmentId: resolvedEnvironmentId,
649 input: {
650 projectId: surface.projectId as ProjectId,
651 repository: surface.repository,
652 number: surface.number,
653 },
654 }),
655 ).data;
656 // Only state and draft reach the tab. A list seed cannot know mergeability, so feeding the
657 // full detail would flip an open tab to the conflict glyph the moment its read lands.
658 const status =
659 detail === null ? (seed ?? null) : { state: detail.state, isDraft: detail.isDraft };
660 if (status === null) {
661 return <GitPullRequest className="size-3 shrink-0 text-muted-foreground" />;
662 }
663 const presentation = resolvePullRequestState(status);
664 return <presentation.Icon className={cn("size-3 shrink-0", presentation.toneClassName)} />;
665}
666
624export function RightPanelTabs(props: RightPanelTabsProps) {667export function RightPanelTabs(props: RightPanelTabsProps) {
625 const ownsDesktopTitleBar = isElectron && props.mode === "inline";668 const ownsDesktopTitleBar = isElectron && props.mode === "inline";
626 const browserProfiles = useBrowserDefaults().profiles;669 const browserProfiles = useBrowserDefaults().profiles;
...@@ -859,7 +902,8 @@ export function RightPanelTabs(props: RightPanelTabsProps) {...@@ -859,7 +902,8 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
859 sessions={props.previewSessions}902 sessions={props.previewSessions}
860 desktopByTabId={props.desktopByTabId}903 desktopByTabId={props.desktopByTabId}
861 theme={resolvedTheme}904 theme={resolvedTheme}
862 pullRequestStatuses={props.pullRequestStatuses}905 environmentId={props.environmentId}
906 pullRequestStatusSeeds={props.pullRequestStatusSeeds}
863 />907 />
864 {pending ? (908 {pending ? (
865 <span909 <span
apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx+1-1
...@@ -450,7 +450,7 @@ export function PullRequestDetailPanel({...@@ -450,7 +450,7 @@ export function PullRequestDetailPanel({
450 onActed?: () => void;450 onActed?: () => void;
451 /** Page-owned detail columns use this to clear the selected pull request. */451 /** Page-owned detail columns use this to clear the selected pull request. */
452 onClose?: () => void;452 onClose?: () => void;
453 /** Keeps compact chrome, such as the right-panel tab, in step with refreshed host state. */453 /** Keeps surrounding thread state in step with refreshed host state. */
454 onStateChange?: (status: {454 onStateChange?: (status: {
455 projectId: string;455 projectId: string;
456 repository: string;456 repository: string;
apps/web/src/rightPanelStore.test.ts-57
...@@ -9,7 +9,6 @@ import {...@@ -9,7 +9,6 @@ import {
9 selectActiveRightPanelSurface,9 selectActiveRightPanelSurface,
10 selectSelectedRightPanelSurface,10 selectSelectedRightPanelSurface,
11 selectThreadRightPanelState,11 selectThreadRightPanelState,
12 updatePullRequestTabStatus,
13 useRightPanelStore,12 useRightPanelStore,
14} from "./rightPanelStore";13} from "./rightPanelStore";
1514
...@@ -485,62 +484,6 @@ describe("rightPanelStore", () => {...@@ -485,62 +484,6 @@ describe("rightPanelStore", () => {
485 ).toEqual([]);484 ).toEqual([]);
486 });485 });
487486
488 describe("updatePullRequestTabStatus", () => {
489 const status = (isDraft: boolean) => ({
490 projectId: "project-a",
491 repository: "pingdotgg/t3code",
492 number: 4909,
493 state: "open" as const,
494 isDraft,
495 });
496
497 // Regression for the tab wearing no state: this failed when the status was written under a
498 // key rebuilt from the pull request while the tab strip reads it under the surface's own id.
499 it("keys a status under the same id a surface opened from an environment carries", () => {
500 const target = {
501 environmentId: "remote",
502 projectId: "project-a",
503 repository: "pingdotgg/t3code",
504 number: 4909,
505 };
506 useRightPanelStore.getState().openPullRequest(refA, target);
507 const surface = selectSelectedRightPanelSurface(
508 useRightPanelStore.getState().byThreadKey,
509 refA,
510 );
511 expect(surface).not.toBeNull();
512
513 const statuses = updatePullRequestTabStatus({}, surface!.id, status(false));
514 expect(statuses[surface!.id]).toEqual(status(false));
515 });
516
517 it("keys a status under the same id a thread surface with no environment carries", () => {
518 const target = { projectId: "project-a", repository: "pingdotgg/t3code", number: 4909 };
519 useRightPanelStore.getState().openPullRequest(refA, target);
520 const surface = selectSelectedRightPanelSurface(
521 useRightPanelStore.getState().byThreadKey,
522 refA,
523 );
524 expect(surface).not.toBeNull();
525
526 const statuses = updatePullRequestTabStatus({}, surface!.id, status(false));
527 expect(statuses[surface!.id]).toEqual(status(false));
528 });
529
530 it("returns the identical map when the tab's state and draft flag are unchanged", () => {
531 const first = updatePullRequestTabStatus({}, "pull-request:1", status(false));
532 const second = updatePullRequestTabStatus(first, "pull-request:1", status(false));
533 expect(second).toBe(first);
534 });
535
536 it("replaces the entry when the draft flag changes", () => {
537 const first = updatePullRequestTabStatus({}, "pull-request:1", status(false));
538 const second = updatePullRequestTabStatus(first, "pull-request:1", status(true));
539 expect(second).not.toBe(first);
540 expect(second["pull-request:1"]).toEqual(status(true));
541 });
542 });
543
544 it("tracks one surface per terminal session", () => {487 it("tracks one surface per terminal session", () => {
545 useRightPanelStore.getState().openTerminal(refA, "term-1");488 useRightPanelStore.getState().openTerminal(refA, "term-1");
546 useRightPanelStore.getState().openTerminal(refA, "term-2");489 useRightPanelStore.getState().openTerminal(refA, "term-2");
apps/web/src/rightPanelStore.ts-17
...@@ -196,23 +196,6 @@ export function pullRequestSurface(target: {...@@ -196,23 +196,6 @@ export function pullRequestSurface(target: {
196 };196 };
197}197}
198198
199/**
200 * A pull-request tab's status map with one entry set. Keyed by the surface the panel is showing
201 * rather than by a key rebuilt from the status, so the tab is found again whether or not that
202 * surface was opened with an environment on it. Returns the same map when the tab's own fields
203 * have not changed, so a caller can skip a re-render.
204 */
205export function updatePullRequestTabStatus<Status extends { state: unknown; isDraft: boolean }>(
206 statuses: Readonly<Record<string, Status>>,
207 surfaceId: string,
208 status: Status,
209): Readonly<Record<string, Status>> {
210 return statuses[surfaceId]?.state === status.state &&
211 statuses[surfaceId]?.isDraft === status.isDraft
212 ? statuses
213 : { ...statuses, [surfaceId]: status };
214}
215
216const upsertSurface = (199const upsertSurface = (
217 current: ThreadRightPanelState,200 current: ThreadRightPanelState,
218 surface: RightPanelSurface,201 surface: RightPanelSurface,
apps/web/src/routes/_chat.pull-requests.tsx+19-20
...@@ -91,7 +91,7 @@ import { PullRequestListEmptyState } from "../components/pullRequest/PullRequest...@@ -91,7 +91,7 @@ import { PullRequestListEmptyState } from "../components/pullRequest/PullRequest
91import { PullRequestListGhost } from "../components/pullRequest/PullRequestGhosts";91import { PullRequestListGhost } from "../components/pullRequest/PullRequestGhosts";
92import { PullRequestRow } from "../components/pullRequest/PullRequestRow";92import { PullRequestRow } from "../components/pullRequest/PullRequestRow";
93import { PullRequestsUnavailableState } from "../components/pullRequest/PullRequestsUnavailableState";93import { PullRequestsUnavailableState } from "../components/pullRequest/PullRequestsUnavailableState";
94import { RightPanelTabs, type PullRequestTabStatus } from "../components/RightPanelTabs";94import { RightPanelTabs, type PullRequestTabStatusSeed } from "../components/RightPanelTabs";
95import {95import {
96 WorkspaceBreadcrumb,96 WorkspaceBreadcrumb,
97 WorkspaceBreadcrumbItem,97 WorkspaceBreadcrumbItem,
...@@ -109,10 +109,10 @@ import { useLiveRefresh } from "../hooks/useLiveRefresh";...@@ -109,10 +109,10 @@ import { useLiveRefresh } from "../hooks/useLiveRefresh";
109import { usePanelAnimationSettings, usePanelPresence } from "../panelAnimations";109import { usePanelAnimationSettings, usePanelPresence } from "../panelAnimations";
110import { toSortableTimestamp } from "../lib/threadSort";110import { toSortableTimestamp } from "../lib/threadSort";
111import {111import {
112 pullRequestSurfaceId,
112 selectActiveRightPanelSurface,113 selectActiveRightPanelSurface,
113 selectSelectedRightPanelSurface,114 selectSelectedRightPanelSurface,
114 selectThreadRightPanelState,115 selectThreadRightPanelState,
115 updatePullRequestTabStatus,
116 useRightPanelStore,116 useRightPanelStore,
117 type PullRequestSurface,117 type PullRequestSurface,
118} from "../rightPanelStore";118} from "../rightPanelStore";
...@@ -475,22 +475,6 @@ function PullRequestsRouteView() {...@@ -475,22 +475,6 @@ function PullRequestsRouteView() {
475 (renderedPullRequestSurface?.environmentId as EnvironmentId | undefined) ??475 (renderedPullRequestSurface?.environmentId as EnvironmentId | undefined) ??
476 selectedProject?.environmentId ??476 selectedProject?.environmentId ??
477 null;477 null;
478 const [pullRequestTabStatuses, setPullRequestTabStatuses] = useState<
479 Record<string, PullRequestTabStatus>
480 >({});
481 // Keyed by the surface the panel is showing rather than by a key rebuilt from the status: a
482 // surface opened from this page carries the environment its row was listed under, and a key
483 // assembled from the pull request alone would never name that surface back.
484 const activePullRequestSurfaceId = activePullRequestSurface?.id;
485 const handlePullRequestTabStatusChange = useCallback(
486 (status: PullRequestTabStatus) => {
487 const id = activePullRequestSurfaceId;
488 if (id === undefined) return;
489 setPullRequestTabStatuses((current) => updatePullRequestTabStatus(current, id, status));
490 },
491 [activePullRequestSurfaceId],
492 );
493
494 const updateSearch = useCallback(478 const updateSearch = useCallback(
495 (patch: {479 (patch: {
496 [Key in keyof PullRequestsSearch]?: PullRequestsSearch[Key] | undefined;480 [Key in keyof PullRequestsSearch]?: PullRequestsSearch[Key] | undefined;
...@@ -1182,6 +1166,21 @@ function PullRequestsRouteView() {...@@ -1182,6 +1166,21 @@ function PullRequestsRouteView() {
1182 viewers,1166 viewers,
1183 ]);1167 ]);
11841168
1169 // Seed the first tab paint from list rows while each tab's cached detail read lands.
1170 const listedPullRequestTabStatuses = useMemo<Record<string, PullRequestTabStatusSeed>>(
1171 () =>
1172 Object.fromEntries(
1173 entries.map((entry) => [
1174 pullRequestSurfaceId(entry),
1175 {
1176 state: entry.state,
1177 isDraft: entry.isDraft,
1178 },
1179 ]),
1180 ),
1181 [entries],
1182 );
1183
1185 const scrollRef = useRef<HTMLDivElement>(null);1184 const scrollRef = useRef<HTMLDivElement>(null);
1186 const sentinelRef = useRef<HTMLDivElement>(null);1185 const sentinelRef = useRef<HTMLDivElement>(null);
1187 useEffect(() => {1186 useEffect(() => {
...@@ -1889,6 +1888,7 @@ function PullRequestsRouteView() {...@@ -1889,6 +1888,7 @@ function PullRequestsRouteView() {
1889 // it. SSR has no window, so fall back to a reasonable width.1888 // it. SSR has no window, so fall back to a reasonable width.
1890 defaultWidth={typeof window === "undefined" ? 640 : Math.floor(window.innerWidth / 2)}1889 defaultWidth={typeof window === "undefined" ? 640 : Math.floor(window.innerWidth / 2)}
1891 surfaces={renderedRightPanelSurfaces}1890 surfaces={renderedRightPanelSurfaces}
1891 environmentId={panelEnvironmentId}
1892 activeSurfaceId={renderedPullRequestSurface.id}1892 activeSurfaceId={renderedPullRequestSurface.id}
1893 pendingSurfaceIds={EMPTY_PENDING_SURFACES}1893 pendingSurfaceIds={EMPTY_PENDING_SURFACES}
1894 previewSessions={EMPTY_PREVIEW_SESSIONS}1894 previewSessions={EMPTY_PREVIEW_SESSIONS}
...@@ -1922,7 +1922,7 @@ function PullRequestsRouteView() {...@@ -1922,7 +1922,7 @@ function PullRequestsRouteView() {
1922 pullRequestAvailable={false}1922 pullRequestAvailable={false}
1923 agentsAvailable={false}1923 agentsAvailable={false}
1924 liveAgentCount={0}1924 liveAgentCount={0}
1925 pullRequestStatuses={pullRequestTabStatuses}1925 pullRequestStatusSeeds={listedPullRequestTabStatuses}
1926 >1926 >
1927 <PullRequestDetailPanel1927 <PullRequestDetailPanel
1928 key={renderedPullRequestSurface.id}1928 key={renderedPullRequestSurface.id}
...@@ -1941,7 +1941,6 @@ function PullRequestsRouteView() {...@@ -1941,7 +1941,6 @@ function PullRequestsRouteView() {
1941 authoredQuery.refresh();1941 authoredQuery.refresh();
1942 reviewingQuery.refresh();1942 reviewingQuery.refresh();
1943 }}1943 }}
1944 onStateChange={handlePullRequestTabStatusChange}
1945 />1944 />
1946 </RightPanelTabs>1945 </RightPanelTabs>
1947 ) : null}1946 ) : null}