1import type { ScopedThreadRef } from "@t3tools/contracts";
2import { parseThreadLinkPrompt, withoutPromptLinkParams } from "@t3tools/shared/promptLinks";
3import { useLocation, useNavigate } from "@tanstack/react-router";
4import { useEffect } from "react";
5
6import { stackedThreadToast, toastManager } from "../ui/toast";
7import { placeLinkedPrompt } from "./linkedPrompt";
8
9const handledLinkKeys = new Set<string>();
10
11/**
12 * Moves a thread link's `?prompt=` into that thread's composer, then drops it from the URL so a
13 * reload or a copied address does not bring it back. Rendered beside the thread view.
14 */
15export function ThreadLinkPrompt({ threadRef }: { readonly threadRef: ScopedThreadRef }) {
16 const navigate = useNavigate();
17 const location = useLocation({
18 select: (current) => ({
19 key: current.state.__TSR_key ?? current.href,
20 pathname: current.pathname,
21 search: current.searchStr,
22 }),
23 structuralSharing: true,
24 });
25
26 useEffect(() => {
27 if (!new URLSearchParams(location.search).has("prompt")) return;
28 if (!handledLinkKeys.has(location.key)) {
29 handledLinkKeys.add(location.key);
30 const parsed = parseThreadLinkPrompt(location.search);
31 if (!parsed.ok) {
32 toastManager.add(
33 stackedThreadToast({
34 type: "error",
35 title: "Could not open the linked prompt",
36 description: parsed.error,
37 }),
38 );
39 } else if (parsed.value !== null) {
40 placeLinkedPrompt(threadRef, parsed.value);
41 }
42 }
43 void navigate({
44 href: `${location.pathname}${withoutPromptLinkParams(location.search)}`,
45 replace: true,
46 });
47 }, [location, navigate, threadRef]);
48
49 return null;
50}