| 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,6 +222,7 @@ import { |
| 222 | import { useNowMinute } from "../hooks/useNowMinute"; | 222 | import { useNowMinute } from "../hooks/useNowMinute"; |
| 223 | import { usePanelAnimationSettings, usePanelPresence } from "../panelAnimations"; | 223 | import { usePanelAnimationSettings, usePanelPresence } from "../panelAnimations"; |
| 224 | import { useNewThreadHandler } from "../hooks/useHandleNewThread"; | 224 | import { useNewThreadHandler } from "../hooks/useHandleNewThread"; |
| 225 | import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl"; | ||
| 225 | import { useThreadActions } from "../hooks/useThreadActions"; | 226 | import { useThreadActions } from "../hooks/useThreadActions"; |
| 226 | import { resolveAppModelSelectionForInstance } from "../modelSelection"; | 227 | import { resolveAppModelSelectionForInstance } from "../modelSelection"; |
| 227 | import { confirmTerminalClose, isTerminalCloseConfirmPending } from "../lib/terminalCloseConfirm"; | 228 | import { confirmTerminalClose, isTerminalCloseConfirmPending } from "../lib/terminalCloseConfirm"; |
| ... | @@ -5077,16 +5078,24 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -5077,16 +5078,24 @@ export default function ChatView(props: ChatViewProps) { |
| 5077 | threadRepository, | 5078 | threadRepository, |
| 5078 | ], | 5079 | ], |
| 5079 | ); | 5080 | ); |
| 5081 | const openPanelPullRequestUrl = useOpenPanelPullRequestUrl(activeThreadRef); | ||
| 5080 | const activeThreadReferenceCopyTarget = useMemo( | 5082 | const activeThreadReferenceCopyTarget = useMemo( |
| 5081 | () => | 5083 | () => |
| 5082 | activeThreadId === null || !isServerThread | 5084 | activeThreadId === null || !isServerThread |
| 5083 | ? null | 5085 | ? null |
| 5084 | : resolveThreadReferenceCopyTarget({ | 5086 | : resolveThreadReferenceCopyTarget({ |
| 5085 | threadId: activeThreadId, | 5087 | threadId: activeThreadId, |
| 5088 | openPanelPullRequestUrl, | ||
| 5086 | linkedPullRequestUrl: linkedThreadPullRequest?.url ?? null, | 5089 | linkedPullRequestUrl: linkedThreadPullRequest?.url ?? null, |
| 5087 | detectedPullRequestUrl: activeThreadPr?.url ?? null, | 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 | const copyActiveThreadReference = useCallback(() => { | 5100 | const copyActiveThreadReference = useCallback(() => { |
| 5092 | const target = activeThreadReferenceCopyTarget; | 5101 | const target = activeThreadReferenceCopyTarget; |
apps/web/src/components/CommandPalette.tsx+7-1| ... | @@ -71,6 +71,7 @@ import { useAtomValue } from "@effect/atom-react"; | ... | @@ -71,6 +71,7 @@ import { useAtomValue } from "@effect/atom-react"; |
| 71 | import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal"; | 71 | import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal"; |
| 72 | import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstraps"; | 72 | import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstraps"; |
| 73 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; | 73 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; |
| 74 | import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl"; | ||
| 74 | import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; | 75 | import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; |
| 75 | import { useClientSettings } from "../hooks/useSettings"; | 76 | import { useClientSettings } from "../hooks/useSettings"; |
| 76 | import { useTheme } from "../hooks/useTheme"; | 77 | import { useTheme } from "../hooks/useTheme"; |
| ... | @@ -633,11 +634,15 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -633,11 +634,15 @@ function OpenCommandPaletteDialog(props: { |
| 633 | ), | 634 | ), |
| 634 | retainTerminalOnBranchMismatch: activeThread.worktreePath === null, | 635 | retainTerminalOnBranchMismatch: activeThread.worktreePath === null, |
| 635 | })?.url ?? null); | 636 | })?.url ?? null); |
| 637 | const openPanelPullRequestUrl = useOpenPanelPullRequestUrl( | ||
| 638 | activeThread ? scopeThreadRef(activeThread.environmentId, activeThread.id) : null, | ||
| 639 | ); | ||
| 636 | const activeThreadReferenceCopyTarget = | 640 | const activeThreadReferenceCopyTarget = |
| 637 | activeThread == null | 641 | activeThread == null |
| 638 | ? null | 642 | ? null |
| 639 | : resolveThreadReferenceCopyTarget({ | 643 | : resolveThreadReferenceCopyTarget({ |
| 640 | threadId: activeThread.id, | 644 | threadId: activeThread.id, |
| 645 | openPanelPullRequestUrl, | ||
| 641 | linkedPullRequestUrl: activeThread.linkedPullRequest?.url ?? null, | 646 | linkedPullRequestUrl: activeThread.linkedPullRequest?.url ?? null, |
| 642 | detectedPullRequestUrl, | 647 | detectedPullRequestUrl, |
| 643 | }); | 648 | }); |
| ... | @@ -2302,9 +2307,10 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -2302,9 +2307,10 @@ function OpenCommandPaletteDialog(props: { |
| 2302 | } | 2307 | } |
| 2303 | return; | 2308 | return; |
| 2304 | } | 2309 | } |
| 2305 | if (command === "thread.copyReference" && activeThreadReferenceCopyTarget !== null) { | 2310 | if (command === "thread.copyReference") { |
| 2306 | event.preventDefault(); | 2311 | event.preventDefault(); |
| 2307 | event.stopPropagation(); | 2312 | event.stopPropagation(); |
| 2313 | if (activeThreadReferenceCopyTarget === null) return; | ||
| 2308 | setOpen(false); | 2314 | setOpen(false); |
| 2309 | void copyActiveThreadReference(); | 2315 | void copyActiveThreadReference(); |
| 2310 | return; | 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,6 +3,32 @@ import { describe, expect, it } from "vite-plus/test"; |
| 3 | import { resolveThreadReferenceCopyTarget } from "./threadReference.ts"; | 3 | import { resolveThreadReferenceCopyTarget } from "./threadReference.ts"; |
| 4 | 4 | ||
| 5 | describe("resolveThreadReferenceCopyTarget", () => { | 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 | it("prefers a durable linked pull request", () => { | 32 | it("prefers a durable linked pull request", () => { |
| 7 | expect( | 33 | expect( |
| 8 | resolveThreadReferenceCopyTarget({ | 34 | resolveThreadReferenceCopyTarget({ |
packages/shared/src/threadReference.ts+6-2| ... | @@ -8,10 +8,14 @@ export interface ThreadReferenceCopyTarget { | ... | @@ -8,10 +8,14 @@ export interface ThreadReferenceCopyTarget { |
| 8 | 8 | ||
| 9 | export function resolveThreadReferenceCopyTarget(input: { | 9 | export function resolveThreadReferenceCopyTarget(input: { |
| 10 | readonly threadId: string; | 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 | readonly linkedPullRequestUrl?: string | null; | 13 | readonly linkedPullRequestUrl?: string | null; |
| 12 | readonly detectedPullRequestUrl?: string | null; | 14 | readonly detectedPullRequestUrl?: string | null; |
| 13 | }): ThreadReferenceCopyTarget { | 15 | }): ThreadReferenceCopyTarget | null { |
| 14 | const pullRequestUrl = input.linkedPullRequestUrl ?? input.detectedPullRequestUrl; | 16 | if (input.openPanelPullRequestUrl === null) return null; |
| 17 | const pullRequestUrl = | ||
| 18 | input.openPanelPullRequestUrl ?? input.linkedPullRequestUrl ?? input.detectedPullRequestUrl; | ||
| 15 | return pullRequestUrl | 19 | return pullRequestUrl |
| 16 | ? { | 20 | ? { |
| 17 | kind: "pull-request", | 21 | kind: "pull-request", |