1import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
2import type {
3 ExecutionEnvironmentCapabilities,
4 ThreadPullRequestLink,
5 VcsStatusResult,
6} from "@t3tools/contracts";
7import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
8
9import {
10 resolveThreadCurrentPullRequestLink,
11 resolveThreadPullRequestBadge,
12} from "@t3tools/shared/threadPullRequests";
13
14export type ThreadPr = NonNullable<VcsStatusResult["pr"]>;
15
16export 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
32const 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
38export 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. */
59export 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. */
107export 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}