1import {
2 repositoryGroupingKeyOf,
3 type ModelSelection,
4 type RepositoryIdentity,
5 type ServerProvider,
6 type ThreadLinkedPullRequest,
7 type ThreadPullRequestLink,
8} from "@t3tools/contracts";
9import { resolveSelectableModel, type SelectableModelOption } from "@t3tools/shared/model";
10import type { PromptLinkPullRequest, PromptLinkRepository } from "@t3tools/shared/promptLinks";
11import {
12 threadPullRequestKeyOf,
13 threadPullRequestsOf,
14 visibleThreadPullRequests,
15} from "@t3tools/shared/threadPullRequests";
16
17/**
18 * How web and mobile resolve a prompt link against what this client knows. Both read the same
19 * shells, so a link lands in the same project and thread on every surface.
20 */
21
22const repositoryKey = (repository: PromptLinkRepository) =>
23 `github.com/${repository.owner}/${repository.name}`.toLowerCase();
24
25/**
26 * The project a link's repository names. Callers pass projects most recently active first, so
27 * the user lands where they last worked. A fork only matches its own owner, not its upstream.
28 */
29export function findPromptLinkProject<
30 P extends { readonly repositoryIdentity?: RepositoryIdentity | null | undefined },
31>(projects: ReadonlyArray<P>, repository: PromptLinkRepository): P | undefined {
32 const key = repositoryKey(repository);
33 return projects.find(
34 (project) =>
35 project.repositoryIdentity != null &&
36 repositoryGroupingKeyOf(project.repositoryIdentity).toLowerCase() === key,
37 );
38}
39
40/** The most recently updated thread linked to the pull request, across every environment given. */
41export function findPromptLinkPullRequestThread<
42 T extends {
43 readonly updatedAt: string;
44 readonly pullRequests?: ReadonlyArray<ThreadPullRequestLink> | undefined;
45 readonly linkedPullRequest?: ThreadLinkedPullRequest | null | undefined;
46 },
47>(threads: ReadonlyArray<T>, pullRequest: PromptLinkPullRequest): T | undefined {
48 const key = threadPullRequestKeyOf({
49 host: "github.com",
50 repository: `${pullRequest.owner}/${pullRequest.name}`,
51 number: pullRequest.number,
52 });
53 let latest: T | undefined;
54 for (const thread of threads) {
55 if (latest !== undefined && thread.updatedAt <= latest.updatedAt) continue;
56 const linked = visibleThreadPullRequests(threadPullRequestsOf(thread)).some(
57 (link) => threadPullRequestKeyOf(link) === key,
58 );
59 if (linked) latest = thread;
60 }
61 return latest;
62}
63
64// People write the name they know. Driver kinds are what the server reports.
65const PROVIDER_ALIASES: Record<string, string> = { claude: "claudeagent" };
66
67/**
68 * The selection a link asks for, on one of the user's own provider instances. `exact` is false
69 * when the instance exists but the model does not, so its default model stands in. Null when
70 * no usable instance of that provider exists; the user's default applies then.
71 */
72export function resolvePromptLinkModelSelection(
73 providers: ReadonlyArray<
74 Pick<ServerProvider, "instanceId" | "driver" | "enabled" | "availability" | "status"> & {
75 readonly models: ReadonlyArray<
76 SelectableModelOption & { readonly isDefault?: boolean | undefined }
77 >;
78 }
79 >,
80 request: { readonly provider: string; readonly model: string | null },
81): { readonly selection: ModelSelection; readonly exact: boolean } | null {
82 const driver = PROVIDER_ALIASES[request.provider] ?? request.provider;
83 const usable = providers.filter(
84 (provider) =>
85 provider.driver.toLowerCase() === driver &&
86 provider.enabled &&
87 provider.availability !== "unavailable",
88 );
89 const instance =
90 usable.find((provider) => provider.status === "ready") ??
91 usable.find((provider) => provider.status === "warning");
92 if (instance === undefined) return null;
93 const requested =
94 request.model === null
95 ? null
96 : resolveSelectableModel(instance.driver, request.model, instance.models);
97 const model =
98 requested ??
99 (instance.models.find((candidate) => candidate.isDefault) ?? instance.models[0])?.slug;
100 if (model === undefined) return null;
101 return {
102 selection: { instanceId: instance.instanceId, model },
103 exact: request.model === null || requested !== null,
104 };
105}