| author | |
| committer | |
| log | 692eb1a5792b9930959b19805acf2bf2611318c9 |
| tree | 42926af5e927cbd8c56e7222d20faf2ca76ed755 |
| parent | 163d50846bc8989b5858802bb02253254dcee0dd |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com>
Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>3 files changed, 143 insertions(+), 1 deletions(-)
apps/web/src/components/ChatView.tsx+61-1| ... | ... | @@ -262,6 +262,7 @@ import { environmentCatalog } from "../connection/catalog"; |
| 262 | 262 | import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; |
| 263 | 263 | import { useKnownTerminalSessions, useThreadRunningTerminalIds } from "../state/terminalSessions"; |
| 264 | 264 | import { projectEnvironment } from "../state/projects"; |
| 265 | import { linkedPullRequestDetailAtom } from "../state/pullRequests"; | |
| 265 | 266 | import { useEnvironmentQuery } from "../state/query"; |
| 266 | 267 | import { |
| 267 | 268 | environmentServerConfigsAtom, |
| ... | ... | @@ -317,6 +318,7 @@ import { |
| 317 | 318 | } from "./chat/ThreadErrorBanner"; |
| 318 | 319 | import { |
| 319 | 320 | resolveDisplayedThreadPr, |
| 321 | threadPullRequestRefreshSource, | |
| 320 | 322 | threadChangeRequestSnapshotsAtom, |
| 321 | 323 | useLinkedThreadPullRequest, |
| 322 | 324 | } from "./ThreadStatusIndicators"; |
| ... | ... | @@ -1745,7 +1747,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 1745 | 1747 | // the tab is found again whether or not that surface was opened with an environment on it. |
| 1746 | 1748 | const activePullRequestSurfaceId = |
| 1747 | 1749 | activeRightPanelSurface?.kind === "pull-request" ? activeRightPanelSurface.id : undefined; |
| 1748 | const handlePullRequestTabStatusChange = useCallback( | |
| 1750 | const updatePullRequestTabStatusFromPanel = useCallback( | |
| 1749 | 1751 | (status: PullRequestTabStatus) => { |
| 1750 | 1752 | const id = activePullRequestSurfaceId; |
| 1751 | 1753 | if (id === undefined) return; |
| ... | ... | @@ -1753,6 +1755,8 @@ function ChatViewContent(props: ChatViewProps) { |
| 1753 | 1755 | }, |
| 1754 | 1756 | [activePullRequestSurfaceId], |
| 1755 | 1757 | ); |
| 1758 | const refreshVcsStatus = useAtomCommand(vcsEnvironment.refreshStatus, { reportFailure: false }); | |
| 1759 | const sidebarPrRefreshKeyRef = useRef<string | null>(null); | |
| 1756 | 1760 | const activeFileSurface = |
| 1757 | 1761 | activeRightPanelSurface?.kind === "file" ? activeRightPanelSurface : null; |
| 1758 | 1762 | const activePreviewState = useThreadPreviewState(activeThreadRef); |
| ... | ... | @@ -4594,6 +4598,62 @@ function ChatViewContent(props: ChatViewProps) { |
| 4594 | 4598 | linkedPullRequest: linkedThreadPullRequest, |
| 4595 | 4599 | linkedPullRequestStatus, |
| 4596 | 4600 | }); |
| 4601 | const handlePullRequestTabStatusChange = useCallback( | |
| 4602 | (status: PullRequestTabStatus) => { | |
| 4603 | updatePullRequestTabStatusFromPanel(status); | |
| 4604 | const source = threadPullRequestRefreshSource({ | |
| 4605 | panel: status, | |
| 4606 | thread: { | |
| 4607 | repository: threadRepository, | |
| 4608 | number: linkedThreadPullRequest?.number ?? activeThreadPr?.number ?? null, | |
| 4609 | state: activeThreadPr?.state ?? null, | |
| 4610 | linked: linkedThreadPullRequest !== null, | |
| 4611 | }, | |
| 4612 | }); | |
| 4613 | if (source === null) { | |
| 4614 | sidebarPrRefreshKeyRef.current = null; | |
| 4615 | return; | |
| 4616 | } | |
| 4617 | const refreshKey = `${activeThreadKey}:${source}:${status.repository}#${status.number}:${status.state}`; | |
| 4618 | if (sidebarPrRefreshKeyRef.current === refreshKey) return; | |
| 4619 | sidebarPrRefreshKeyRef.current = refreshKey; | |
| 4620 | ||
| 4621 | if (source === "linked-detail" && activeThreadRef && linkedThreadPullRequest) { | |
| 4622 | appAtomRegistry.refresh( | |
| 4623 | linkedPullRequestDetailAtom({ | |
| 4624 | environmentId: activeThreadRef.environmentId, | |
| 4625 | input: { | |
| 4626 | projectId: linkedThreadPullRequest.projectId, | |
| 4627 | repository: linkedThreadPullRequest.repository, | |
| 4628 | number: linkedThreadPullRequest.number, | |
| 4629 | }, | |
| 4630 | }), | |
| 4631 | ); | |
| 4632 | return; | |
| 4633 | } | |
| 4634 | if (source === "vcs" && activeThreadRef && gitCwd !== null) { | |
| 4635 | void refreshVcsStatus({ | |
| 4636 | environmentId: activeThreadRef.environmentId, | |
| 4637 | input: { cwd: gitCwd }, | |
| 4638 | }).then(() => { | |
| 4639 | if (sidebarPrRefreshKeyRef.current === refreshKey) { | |
| 4640 | sidebarPrRefreshKeyRef.current = null; | |
| 4641 | } | |
| 4642 | }); | |
| 4643 | } | |
| 4644 | }, | |
| 4645 | [ | |
| 4646 | activeThreadKey, | |
| 4647 | activeThreadPr?.number, | |
| 4648 | activeThreadPr?.state, | |
| 4649 | activeThreadRef, | |
| 4650 | gitCwd, | |
| 4651 | linkedThreadPullRequest, | |
| 4652 | refreshVcsStatus, | |
| 4653 | threadRepository, | |
| 4654 | updatePullRequestTabStatusFromPanel, | |
| 4655 | ], | |
| 4656 | ); | |
| 4597 | 4657 | const activeThreadReferenceCopyTarget = useMemo( |
| 4598 | 4658 | () => |
| 4599 | 4659 | activeThreadId === null || !isServerThread |
apps/web/src/components/ThreadStatusIndicators.test.ts+56| ... | ... | @@ -10,6 +10,7 @@ import { |
| 10 | 10 | resolveDisplayedThreadPrProvider, |
| 11 | 11 | resolveThreadPr, |
| 12 | 12 | settledPrHoverColorClass, |
| 13 | threadPullRequestRefreshSource, | |
| 13 | 14 | threadChangeRequestSnapshotsAtom, |
| 14 | 15 | type ThreadChangeRequestSnapshot, |
| 15 | 16 | } from "./ThreadStatusIndicators"; |
| ... | ... | @@ -56,6 +57,61 @@ function snapshotFor( |
| 56 | 57 | return { branch, pr, sourceControlProvider }; |
| 57 | 58 | } |
| 58 | 59 | |
| 60 | describe("threadPullRequestRefreshSource", () => { | |
| 61 | const panel = { repository: "pingdotgg/t3code", number: 42, state: "merged" as const }; | |
| 62 | ||
| 63 | it("refreshes the VCS stream when the open panel is newer than an inferred sidebar PR", () => { | |
| 64 | expect( | |
| 65 | threadPullRequestRefreshSource({ | |
| 66 | panel, | |
| 67 | thread: { repository: "pingdotgg/t3code", number: 42, state: "open", linked: false }, | |
| 68 | }), | |
| 69 | ).toBe("vcs"); | |
| 70 | }); | |
| 71 | ||
| 72 | it("refreshes linked detail when the open panel is newer than a linked sidebar PR", () => { | |
| 73 | expect( | |
| 74 | threadPullRequestRefreshSource({ | |
| 75 | panel, | |
| 76 | thread: { repository: "pingdotgg/t3code", number: 42, state: "open", linked: true }, | |
| 77 | }), | |
| 78 | ).toBe("linked-detail"); | |
| 79 | }); | |
| 80 | ||
| 81 | it("refreshes when the sidebar has not resolved state yet", () => { | |
| 82 | expect( | |
| 83 | threadPullRequestRefreshSource({ | |
| 84 | panel, | |
| 85 | thread: { repository: "pingdotgg/t3code", number: 42, state: null, linked: false }, | |
| 86 | }), | |
| 87 | ).toBe("vcs"); | |
| 88 | }); | |
| 89 | ||
| 90 | it("does nothing once sidebar state matches or the panel shows another PR", () => { | |
| 91 | expect( | |
| 92 | threadPullRequestRefreshSource({ | |
| 93 | panel, | |
| 94 | thread: { repository: "pingdotgg/t3code", number: 42, state: "merged", linked: false }, | |
| 95 | }), | |
| 96 | ).toBeNull(); | |
| 97 | expect( | |
| 98 | threadPullRequestRefreshSource({ | |
| 99 | panel, | |
| 100 | thread: { repository: "pingdotgg/t3code", number: 41, state: "open", linked: false }, | |
| 101 | }), | |
| 102 | ).toBeNull(); | |
| 103 | }); | |
| 104 | ||
| 105 | it("matches repository identity without case sensitivity", () => { | |
| 106 | expect( | |
| 107 | threadPullRequestRefreshSource({ | |
| 108 | panel: { ...panel, repository: "PingDotGG/T3Code" }, | |
| 109 | thread: { repository: "pingdotgg/t3code", number: 42, state: "open", linked: false }, | |
| 110 | }), | |
| 111 | ).toBe("vcs"); | |
| 112 | }); | |
| 113 | }); | |
| 114 | ||
| 59 | 115 | describe("resolveThreadPr", () => { |
| 60 | 116 | it("keeps local-checkout PR indicators scoped to the stored thread branch", () => { |
| 61 | 117 | expect( |
apps/web/src/components/ThreadStatusIndicators.tsx+26| ... | ... | @@ -39,6 +39,32 @@ export interface TerminalStatusIndicator { |
| 39 | 39 | |
| 40 | 40 | export type ThreadPr = VcsStatusResult["pr"]; |
| 41 | 41 | |
| 42 | export type ThreadPullRequestRefreshSource = "linked-detail" | "vcs"; | |
| 43 | ||
| 44 | /** Refresh only when the panel has newer state for this thread's own pull request. */ | |
| 45 | export function threadPullRequestRefreshSource(input: { | |
| 46 | readonly panel: { | |
| 47 | readonly repository: string; | |
| 48 | readonly number: number; | |
| 49 | readonly state: NonNullable<ThreadPr>["state"]; | |
| 50 | }; | |
| 51 | readonly thread: { | |
| 52 | readonly repository: string | null; | |
| 53 | readonly number: number | null; | |
| 54 | readonly state: NonNullable<ThreadPr>["state"] | null; | |
| 55 | readonly linked: boolean; | |
| 56 | }; | |
| 57 | }): ThreadPullRequestRefreshSource | null { | |
| 58 | if ( | |
| 59 | input.thread.repository?.toLowerCase() !== input.panel.repository.toLowerCase() || | |
| 60 | input.thread.number !== input.panel.number || | |
| 61 | input.thread.state === input.panel.state | |
| 62 | ) { | |
| 63 | return null; | |
| 64 | } | |
| 65 | return input.thread.linked ? "linked-detail" : "vcs"; | |
| 66 | } | |
| 67 | ||
| 42 | 68 | export interface LinkedThreadPullRequestStatus { |
| 43 | 69 | readonly pr: NonNullable<ThreadPr>; |
| 44 | 70 | readonly sourceControlProvider: NonNullable<VcsStatusResult["sourceControlProvider"]>; |