From a3997512d2c905602e3c08d5f8fc7840aa14cfb5 Mon Sep 17 00:00:00 2001 From: Mohamed Aziz Mejri Date: Wed, 29 Oct 2025 17:43:19 +0100 Subject: [PATCH] Feat : allow referencing files (#1648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I implemented file referencing feature mentioned in issue #1591 --- ## Summary by cubic Adds support for referencing app files in chat using @file:. The input now autocompletes files, and the backend validates and surfaces referenced files in the chat context as read-only. - **New Features** - Chat input autocompletes @ mentions for apps, prompts, and files. - Recognizes @file: and converts it to a structured mention on submit. - Backend parses @file mentions, checks file existence, and adds a “Referenced Files” section to the system message. - New get-app-files IPC handler and useAppFiles hook to load file paths for the selected app (appFilesAtom added). - e2e test for mentioning a file and capturing server dump. --- e2e-tests/mention_files.spec.ts | 15 + .../mention_files.spec.ts_mention-file-1.txt | 1035 +++++++++++++++++ src/components/chat/LexicalChatInput.tsx | 44 +- 3 files changed, 1085 insertions(+), 9 deletions(-) create mode 100644 e2e-tests/mention_files.spec.ts create mode 100644 e2e-tests/snapshots/mention_files.spec.ts_mention-file-1.txt diff --git a/e2e-tests/mention_files.spec.ts b/e2e-tests/mention_files.spec.ts new file mode 100644 index 0000000..c1d84c9 --- /dev/null +++ b/e2e-tests/mention_files.spec.ts @@ -0,0 +1,15 @@ +import { test } from "./helpers/test_helper"; + +test("mention file", async ({ po }) => { + await po.setUp({ autoApprove: true }); + + await po.importApp("minimal-with-ai-rules"); + await po.goToAppsTab(); + await po.getChatInput().click(); + await po.getChatInput().fill("[dump] @"); + await po.page.getByRole("menuitem", { name: "Choose AI_RULES.md" }).click(); + await po.page.getByRole("button", { name: "Send message" }).click(); + await po.waitForChatCompletion(); + + await po.snapshotServerDump("all-messages"); +}); diff --git a/e2e-tests/snapshots/mention_files.spec.ts_mention-file-1.txt b/e2e-tests/snapshots/mention_files.spec.ts_mention-file-1.txt new file mode 100644 index 0000000..133ddbf --- /dev/null +++ b/e2e-tests/snapshots/mention_files.spec.ts_mention-file-1.txt @@ -0,0 +1,1035 @@ +=== +role: system +message: +${BUILD_SYSTEM_PREFIX} + +# Tech Stack + +- You are building a React application. +- Use TypeScript. +- Use React Router. KEEP the routes in src/App.tsx +- Always put source code in the src folder. +- Put pages into src/pages/ +- Put components into src/components/ +- The main page (default page) is src/pages/Index.tsx +- UPDATE the main page to include the new components. OTHERWISE, the user can NOT see any components! +- ALWAYS try to use the shadcn/ui library. +- Tailwind CSS: always use Tailwind CSS for styling components. Utilize Tailwind classes extensively for layout, spacing, colors, and other design aspects. + +Available packages and libraries: + +- The lucide-react package is installed for icons. +- You ALREADY have ALL the shadcn/ui components and their dependencies installed. So you don't need to install them again. +- You have ALL the necessary Radix UI components installed. +- Use prebuilt components from the shadcn/ui library after importing them. Note that these files shouldn't be edited, so make new components if you need to change them. + + +${BUILD_SYSTEM_POSTFIX} + + +If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets), +tell them that they need to add supabase to their app. + +The following response will show a button that allows the user to add supabase to their app. + + + +# Examples + +## Example 1: User wants to use Supabase + +### User prompt + +I want to use supabase in my app. + +### Assistant response + +You need to first add Supabase to your app. + + + +## Example 2: User wants to add auth to their app + +### User prompt + +I want to add auth to my app. + +### Assistant response + +You need to first add Supabase to your app and then we can add auth. + + + + +=== +role: user +message: This is my codebase. +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + + + + +# Tech Stack + +- You are building a React application. +- Use TypeScript. +- Use React Router. KEEP the routes in src/App.tsx +- Always put source code in the src folder. +- Put pages into src/pages/ +- Put components into src/components/ +- The main page (default page) is src/pages/Index.tsx +- UPDATE the main page to include the new components. OTHERWISE, the user can NOT see any components! +- ALWAYS try to use the shadcn/ui library. +- Tailwind CSS: always use Tailwind CSS for styling components. Utilize Tailwind classes extensively for layout, spacing, colors, and other design aspects. + +Available packages and libraries: + +- The lucide-react package is installed for icons. +- You ALREADY have ALL the shadcn/ui components and their dependencies installed. So you don't need to install them again. +- You have ALL the necessary Radix UI components installed. +- Use prebuilt components from the shadcn/ui library after importing them. Note that these files shouldn't be edited, so make new components if you need to change them. + + + + +// File contents excluded from context + + + +// File contents excluded from context + + + + + + + + + dyad-generated-app + + + +
+ + + + +
+ + +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +# Welcome to your Dyad app + + + + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + + + + +import { Toaster } from "@/components/ui/toaster"; +import { Toaster as Sonner } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Index from "./pages/Index"; +import NotFound from "./pages/NotFound"; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + + + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + +); + +export default App; + + + + +export const MadeWithDyad = () => { + return ( +
+ + Made with Dyad + +
+ ); +}; + +
+ + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + + --sidebar-background: 0 0% 98%; + + --sidebar-foreground: 240 5.3% 26.1%; + + --sidebar-primary: 240 5.9% 10%; + + --sidebar-primary-foreground: 0 0% 98%; + + --sidebar-accent: 240 4.8% 95.9%; + + --sidebar-accent-foreground: 240 5.9% 10%; + + --sidebar-border: 220 13% 91%; + + --sidebar-ring: 217.2 91.2% 59.8%; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } +} + + + + +import * as React from "react"; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState( + undefined, + ); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener("change", onChange); + }, []); + + return !!isMobile; +} + + + + +import * as React from "react"; + +import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; + +const TOAST_LIMIT = 1; +const TOAST_REMOVE_DELAY = 1000000; + +type ToasterToast = ToastProps & { + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; + +const _actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const; + +let count = 0; + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); +} + +type ActionType = typeof _actionTypes; + +type Action = + | { + type: ActionType["ADD_TOAST"]; + toast: ToasterToast; + } + | { + type: ActionType["UPDATE_TOAST"]; + toast: Partial; + } + | { + type: ActionType["DISMISS_TOAST"]; + toastId?: ToasterToast["id"]; + } + | { + type: ActionType["REMOVE_TOAST"]; + toastId?: ToasterToast["id"]; + }; + +interface State { + toasts: ToasterToast[]; +} + +const toastTimeouts = new Map>(); + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return; + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId); + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }); + }, TOAST_REMOVE_DELAY); + + toastTimeouts.set(toastId, timeout); +}; + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + }; + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t, + ), + }; + + case "DISMISS_TOAST": { + const { toastId } = action; + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId); + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id); + }); + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t, + ), + }; + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + }; + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + }; + } +}; + +const listeners: Array<(state: State) => void> = []; + +let memoryState: State = { toasts: [] }; + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action); + listeners.forEach((listener) => { + listener(memoryState); + }); +} + +type Toast = Omit; + +function toast({ ...props }: Toast) { + const id = genId(); + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }); + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss(); + }, + }, + }); + + return { + id: id, + dismiss, + update, + }; +} + +function useToast() { + const [state, setState] = React.useState(memoryState); + + React.useEffect(() => { + listeners.push(setState); + return () => { + const index = listeners.indexOf(setState); + if (index > -1) { + listeners.splice(index, 1); + } + }; + }, [state]); + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + }; +} + +export { useToast, toast }; + + + + +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + + + + +import { createRoot } from "react-dom/client"; +import App from "./App.tsx"; +import "./globals.css"; + +createRoot(document.getElementById("root")!).render(); + + + + +// Update this page (the content is just a fallback if you fail to update the page) + +import { MadeWithDyad } from "@/components/made-with-dyad"; + +const Index = () => { + return ( +
+
+

