| author | |
| committer | |
| log | 1eb36b45ef7aa5c028380afb7266bcabf258a55e |
| tree | 366ddc5395c84f2108158abadb61fe63435f5c65 |
| parent | ba3cb0773859334fe9f75016295bb3893c9e6044 |
| signature | Signed by PGP key B5690EEEBB952194 |
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"; |
| 156 | import { | 155 | import { |
| ... | @@ -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 | agentsAvailable | 7729 | 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 | ) : null | 7750 | ) : 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 | agentsAvailable | 7778 | 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 | <RightPanelTabs | 97 | <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 @@ |
| 1 | import type { ContextMenuItem, PreviewSessionSnapshot, PullRequestState } from "@t3tools/contracts"; | 1 | import type { |
| 2 | ContextMenuItem, | ||
| 3 | EnvironmentId, | ||
| 4 | PreviewSessionSnapshot, | ||
| 5 | ProjectId, | ||
| 6 | PullRequestState, | ||
| 7 | } from "@t3tools/contracts"; | ||
| 2 | import { getTerminalLabel } from "@t3tools/shared/terminalLabels"; | 8 | import { getTerminalLabel } from "@t3tools/shared/terminalLabels"; |
| 3 | import { | 9 | import { |
| 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"; |
| 45 | import { PanelTabCloseButton } from "~/components/ui/panel-tab-close-button"; | 51 | import { PanelTabCloseButton } from "~/components/ui/panel-tab-close-button"; |
| 46 | import { faviconUrlForOrigin } from "~/lib/favicon"; | 52 | import { faviconUrlForOrigin } from "~/lib/favicon"; |
| 47 | import { useTheme } from "~/hooks/useTheme"; | 53 | import { useTheme } from "~/hooks/useTheme"; |
| 54 | import { pullRequestEnvironment } from "~/state/pullRequests"; | ||
| 55 | import { useEnvironmentQuery } from "~/state/query"; | ||
| 48 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; | 56 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; |
| 49 | 57 | ||
| 50 | import { PreviewPanelShell, type PreviewPanelMode } from "./preview/PreviewPanelShell"; | 58 | import { PreviewPanelShell, type PreviewPanelMode } from "./preview/PreviewPanelShell"; |
| 51 | import { FaviconImage } from "./preview/PreviewFaviconIcon"; | 59 | import { FaviconImage } from "./preview/PreviewFaviconIcon"; |
| 52 | import { previewBridge } from "./preview/previewBridge"; | 60 | import { previewBridge } from "./preview/previewBridge"; |
| 53 | import { PierreEntryIcon } from "./chat/PierreEntryIcon"; | 61 | import { PierreEntryIcon } from "./chat/PierreEntryIcon"; |
| 62 | import { resolvePullRequestState } from "./pullRequest/pullRequestPresentation"; | ||
| 54 | 63 | ||
| 55 | interface RightPanelTabsProps { | 64 | interface 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 | } |
| 113 | 124 | ||
| 125 | export type PullRequestTabStatusSeed = Pick<PullRequestTabStatus, "state" | "isDraft">; | ||
| 126 | |||
| 114 | export function shouldOpenDefaultBrowserProfileFromMenuClick( | 127 | export 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?.isDraft | 627 | ); |
| 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 | } |
| 623 | 632 | ||
| 633 | function 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 | |||
| 624 | export function RightPanelTabs(props: RightPanelTabsProps) { | 667 | export 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 | <span | 909 | <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"; |
| 15 | 14 | ||
| ... | @@ -485,62 +484,6 @@ describe("rightPanelStore", () => { | ... | @@ -485,62 +484,6 @@ describe("rightPanelStore", () => { |
| 485 | ).toEqual([]); | 484 | ).toEqual([]); |
| 486 | }); | 485 | }); |
| 487 | 486 | ||
| 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 | } |
| 198 | 198 | ||
| 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 | */ | ||
| 205 | export 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 | |||
| 216 | const upsertSurface = ( | 199 | const 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 |
| 91 | import { PullRequestListGhost } from "../components/pullRequest/PullRequestGhosts"; | 91 | import { PullRequestListGhost } from "../components/pullRequest/PullRequestGhosts"; |
| 92 | import { PullRequestRow } from "../components/pullRequest/PullRequestRow"; | 92 | import { PullRequestRow } from "../components/pullRequest/PullRequestRow"; |
| 93 | import { PullRequestsUnavailableState } from "../components/pullRequest/PullRequestsUnavailableState"; | 93 | import { PullRequestsUnavailableState } from "../components/pullRequest/PullRequestsUnavailableState"; |
| 94 | import { RightPanelTabs, type PullRequestTabStatus } from "../components/RightPanelTabs"; | 94 | import { RightPanelTabs, type PullRequestTabStatusSeed } from "../components/RightPanelTabs"; |
| 95 | import { | 95 | import { |
| 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"; |
| 109 | import { usePanelAnimationSettings, usePanelPresence } from "../panelAnimations"; | 109 | import { usePanelAnimationSettings, usePanelPresence } from "../panelAnimations"; |
| 110 | import { toSortableTimestamp } from "../lib/threadSort"; | 110 | import { toSortableTimestamp } from "../lib/threadSort"; |
| 111 | import { | 111 | import { |
| 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 | ]); |
| 1184 | 1168 | ||
| 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 | <PullRequestDetailPanel | 1927 | <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} |