1import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
2import {
3 type ModelSelection,
4 repositoryGroupingKeyOf,
5 type ServerProvider,
6} from "@t3tools/contracts";
7import {
8 buildNewThreadLink,
9 parsePromptLinkRepository,
10 PROMPT_LINK_MAX_PROMPT_LENGTH,
11 type PromptLinkRepository,
12} from "@t3tools/shared/promptLinks";
13
14import { type ComposerThreadTarget, useComposerDraftStore } from "~/composerDraftStore";
15import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
16import { configuredHostedAppUrl } from "~/hostedPairing";
17
18import { stackedThreadToast, toastManager } from "../ui/toast";
19
20/** A shareable link to a prompt on the hosted app, or null when the prompt cannot fit in one. */
21export function promptLinkFor(
22 prompt: string,
23 context?: {
24 readonly repository: PromptLinkRepository | null;
25 readonly provider: string | null;
26 readonly model: string | null;
27 },
28): string | null {
29 const trimmed = prompt.trim();
30 if (trimmed.length === 0 || trimmed.length > PROMPT_LINK_MAX_PROMPT_LENGTH) return null;
31 return buildNewThreadLink({ prompt: trimmed, ...context, origin: configuredHostedAppUrl() });
32}
33
34/** The project's GitHub repository, the only kind a prompt link can name. */
35export function promptLinkRepositoryOf(
36 project: Pick<EnvironmentProject, "repositoryIdentity"> | undefined,
37): PromptLinkRepository | null {
38 const identity = project?.repositoryIdentity;
39 if (identity == null) return null;
40 const key = repositoryGroupingKeyOf(identity);
41 return key.startsWith("github.com/")
42 ? parsePromptLinkRepository(key.slice("github.com/".length))
43 : null;
44}
45
46/**
47 * The draft in a composer as a link: its text and, with `withContext`, the project's repository
48 * and the model the composer has picked. Null when the composer holds no prompt to share.
49 */
50export function composerPromptLink(input: {
51 readonly target: ComposerThreadTarget;
52 readonly project: EnvironmentProject | undefined;
53 readonly providers: ReadonlyArray<ServerProvider>;
54 readonly threadModelSelection: ModelSelection | null;
55 readonly withContext: boolean;
56}): string | null {
57 const draft = useComposerDraftStore.getState().getComposerDraft(input.target);
58 if (!draft) return null;
59 if (!input.withContext) return promptLinkFor(draft.prompt);
60 const selection =
61 (draft.activeProvider ? draft.modelSelectionByProvider[draft.activeProvider] : undefined) ??
62 input.threadModelSelection;
63 const provider = selection
64 ? (input.providers.find((candidate) => candidate.instanceId === selection.instanceId)?.driver ??
65 null)
66 : null;
67 return promptLinkFor(draft.prompt, {
68 repository: promptLinkRepositoryOf(input.project),
69 provider,
70 model: provider === null ? null : (selection?.model ?? null),
71 });
72}
73
74/** Copies a prompt link and says so. */
75export async function copyPromptLink(link: string): Promise<void> {
76 try {
77 if (!(await writeTextToClipboard(link))) return;
78 toastManager.add({ type: "success", title: "Prompt link copied" });
79 } catch (error) {
80 toastManager.add(
81 stackedThreadToast({
82 type: "error",
83 title: "Could not copy the prompt link",
84 description: error instanceof Error ? error.message : "An error occurred.",
85 }),
86 );
87 }
88}