| author | |
| committer | |
| log | a0eb23993a3bacf96d618918f43ea18f2da72390 |
| tree | 4309939c82fd383884061ecc330943906ca6dc1a |
| parent | b01771c2383bbcdf154b8c4e9f1a7d5e3867e2d0 |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 108 insertions(+), 4 deletions(-)
apps/web/src/components/ChatView.tsx+10-1| ... | ... | @@ -222,6 +222,7 @@ import { |
| 222 | 222 | import { useNowMinute } from "../hooks/useNowMinute"; |
| 223 | 223 | import { usePanelAnimationSettings, usePanelPresence } from "../panelAnimations"; |
| 224 | 224 | import { useNewThreadHandler } from "../hooks/useHandleNewThread"; |
| 225 | import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl"; | |
| 225 | 226 | import { useThreadActions } from "../hooks/useThreadActions"; |
| 226 | 227 | import { resolveAppModelSelectionForInstance } from "../modelSelection"; |
| 227 | 228 | import { confirmTerminalClose, isTerminalCloseConfirmPending } from "../lib/terminalCloseConfirm"; |
| ... | ... | @@ -5077,16 +5078,24 @@ export default function ChatView(props: ChatViewProps) { |
| 5077 | 5078 | threadRepository, |
| 5078 | 5079 | ], |
| 5079 | 5080 | ); |
| 5081 | const openPanelPullRequestUrl = useOpenPanelPullRequestUrl(activeThreadRef); | |
| 5080 | 5082 | const activeThreadReferenceCopyTarget = useMemo( |
| 5081 | 5083 | () => |
| 5082 | 5084 | activeThreadId === null || !isServerThread |
| 5083 | 5085 | ? null |
| 5084 | 5086 | : resolveThreadReferenceCopyTarget({ |
| 5085 | 5087 | threadId: activeThreadId, |
| 5088 | openPanelPullRequestUrl, | |
| 5086 | 5089 | linkedPullRequestUrl: linkedThreadPullRequest?.url ?? null, |
| 5087 | 5090 | detectedPullRequestUrl: activeThreadPr?.url ?? null, |
| 5088 | 5091 | }), |
| 5089 | [activeThreadId, activeThreadPr?.url, isServerThread, linkedThreadPullRequest?.url], | |
| 5092 | [ | |
| 5093 | activeThreadId, | |
| 5094 | activeThreadPr?.url, | |
| 5095 | isServerThread, | |
| 5096 | linkedThreadPullRequest?.url, | |
| 5097 | openPanelPullRequestUrl, | |
| 5098 | ], | |
| 5090 | 5099 | ); |
| 5091 | 5100 | const copyActiveThreadReference = useCallback(() => { |
| 5092 | 5101 | const target = activeThreadReferenceCopyTarget; |
apps/web/src/components/CommandPalette.tsx+7-1| ... | ... | @@ -71,6 +71,7 @@ import { useAtomValue } from "@effect/atom-react"; |
| 71 | 71 | import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal"; |
| 72 | 72 | import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstraps"; |
| 73 | 73 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; |
| 74 | import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl"; | |
| 74 | 75 | import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; |
| 75 | 76 | import { useClientSettings } from "../hooks/useSettings"; |
| 76 | 77 | import { useTheme } from "../hooks/useTheme"; |
| ... | ... | @@ -633,11 +634,15 @@ function OpenCommandPaletteDialog(props: { |
| 633 | 634 | ), |
| 634 | 635 | retainTerminalOnBranchMismatch: activeThread.worktreePath === null, |
| 635 | 636 | })?.url ?? null); |
| 637 | const openPanelPullRequestUrl = useOpenPanelPullRequestUrl( | |
| 638 | activeThread ? scopeThreadRef(activeThread.environmentId, activeThread.id) : null, | |
| 639 | ); | |
| 636 | 640 | const activeThreadReferenceCopyTarget = |
| 637 | 641 | activeThread == null |
| 638 | 642 | ? null |
| 639 | 643 | : resolveThreadReferenceCopyTarget({ |
| 640 | 644 | threadId: activeThread.id, |
| 645 | openPanelPullRequestUrl, | |
| 641 | 646 | linkedPullRequestUrl: activeThread.linkedPullRequest?.url ?? null, |
| 642 | 647 | detectedPullRequestUrl, |
| 643 | 648 | }); |
| ... | ... | @@ -2302,9 +2307,10 @@ function OpenCommandPaletteDialog(props: { |
| 2302 | 2307 | } |
| 2303 | 2308 | return; |
| 2304 | 2309 | } |
| 2305 | if (command === "thread.copyReference" && activeThreadReferenceCopyTarget !== null) { | |
| 2310 | if (command === "thread.copyReference") { | |
| 2306 | 2311 | event.preventDefault(); |
| 2307 | 2312 | event.stopPropagation(); |
| 2313 | if (activeThreadReferenceCopyTarget === null) return; | |
| 2308 | 2314 | setOpen(false); |
| 2309 | 2315 | void copyActiveThreadReference(); |
| 2310 | 2316 | return; |
apps/web/src/hooks/useOpenPanelPullRequestUrl.ts created+59| ... | ... | @@ -0,0 +1,59 @@ |
| 1 | import { scopeProjectRef } from "@t3tools/client-runtime/environment"; | |
| 2 | import { EnvironmentId, ProjectId, type ScopedThreadRef } from "@t3tools/contracts"; | |
| 3 | import { useMemo } from "react"; | |
| 4 | ||
| 5 | import { | |
| 6 | readPullRequestDetailSnapshot, | |
| 7 | resolveDisplayedPullRequestDetail, | |
| 8 | } from "../components/pullRequest/pullRequestDetail.logic"; | |
| 9 | import { gitHubPullRequestBrowserUrl } from "../lib/openPullRequestLink"; | |
| 10 | import { selectActiveRightPanelSurface, useRightPanelStore } from "../rightPanelStore"; | |
| 11 | import { useProject } from "../state/entities"; | |
| 12 | import { pullRequestEnvironment } from "../state/pullRequests"; | |
| 13 | import { useEnvironmentQuery } from "../state/query"; | |
| 14 | ||
| 15 | export function useOpenPanelPullRequestUrl(threadRef: ScopedThreadRef | null) { | |
| 16 | const surface = useRightPanelStore((state) => | |
| 17 | selectActiveRightPanelSurface(state.byThreadKey, threadRef), | |
| 18 | ); | |
| 19 | const reference = surface?.kind === "pull-request" ? surface : null; | |
| 20 | const environmentId = reference?.environmentId | |
| 21 | ? EnvironmentId.make(reference.environmentId) | |
| 22 | : threadRef?.environmentId; | |
| 23 | const project = useProject( | |
| 24 | reference && environmentId | |
| 25 | ? scopeProjectRef(environmentId, ProjectId.make(reference.projectId)) | |
| 26 | : null, | |
| 27 | ); | |
| 28 | const detail = useEnvironmentQuery( | |
| 29 | reference && environmentId | |
| 30 | ? pullRequestEnvironment.detail({ | |
| 31 | environmentId, | |
| 32 | input: { | |
| 33 | projectId: ProjectId.make(reference.projectId), | |
| 34 | repository: reference.repository, | |
| 35 | number: reference.number, | |
| 36 | }, | |
| 37 | }) | |
| 38 | : null, | |
| 39 | ).data; | |
| 40 | const cachedDetail = useMemo( | |
| 41 | () => | |
| 42 | reference && environmentId | |
| 43 | ? readPullRequestDetailSnapshot( | |
| 44 | typeof window === "undefined" ? undefined : window.localStorage, | |
| 45 | environmentId, | |
| 46 | reference, | |
| 47 | ) | |
| 48 | : null, | |
| 49 | [environmentId, reference], | |
| 50 | ); | |
| 51 | return reference | |
| 52 | ? (resolveDisplayedPullRequestDetail({ live: detail, cached: cachedDetail, reference })?.url ?? | |
| 53 | gitHubPullRequestBrowserUrl( | |
| 54 | project?.repositoryIdentity, | |
| 55 | reference.repository, | |
| 56 | reference.number, | |
| 57 | )) | |
| 58 | : undefined; | |
| 59 | } |
packages/shared/src/threadReference.test.ts+26| ... | ... | @@ -3,6 +3,32 @@ import { describe, expect, it } from "vite-plus/test"; |
| 3 | 3 | import { resolveThreadReferenceCopyTarget } from "./threadReference.ts"; |
| 4 | 4 | |
| 5 | 5 | describe("resolveThreadReferenceCopyTarget", () => { |
| 6 | it("does not copy another reference while the open panel URL is unavailable", () => { | |
| 7 | expect( | |
| 8 | resolveThreadReferenceCopyTarget({ | |
| 9 | threadId: "thread-1", | |
| 10 | openPanelPullRequestUrl: null, | |
| 11 | linkedPullRequestUrl: "https://github.com/t3/pr/12", | |
| 12 | detectedPullRequestUrl: "https://github.com/t3/pr/13", | |
| 13 | }), | |
| 14 | ).toBeNull(); | |
| 15 | }); | |
| 16 | ||
| 17 | it("prefers the open panel pull request over linked and detected pull requests", () => { | |
| 18 | expect( | |
| 19 | resolveThreadReferenceCopyTarget({ | |
| 20 | threadId: "thread-1", | |
| 21 | openPanelPullRequestUrl: "https://github.com/t3/pr/14", | |
| 22 | linkedPullRequestUrl: "https://github.com/t3/pr/12", | |
| 23 | detectedPullRequestUrl: "https://github.com/t3/pr/13", | |
| 24 | }), | |
| 25 | ).toMatchObject({ | |
| 26 | kind: "pull-request", | |
| 27 | value: "https://github.com/t3/pr/14", | |
| 28 | successTitle: "PR link copied", | |
| 29 | }); | |
| 30 | }); | |
| 31 | ||
| 6 | 32 | it("prefers a durable linked pull request", () => { |
| 7 | 33 | expect( |
| 8 | 34 | resolveThreadReferenceCopyTarget({ |
packages/shared/src/threadReference.ts+6-2| ... | ... | @@ -8,10 +8,14 @@ export interface ThreadReferenceCopyTarget { |
| 8 | 8 | |
| 9 | 9 | export function resolveThreadReferenceCopyTarget(input: { |
| 10 | 10 | readonly threadId: string; |
| 11 | /** Undefined means no PR panel; null means its URL is not available yet. */ | |
| 12 | readonly openPanelPullRequestUrl?: string | null | undefined; | |
| 11 | 13 | readonly linkedPullRequestUrl?: string | null; |
| 12 | 14 | readonly detectedPullRequestUrl?: string | null; |
| 13 | }): ThreadReferenceCopyTarget { | |
| 14 | const pullRequestUrl = input.linkedPullRequestUrl ?? input.detectedPullRequestUrl; | |
| 15 | }): ThreadReferenceCopyTarget | null { | |
| 16 | if (input.openPanelPullRequestUrl === null) return null; | |
| 17 | const pullRequestUrl = | |
| 18 | input.openPanelPullRequestUrl ?? input.linkedPullRequestUrl ?? input.detectedPullRequestUrl; | |
| 15 | 19 | return pullRequestUrl |
| 16 | 20 | ? { |
| 17 | 21 | kind: "pull-request", |