Welcome to Your Blank App

+

+ Start building your amazing project here! +

+
+ +
+ ); +}; + +export default Index; + +
+ + +import { useLocation } from "react-router-dom"; +import { useEffect } from "react"; + +const NotFound = () => { + const location = useLocation(); + + useEffect(() => { + console.error( + "404 Error: User attempted to access non-existent route:", + location.pathname, + ); + }, [location.pathname]); + + return ( +
+
+

404

+

Oops! Page not found

+ + Return to Home + +
+
+ ); +}; + +export default NotFound; + +
+ + +import { toast } from "sonner"; + +export const showSuccess = (message: string) => { + toast.success(message); +}; + +export const showError = (message: string) => { + toast.error(message); +}; + +export const showLoading = (message: string) => { + return toast.loading(message); +}; + +export const dismissToast = (toastId: string) => { + toast.dismiss(toastId); +}; + + + + +/// + + + + +import type { Config } from "tailwindcss"; + +export default { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + sidebar: { + DEFAULT: "hsl(var(--sidebar-background))", + foreground: "hsl(var(--sidebar-foreground))", + primary: "hsl(var(--sidebar-primary))", + "primary-foreground": "hsl(var(--sidebar-primary-foreground))", + accent: "hsl(var(--sidebar-accent))", + "accent-foreground": "hsl(var(--sidebar-accent-foreground))", + border: "hsl(var(--sidebar-border))", + ring: "hsl(var(--sidebar-ring))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { + height: "0", + }, + to: { + height: "var(--radix-accordion-content-height)", + }, + }, + "accordion-up": { + from: { + height: "var(--radix-accordion-content-height)", + }, + to: { + height: "0", + }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} satisfies Config; + + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} + + + + +import { defineConfig } from "vite"; +import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger"; +import react from "@vitejs/plugin-react-swc"; +import path from "path"; + +export default defineConfig(() => ({ + server: { + host: "::", + port: 8080, + }, + plugins: [dyadComponentTagger(), react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +})); + + + + + +=== +role: assistant +message: OK, got it. I'm ready to help + +=== +role: user +message: [dump] @file:AI_RULES.md \ No newline at end of file diff --git a/src/components/chat/LexicalChatInput.tsx b/src/components/chat/LexicalChatInput.tsx index 271d520..9d28457 100644 --- a/src/components/chat/LexicalChatInput.tsx +++ b/src/components/chat/LexicalChatInput.tsx @@ -26,6 +26,7 @@ import { forwardRef } from "react"; import { useAtomValue } from "jotai"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { MENTION_REGEX, parseAppMentions } from "@/shared/parse_mention_apps"; +import { useLoadApp } from "@/hooks/useLoadApp"; // Define the theme for mentions const beautifulMentionsTheme: BeautifulMentionsTheme = { @@ -38,9 +39,10 @@ const CustomMenuItem = forwardRef< HTMLLIElement, BeautifulMentionsMenuItemProps >(({ selected, item, ...props }, ref) => { - const isPrompt = typeof item !== "string" && item.data?.type === "prompt"; - const label = isPrompt ? "Prompt" : "App"; - const value = typeof item === "string" ? item : (item as any)?.value; + const isPrompt = item.data?.type === "prompt"; + const isApp = item.data?.type === "app"; + const label = isPrompt ? "Prompt" : isApp ? "App" : "File"; + const value = (item as any)?.value; return (
  • {label} @@ -181,7 +185,7 @@ function ExternalValueSyncPlugin({ // Build nodes from internal value, turning @app:Name and @prompt: into mention nodes let lastIndex = 0; let match: RegExpExecArray | null; - const combined = /@app:([a-zA-Z0-9_-]+)|@prompt:(\d+)/g; + const combined = /@app:([a-zA-Z0-9_-]+)|@prompt:(\d+)|@file:([^\s]+)/g; while ((match = combined.exec(value)) !== null) { const start = match.index; const full = match[0]; @@ -196,6 +200,9 @@ function ExternalValueSyncPlugin({ const id = Number(match[2]); const title = promptsById[id] || `prompt:${id}`; paragraph.append($createBeautifulMentionNode("@", title)); + } else if (match[3]) { + const filePath = match[3]; + paragraph.append($createBeautifulMentionNode("@", filePath)); } lastIndex = start + full.length; } @@ -243,6 +250,8 @@ export function LexicalChatInput({ const { prompts } = usePrompts(); const [shouldClear, setShouldClear] = useState(false); const selectedAppId = useAtomValue(selectedAppIdAtom); + const { app } = useLoadApp(selectedAppId); + const appFiles = app?.files; // Prepare mention items - convert apps to mention format const mentionItems = React.useMemo(() => { @@ -271,7 +280,10 @@ export function LexicalChatInput({ return true; }); - const appMentions = filteredApps.map((app) => app.name); + const appMentions = filteredApps.map((app) => ({ + value: app.name, + type: "app", + })); const promptItems = (prompts || []).map((p) => ({ value: p.title, @@ -279,10 +291,15 @@ export function LexicalChatInput({ id: p.id, })); + const fileItems = (appFiles || []).map((item) => ({ + value: item, + type: "file", + })); + return { - "@": [...appMentions, ...promptItems], + "@": [...appMentions, ...promptItems, ...fileItems], }; - }, [apps, selectedAppId, value, excludeCurrentApp, prompts]); + }, [apps, selectedAppId, value, excludeCurrentApp, prompts, appFiles]); const initialConfig = { namespace: "ChatInput", @@ -325,11 +342,20 @@ export function LexicalChatInput({ const regex = new RegExp(`@(${escapedTitle})(?![\\w-])`, "g"); textContent = textContent.replace(regex, `@prompt:${id}`); } + + for (const fullPath of appFiles || []) { + const escapedDisplay = fullPath.replace( + /[.*+?^${}()|[\]\\]/g, + "\\$&", + ); + const fileRegex = new RegExp(`@(${escapedDisplay})(?![\\w-])`, "g"); + textContent = textContent.replace(fileRegex, `@file:${fullPath}`); + } } onChange(textContent); }); }, - [onChange, apps, prompts], + [onChange, apps, prompts, appFiles], ); const handleSubmit = useCallback(() => {