Skip to content
Source
Sign inSign up
Open History to see the last commit

promptLinks.ts

128 lines5.0 KB

1import {
2 repositoryGroupingKeyOf,
3 type EnvironmentId,
4 type ModelSelection,
5 type PullRequestLinkedThreadsResult,
6 type RepositoryIdentity,
7 type ServerProvider,
8 type ThreadLinkedPullRequest,
9 type ThreadId,
10 type ThreadPullRequestLink,
11} from "@t3tools/contracts";
12import { resolveSelectableModel, type SelectableModelOption } from "@t3tools/shared/model";
13import type { AsyncResult } from "effect/reactivity";
14import type { PromptLinkPullRequest, PromptLinkRepository } from "@t3tools/shared/promptLinks";
15import {
16 threadPullRequestKeyOf,
17 threadPullRequestsOf,
18 visibleThreadPullRequests,
19} from "./threadPullRequests.ts";
20​
21// A link's `owner/name` is GitHub's. GitCafe syncs a repository two-way with GitHub under the
22// same path, so a checkout cloned from git.cafe counts as the linked repository.
23const PROMPT_LINK_REPOSITORY_HOSTS = ["github.com", "git.cafe", "staging.git.cafe"];
24​
25/**
26 * The repository's projects in the order given. Callers pass projects most recently active
27 * first and open the first, so the user lands where they last worked.
28 */
29export function findPromptLinkProjects<
30 P extends { readonly repositoryIdentity?: RepositoryIdentity | null | undefined },
31>(projects: ReadonlyArray<P>, repository: PromptLinkRepository): P[] {
32 const path = `${repository.owner}/${repository.name}`.toLowerCase();
33 const keys = new Set(PROMPT_LINK_REPOSITORY_HOSTS.map((host) => `${host}/${path}`));
34 return projects.filter(
35 (project) =>
36 project.repositoryIdentity != null &&
37 keys.has(repositoryGroupingKeyOf(project.repositoryIdentity).toLowerCase()),
38 );
40​
41export function findPromptLinkPullRequestThread<
42 T extends {
43 readonly updatedAt: string;
44 readonly pullRequests?: ReadonlyArray<ThreadPullRequestLink> | undefined;
45 readonly linkedPullRequest?: ThreadLinkedPullRequest | null | undefined;
46 readonly lineage?: { readonly sideChat?: boolean | undefined } | undefined;
47 },
48>(threads: ReadonlyArray<T>, pullRequest: PromptLinkPullRequest): T | undefined {
49 const key = threadPullRequestKeyOf({
50 host: "github.com",
51 repository: `${pullRequest.owner}/${pullRequest.name}`,
52 number: pullRequest.number,
53 });
54 let latest: T | undefined;
55 for (const thread of threads) {
56 if (latest !== undefined && thread.updatedAt <= latest.updatedAt) continue;
57 // Side chats inherit their parent's links but stay hidden.
58 if (thread.lineage?.sideChat === true) continue;
59 const linked = visibleThreadPullRequests(threadPullRequestsOf(thread)).some(
60 (link) => threadPullRequestKeyOf(link) === key,
61 );
62 if (linked) latest = thread;
63 }
64 return latest;
66​
67/**
68 * The first open thread from per-environment pull request lookups, in the order given, once
69 * every lookup before it has answered. `null` when none has one; `undefined` until then.
70 */
71export function pickPromptLinkPullRequestThread(
72 lookups: ReadonlyArray<{
73 readonly environmentId: EnvironmentId;
74 readonly result: AsyncResult.AsyncResult<PullRequestLinkedThreadsResult, unknown>;
75 }>,
76): { readonly environmentId: EnvironmentId; readonly id: ThreadId } | null | undefined {
77 for (const { environmentId, result } of lookups) {
78 if (result._tag === "Initial" || result.waiting) return undefined;
79 const thread =
80 result._tag === "Success"
81 ? result.value.threads.find((candidate) => candidate.archivedAt === null)
82 : undefined;
83 if (thread !== undefined) return { environmentId, id: thread.id };
84 }
85 return null;
87​
88const PROVIDER_ALIASES: Record<string, string> = { claude: "claudeagent" };
89​
90/**
91 * `exact` is false when the instance exists but the model does not, so its default model
92 * stands in. Null when no usable instance of that provider exists; the user's default
93 * applies then.
94 */
95export function resolvePromptLinkModelSelection(
96 providers: ReadonlyArray<
97 Pick<ServerProvider, "instanceId" | "driver" | "enabled" | "availability" | "status"> & {
98 readonly models: ReadonlyArray<
99 SelectableModelOption & { readonly isDefault?: boolean | undefined }
100 >;
101 }
102 >,
103 request: { readonly provider: string; readonly model: string | null },
104): { readonly selection: ModelSelection; readonly exact: boolean } | null {
105 const driver = PROVIDER_ALIASES[request.provider] ?? request.provider;
106 const usable = providers.filter(
107 (provider) =>
108 provider.driver.toLowerCase() === driver &&
109 provider.enabled &&
110 provider.availability !== "unavailable",
111 );
112 const instance =
113 usable.find((provider) => provider.status === "ready") ??
114 usable.find((provider) => provider.status === "warning");
115 if (instance === undefined) return null;
116 const requested =
117 request.model === null
118 ? null
119 : resolveSelectableModel(instance.driver, request.model, instance.models);
120 const model =
121 requested ??
122 (instance.models.find((candidate) => candidate.isDefault) ?? instance.models[0])?.slug;
123 if (model === undefined) return null;
124 return {
125 selection: { instanceId: instance.instanceId, model },
126 exact: request.model === null || requested !== null,
127 };