| 1 | import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; |
| 2 | import type { |
| 3 | ExecutionEnvironmentCapabilities, |
| 4 | ThreadPullRequestLink, |
| 5 | VcsStatusResult, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; |
| 8 | |
| 9 | import { |
| 10 | resolveThreadCurrentPullRequestLink, |
| 11 | resolveThreadPullRequestBadge, |
| 12 | } from "@t3tools/shared/threadPullRequests"; |
| 13 | |
| 14 | export type ThreadPr = NonNullable<VcsStatusResult["pr"]>; |
| 15 | |
| 16 | export interface ThreadPrPresentation { |
| 17 | readonly number: number; |
| 18 | readonly state: ThreadPr["state"] | null; |
| 19 | readonly kind: "pull-request" | "stack"; |
| 20 | readonly others: number; |
| 21 | readonly isDraft: boolean; |
| 22 | /** Provider-side last activity, bounding when a terminal state landed. */ |
| 23 | readonly updatedAt: string | null; |
| 24 | readonly url: string; |
| 25 | /** Compact pull request number or linked count, e.g. "3774" or "+2". */ |
| 26 | readonly label: string; |
| 27 | /** Full, provider-aware label for assistive technologies. */ |
| 28 | readonly accessibilityLabel: string; |
| 29 | readonly textClassName: string; |
| 30 | } |
| 31 | |
| 32 | const PR_STATE_TEXT_CLASS: Record<ThreadPr["state"], string> = { |
| 33 | open: "text-adaptive-emerald-600-400", |
| 34 | merged: "text-adaptive-violet-600-400", |
| 35 | closed: "text-foreground-muted", |
| 36 | }; |
| 37 | |
| 38 | export function presentThreadPr( |
| 39 | pr: ThreadPr, |
| 40 | provider: VcsStatusResult["sourceControlProvider"] | null | undefined, |
| 41 | ): ThreadPrPresentation { |
| 42 | const { changeRequest } = sourceControlClients.get(provider?.kind); |
| 43 | const isDraft = pr.state === "open" && pr.isDraft === true; |
| 44 | return { |
| 45 | kind: "pull-request", |
| 46 | others: 0, |
| 47 | number: pr.number, |
| 48 | state: pr.state, |
| 49 | isDraft, |
| 50 | updatedAt: pr.updatedAt ?? null, |
| 51 | url: pr.url, |
| 52 | label: String(pr.number), |
| 53 | accessibilityLabel: `#${pr.number} ${changeRequest.singular} ${isDraft ? "draft" : pr.state}`, |
| 54 | textClassName: isDraft ? "text-foreground-muted" : PR_STATE_TEXT_CLASS[pr.state], |
| 55 | }; |
| 56 | } |
| 57 | |
| 58 | /** Persisted links render immediately, including links awaiting their first host sync. */ |
| 59 | export function presentThreadLinkedPullRequests( |
| 60 | links: ReadonlyArray<ThreadPullRequestLink>, |
| 61 | ): ThreadPrPresentation | null { |
| 62 | const link = resolveThreadCurrentPullRequestLink(links); |
| 63 | const badge = resolveThreadPullRequestBadge(links); |
| 64 | if (link === null || badge === null) return null; |
| 65 | const snapshot = link.snapshot; |
| 66 | const linkedCount = badge.kind === "pull-request" && badge.others > 0 ? badge.others + 1 : null; |
| 67 | const isMultiple = badge.kind === "stack" || linkedCount !== null; |
| 68 | const state = isMultiple |
| 69 | ? badge.state === "draft" |
| 70 | ? "open" |
| 71 | : badge.state |
| 72 | : (snapshot?.state ?? null); |
| 73 | const isDraft = isMultiple |
| 74 | ? badge.state === "draft" |
| 75 | : snapshot?.isDraft === true && state === "open"; |
| 76 | const label = |
| 77 | badge.kind === "stack" |
| 78 | ? String(badge.layers) |
| 79 | : linkedCount !== null |
| 80 | ? `+${linkedCount}` |
| 81 | : String(link.number); |
| 82 | return { |
| 83 | kind: badge.kind, |
| 84 | others: badge.kind === "pull-request" ? badge.others : 0, |
| 85 | number: link.number, |
| 86 | state, |
| 87 | isDraft, |
| 88 | updatedAt: snapshot?.updatedAt ?? null, |
| 89 | url: link.url, |
| 90 | label, |
| 91 | accessibilityLabel: |
| 92 | badge.kind === "stack" |
| 93 | ? `${badge.layers} pull requests in stack, ${isDraft ? "draft" : (state ?? "status pending")}` |
| 94 | : linkedCount !== null |
| 95 | ? `${linkedCount} linked pull requests, overall ${badge.state}` |
| 96 | : `#${link.number} pull request ${state === null ? "status pending" : isDraft ? "draft" : state}`, |
| 97 | textClassName: |
| 98 | state === null || isDraft |
| 99 | ? "text-foreground-muted" |
| 100 | : isMultiple && state === "closed" |
| 101 | ? "text-adaptive-rose-600-400" |
| 102 | : PR_STATE_TEXT_CLASS[state], |
| 103 | }; |
| 104 | } |
| 105 | |
| 106 | /** Only the array capability replaces legacy references with persisted snapshots. */ |
| 107 | export function resolveThreadPrSource( |
| 108 | thread: Pick<EnvironmentThreadShell, "pullRequests" | "linkedPullRequest" | "branchPullRequest">, |
| 109 | capabilities: |
| 110 | | Pick<ExecutionEnvironmentCapabilities, "threadPullRequests" | "threadPullRequestLinking"> |
| 111 | | undefined, |
| 112 | ) { |
| 113 | const supportsSnapshots = capabilities?.threadPullRequests === true; |
| 114 | const linkedPresentation = supportsSnapshots |
| 115 | ? presentThreadLinkedPullRequests(thread.pullRequests) |
| 116 | : null; |
| 117 | const pullRequestRef = |
| 118 | linkedPresentation !== null |
| 119 | ? null |
| 120 | : ((supportsSnapshots |
| 121 | ? thread.branchPullRequest |
| 122 | : (thread.linkedPullRequest ?? thread.branchPullRequest)) ?? null); |
| 123 | return { linkedPresentation, pullRequestRef }; |
| 124 | } |