1import * as Effect from "effect/Effect";
2import * as Schema from "effect/Schema";
3import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
4
5const encodeSubject = Schema.encodeEffect(
6 Schema.fromJsonString(Schema.Struct({ title: Schema.String, body: Schema.String })),
7);
8
9/** Providers select supported links before the title lookup budget is applied. */
10export const resolveThreadTitleLinks = Effect.fn("resolveThreadTitleLinks")(function* (input: {
11 message: string;
12 cwd: string;
13}) {
14 const providers = yield* SourceControlProviderRegistry.SourceControlProviderRegistry;
15 const links = new Map<string, NonNullable<ReturnType<typeof providers.resolveLink>>>();
16 for (const match of input.message.matchAll(/https:\/\/[^\s<>"')\]`]+/g)) {
17 const candidate = match[0].replace(/[.,;!?]+$/, "");
18 if (!URL.canParse(candidate)) continue;
19 const url = new URL(candidate);
20 url.hash = "";
21 url.search = "";
22 if (links.has(url.href)) continue;
23 const lookup = providers.resolveLink({ cwd: input.cwd, url });
24 if (!lookup) continue;
25 links.set(url.href, lookup);
26 if (links.size === 2) break;
27 }
28 const subjects = yield* Effect.forEach(
29 links,
30 ([url, lookup]) =>
31 lookup.pipe(
32 Effect.flatMap((subject) =>
33 encodeSubject({
34 title: subject.title.slice(0, 300),
35 body: subject.body?.slice(0, 1_200) ?? "",
36 }),
37 ),
38 Effect.map((summary) => `${url}\n${summary}`),
39 Effect.timeout("3 seconds"),
40 Effect.orElseSucceed(() => `${url}: unavailable`),
41 ),
42 { concurrency: 2 },
43 );
44 return subjects.length > 0 ? subjects.join("\n\n") : undefined;
45});