| 1 | import type { PromptLinkRepository } from "@t3tools/shared/promptLinks"; |
| 2 | |
| 3 | /** The query keys a prompt link adds to the `new` and thread routes. */ |
| 4 | export const PROMPT_LINK_PARAM_KEYS = ["prompt", "repo", "pr", "provider", "model"] as const; |
| 5 | |
| 6 | type RouteParams = Readonly<Record<string, unknown>> | undefined; |
| 7 | |
| 8 | /** |
| 9 | * Rebuilds the link's query from React Navigation route params, so the shared parser reads the |
| 10 | * same input on every surface. Null when the route carries no prompt link. |
| 11 | */ |
| 12 | export function promptLinkSearchFromParams( |
| 13 | params: RouteParams, |
| 14 | keys: ReadonlyArray<string> = PROMPT_LINK_PARAM_KEYS, |
| 15 | ): string | null { |
| 16 | const search = new URLSearchParams(); |
| 17 | for (const key of keys) { |
| 18 | const raw = params?.[key]; |
| 19 | const value = Array.isArray(raw) ? raw[0] : raw; |
| 20 | if (typeof value === "string") search.set(key, value); |
| 21 | } |
| 22 | return search.size > 0 ? search.toString() : null; |
| 23 | } |
| 24 | |
| 25 | /** Route params that remove a handled prompt link, so it applies once. */ |
| 26 | export function clearedPromptLinkParams(keys: ReadonlyArray<string> = PROMPT_LINK_PARAM_KEYS) { |
| 27 | return Object.fromEntries(keys.map((key) => [key, undefined])); |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * The text of the one notice a prompt link shows. Every link says the prompt came from a link; |
| 32 | * hidden characters replace that line with a stronger warning. |
| 33 | */ |
| 34 | export function promptLinkNotice(input: { |
| 35 | readonly hiddenCharacters: number; |
| 36 | readonly modelUnavailable?: boolean; |
| 37 | readonly pullRequestUnavailable?: number | null; |
| 38 | }): string { |
| 39 | return [ |
| 40 | input.hiddenCharacters > 0 |
| 41 | ? "This prompt had hidden characters, it may be malicious. They were removed. Read it before you send." |
| 42 | : "This prompt came from a link. Read it before you send.", |
| 43 | input.modelUnavailable |
| 44 | ? "The requested model is not available, so your default is used." |
| 45 | : null, |
| 46 | input.pullRequestUnavailable != null |
| 47 | ? `Pull request #${input.pullRequestUnavailable} could not be loaded, so this task starts from your default branch.` |
| 48 | : null, |
| 49 | ] |
| 50 | .filter((line) => line !== null) |
| 51 | .join("\n\n"); |
| 52 | } |
| 53 | |
| 54 | /** How a linked prompt lands in an existing thread composer. */ |
| 55 | export function linkedThreadDraftAction(currentText: string, prompt: string) { |
| 56 | if (currentText.trim().length === 0) return "fill" as const; |
| 57 | return currentText === prompt ? ("keep" as const) : ("ask" as const); |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * The machine a prompt without a project opens on: the most recently active one that offers |
| 62 | * threads without a project, else the first that does. |
| 63 | */ |
| 64 | export function promptLinkScratchEnvironmentId<Id extends string>( |
| 65 | environmentIdsMostRecentFirst: ReadonlyArray<Id>, |
| 66 | scratchEnvironmentIds: ReadonlyArray<Id>, |
| 67 | ): Id | null { |
| 68 | return ( |
| 69 | environmentIdsMostRecentFirst.find((id) => scratchEnvironmentIds.includes(id)) ?? |
| 70 | scratchEnvironmentIds[0] ?? |
| 71 | null |
| 72 | ); |
| 73 | } |
| 74 | |
| 75 | /** Each environment once, by its most recently updated thread first. */ |
| 76 | export function latestThreadEnvironmentIds<Id extends string>( |
| 77 | threads: ReadonlyArray<{ readonly environmentId: Id; readonly updatedAt: string }>, |
| 78 | ): ReadonlyArray<Id> { |
| 79 | const latest = new Map<Id, string>(); |
| 80 | for (const thread of threads) { |
| 81 | if (thread.updatedAt > (latest.get(thread.environmentId) ?? "")) { |
| 82 | latest.set(thread.environmentId, thread.updatedAt); |
| 83 | } |
| 84 | } |
| 85 | return Array.from(latest.keys()).sort((left, right) => |
| 86 | (latest.get(right) ?? "").localeCompare(latest.get(left) ?? ""), |
| 87 | ); |
| 88 | } |
| 89 | |
| 90 | /** `owner/name`, as the user knows the repository. */ |
| 91 | export const promptLinkRepositoryName = (repository: PromptLinkRepository) => |
| 92 | `${repository.owner}/${repository.name}`; |