diff --git a/cli/templates/files/ai-agent/app/markdown-renderer.tsx b/cli/templates/files/ai-agent/app/markdown-renderer.tsx new file mode 100644 index 0000000000..7c911c7a45 --- /dev/null +++ b/cli/templates/files/ai-agent/app/markdown-renderer.tsx @@ -0,0 +1,16 @@ +"use client"; + +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import type { MarkdownRendererProps } from "veryfront/markdown"; + +/** + * Rich Markdown for assistant answers. + * + * `veryfront/markdown` presents plain source until a renderer is installed, so + * this component supplies one. Swap in any renderer that accepts + * `MarkdownRendererProps` to change how answers are parsed and rendered. + */ +export function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element { + return {source}; +} diff --git a/cli/templates/files/ai-agent/app/page.tsx b/cli/templates/files/ai-agent/app/page.tsx index aca68be69b..b911f20862 100644 --- a/cli/templates/files/ai-agent/app/page.tsx +++ b/cli/templates/files/ai-agent/app/page.tsx @@ -1,7 +1,13 @@ "use client"; import { Chat } from "veryfront/chat"; +import { MarkdownRendererProvider } from "veryfront/markdown"; +import { MarkdownRenderer } from "./markdown-renderer.tsx"; export default function ChatPage(): React.JSX.Element { - return ; + return ( + + + + ); } diff --git a/cli/templates/files/ai-agent/tsconfig.json b/cli/templates/files/ai-agent/tsconfig.json index f5392fef00..569002ba34 100644 --- a/cli/templates/files/ai-agent/tsconfig.json +++ b/cli/templates/files/ai-agent/tsconfig.json @@ -7,10 +7,19 @@ "jsx": "react-jsx", "skipLibCheck": true, "esModuleInterop": true, + "noEmit": true, + "allowImportingTsExtensions": true, "paths": { - "@/*": ["./*"] + "@/*": [ + "./*" + ] } }, - "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] } diff --git a/cli/templates/files/coding-agent/app/markdown-renderer.tsx b/cli/templates/files/coding-agent/app/markdown-renderer.tsx new file mode 100644 index 0000000000..18baa3657f --- /dev/null +++ b/cli/templates/files/coding-agent/app/markdown-renderer.tsx @@ -0,0 +1,16 @@ +'use client' + +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import type { MarkdownRendererProps } from 'veryfront/markdown' + +/** + * Rich Markdown for assistant answers. + * + * `veryfront/markdown` presents plain source until a renderer is installed, so + * this component supplies one. Swap in any renderer that accepts + * `MarkdownRendererProps` to change how answers are parsed and rendered. + */ +export function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element { + return {source} +} diff --git a/cli/templates/files/coding-agent/app/page.tsx b/cli/templates/files/coding-agent/app/page.tsx index 738b8c6bb1..97e9807fbd 100644 --- a/cli/templates/files/coding-agent/app/page.tsx +++ b/cli/templates/files/coding-agent/app/page.tsx @@ -1,14 +1,18 @@ 'use client' import { Chat } from 'veryfront/chat' +import { MarkdownRendererProvider } from 'veryfront/markdown' +import { MarkdownRenderer } from './markdown-renderer.tsx' export default function CodeAgent(): React.JSX.Element { return ( - + + + ) } diff --git a/cli/templates/files/coding-agent/tsconfig.json b/cli/templates/files/coding-agent/tsconfig.json index f5392fef00..569002ba34 100644 --- a/cli/templates/files/coding-agent/tsconfig.json +++ b/cli/templates/files/coding-agent/tsconfig.json @@ -7,10 +7,19 @@ "jsx": "react-jsx", "skipLibCheck": true, "esModuleInterop": true, + "noEmit": true, + "allowImportingTsExtensions": true, "paths": { - "@/*": ["./*"] + "@/*": [ + "./*" + ] } }, - "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] } diff --git a/cli/templates/files/docs-agent/app/markdown-renderer.tsx b/cli/templates/files/docs-agent/app/markdown-renderer.tsx new file mode 100644 index 0000000000..18baa3657f --- /dev/null +++ b/cli/templates/files/docs-agent/app/markdown-renderer.tsx @@ -0,0 +1,16 @@ +'use client' + +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import type { MarkdownRendererProps } from 'veryfront/markdown' + +/** + * Rich Markdown for assistant answers. + * + * `veryfront/markdown` presents plain source until a renderer is installed, so + * this component supplies one. Swap in any renderer that accepts + * `MarkdownRendererProps` to change how answers are parsed and rendered. + */ +export function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element { + return {source} +} diff --git a/cli/templates/files/docs-agent/app/page.tsx b/cli/templates/files/docs-agent/app/page.tsx index 896c62a09c..88d70a63a0 100644 --- a/cli/templates/files/docs-agent/app/page.tsx +++ b/cli/templates/files/docs-agent/app/page.tsx @@ -1,14 +1,18 @@ 'use client' import { Chat } from 'veryfront/chat' +import { MarkdownRendererProvider } from 'veryfront/markdown' +import { MarkdownRenderer } from './markdown-renderer.tsx' -export default function DocsChat() { +export default function DocsChat(): React.JSX.Element { return ( - + + + ) } diff --git a/cli/templates/files/multi-agent-system/app/markdown-renderer.tsx b/cli/templates/files/multi-agent-system/app/markdown-renderer.tsx new file mode 100644 index 0000000000..18baa3657f --- /dev/null +++ b/cli/templates/files/multi-agent-system/app/markdown-renderer.tsx @@ -0,0 +1,16 @@ +'use client' + +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import type { MarkdownRendererProps } from 'veryfront/markdown' + +/** + * Rich Markdown for assistant answers. + * + * `veryfront/markdown` presents plain source until a renderer is installed, so + * this component supplies one. Swap in any renderer that accepts + * `MarkdownRendererProps` to change how answers are parsed and rendered. + */ +export function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element { + return {source} +} diff --git a/cli/templates/files/multi-agent-system/app/page.tsx b/cli/templates/files/multi-agent-system/app/page.tsx index 296e3eb462..8fdc87f874 100644 --- a/cli/templates/files/multi-agent-system/app/page.tsx +++ b/cli/templates/files/multi-agent-system/app/page.tsx @@ -1,14 +1,18 @@ 'use client' import { Chat } from 'veryfront/chat' +import { MarkdownRendererProvider } from 'veryfront/markdown' +import { MarkdownRenderer } from './markdown-renderer.tsx' export default function MultiAgentChat(): React.JSX.Element { return ( - + + + ) } diff --git a/cli/templates/files/multi-agent-system/tsconfig.json b/cli/templates/files/multi-agent-system/tsconfig.json index f5392fef00..569002ba34 100644 --- a/cli/templates/files/multi-agent-system/tsconfig.json +++ b/cli/templates/files/multi-agent-system/tsconfig.json @@ -7,10 +7,19 @@ "jsx": "react-jsx", "skipLibCheck": true, "esModuleInterop": true, + "noEmit": true, + "allowImportingTsExtensions": true, "paths": { - "@/*": ["./*"] + "@/*": [ + "./*" + ] } }, - "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] } diff --git a/cli/templates/index.test.ts b/cli/templates/index.test.ts index 163285047a..84341d2ef7 100644 --- a/cli/templates/index.test.ts +++ b/cli/templates/index.test.ts @@ -8,7 +8,7 @@ import { import { describe, it } from "#veryfront/testing/bdd.ts"; import type { EvalRecord } from "veryfront/eval"; -import { getTemplate, templateConfigs } from "./index.ts"; +import { getTemplate, getTemplateConfig, templateConfigs } from "./index.ts"; import { STARTER_TEMPLATE_NAMES, type TemplateName } from "./types.ts"; const STYLED_STARTER_TEMPLATES: TemplateName[] = [ @@ -844,3 +844,66 @@ describe("cli/templates", () => { ); }); }); + +describe("chat starters scaffold a Markdown renderer", () => { + const CHAT_TEMPLATES: TemplateName[] = [ + "ai-agent", + "coding-agent", + "docs-agent", + "multi-agent-system", + ]; + + it("ships the renderer alongside the parser it imports", async () => { + for (const name of CHAT_TEMPLATES) { + const files = await getTemplate(name); + assertExists(files, `${name} should load`); + + const renderer = files.find((file) => file.path === "app/markdown-renderer.tsx"); + assertExists(renderer, `${name} should scaffold app/markdown-renderer.tsx`); + + const dependencies = getTemplateConfig(name)?.npmDependencies ?? {}; + for (const dependency of ["react-markdown", "remark-gfm"]) { + assertEquals( + typeof dependencies[dependency], + "string", + `${name} imports ${dependency} but does not install it`, + ); + } + } + }); + + it("allows the relative .tsx import during consumer tsc", async () => { + for (const name of CHAT_TEMPLATES) { + const files = await getTemplate(name); + const tsconfig = files?.find((file) => file.path === "tsconfig.json"); + assertExists(tsconfig, `${name} should declare consumer TypeScript options`); + + // `app/page.tsx` imports `./markdown-renderer.tsx`, which consumer tsc + // rejects with TS5097 unless both options are set. + assertEquals( + tsconfig.content.includes('"allowImportingTsExtensions": true'), + true, + `${name} imports a .tsx module and must allow the extension`, + ); + assertEquals( + tsconfig.content.includes('"noEmit": true'), + true, + `${name} needs noEmit to keep allowImportingTsExtensions valid`, + ); + } + }); + + it("gives the router aliases the same dependencies as ai-agent", () => { + // `getTemplate` serves ai-agent files for these names, so the config has to + // follow or they scaffold the renderer with nothing to import. + const expected = getTemplateConfig("ai-agent")?.npmDependencies; + + for (const alias of ["pages-router", "app-router"] as TemplateName[]) { + assertEquals( + getTemplateConfig(alias)?.npmDependencies, + expected, + `${alias} scaffolds the ai-agent files and must resolve its dependencies`, + ); + } + }); +}); diff --git a/cli/templates/index.ts b/cli/templates/index.ts index b3616d78a9..17b681514b 100644 --- a/cli/templates/index.ts +++ b/cli/templates/index.ts @@ -44,10 +44,34 @@ export { validateFeatures, } from "./feature-loader.ts"; +/** + * Markdown renderer dependencies for the chat starters. + * + * `veryfront/markdown` presents plain source until a renderer is installed, so + * every starter that renders `` scaffolds one in `app/markdown-renderer.tsx` + * and installs the parser it uses. Versions are exact: these reach the browser + * through the module pipeline, where a floating range would resolve to whatever + * is latest at request time. + */ +const CHAT_MARKDOWN_DEPENDENCIES: Record = { + "react-markdown": "9.0.3", + "remark-gfm": "4.0.1", +}; + export const templateConfigs: Partial> = { + "ai-agent": { + npmDependencies: { ...CHAT_MARKDOWN_DEPENDENCIES }, + }, + "coding-agent": { + npmDependencies: { ...CHAT_MARKDOWN_DEPENDENCIES }, + }, + "multi-agent-system": { + npmDependencies: { ...CHAT_MARKDOWN_DEPENDENCIES }, + }, "docs-agent": { firstPartyExtensions: ["@veryfront/ext-document-kreuzberg"], npmDependencies: { + ...CHAT_MARKDOWN_DEPENDENCIES, "@kreuzberg/node": "^4.4.2", "@kreuzberg/wasm": "4.5.2", }, @@ -56,9 +80,19 @@ export const templateConfigs: Partial> = { const DIRECTORY_BASED_TEMPLATES: TemplateName[] = [...STARTER_TEMPLATE_NAMES]; +/** + * The router aliases scaffold the `ai-agent` files, so they must resolve the + * same config. Keeping the mapping in one place stops files and dependencies + * from drifting apart. + */ +function resolveTemplateAlias(name: TemplateName): TemplateName { + return name === "pages-router" || name === "app-router" ? "ai-agent" : name; +} + export async function getTemplate(name: TemplateName): Promise { - if (name === "pages-router" || name === "app-router") { - return getTemplate("ai-agent"); + const resolved = resolveTemplateAlias(name); + if (resolved !== name) { + return getTemplate(resolved); } if (!DIRECTORY_BASED_TEMPLATES.includes(name)) { @@ -78,7 +112,7 @@ export async function getTemplate(name: TemplateName): Promise\n \n Assistant\n \n \n {children}\n \n );\n}\n", - "app/page.tsx": "\"use client\";\n\nimport { Chat } from \"veryfront/chat\";\n\nexport default function ChatPage(): React.JSX.Element {\n return ;\n}\n", + "app/markdown-renderer.tsx": "\"use client\";\n\nimport ReactMarkdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport type { MarkdownRendererProps } from \"veryfront/markdown\";\n\n/**\n * Rich Markdown for assistant answers.\n *\n * `veryfront/markdown` presents plain source until a renderer is installed, so\n * this component supplies one. Swap in any renderer that accepts\n * `MarkdownRendererProps` to change how answers are parsed and rendered.\n */\nexport function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element {\n return {source};\n}\n", + "app/page.tsx": "\"use client\";\n\nimport { Chat } from \"veryfront/chat\";\nimport { MarkdownRendererProvider } from \"veryfront/markdown\";\nimport { MarkdownRenderer } from \"./markdown-renderer.tsx\";\n\nexport default function ChatPage(): React.JSX.Element {\n return (\n \n \n \n );\n}\n", "evals/assistant.eval.ts": "import { datasets, evalAgent, judges, metrics } from \"veryfront/eval\";\n\nexport default evalAgent({\n name: \"Assistant smoke test\",\n target: \"agent:assistant\",\n dataset: datasets.inline([\n {\n id: \"calculator\",\n input:\n \"Calculate an 18% tip on $84.50, split the total among three people, and explain the result briefly.\",\n reference: \"$99.71 total; two people pay $33.24 and one pays $33.23.\",\n },\n ]),\n metrics: [\n metrics.answer.regex({\n pattern: String.raw`(?\n \n \n\n", "README.md": "# AI Agent\n\nA small, customizable agent with a streaming chat UI and tool support.\n\n## What's included\n\n- Single assistant agent with streaming chat UI\n- Example calculator tool\n- Smoke eval for the agent and calculator\n- App-mode `Chat` component for real-time responses\n\n## Structure\n\n```\nagents/assistant.ts Agent definition\ntools/calculator.ts Example tool\nevals/assistant.eval.ts Agent smoke eval\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Chat interface\n```\n\n## Customize\n\n- Edit `agents/assistant.ts` to change the agent's identity, instructions, and suggestions.\n- Add or replace files in `tools/` to give the agent new capabilities.\n- Update `evals/assistant.eval.ts`, then run `npm run eval -- assistant`.\n- Edit `app/page.tsx` when you need to customize the chat UI.\n", "tools/calculator.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\nexport default tool({\n id: \"calculator\",\n description: \"Perform arithmetic. For round, a is the value and b is the decimal places.\",\n inputSchema: defineSchema((v) => v.object({\n operation: v.enum([\"add\", \"subtract\", \"multiply\", \"divide\", \"round\"]),\n a: v.number(),\n b: v.number(),\n }))(),\n execute: ({ operation, a, b }) => {\n const precision = Math.min(100, Math.max(0, Math.trunc(b)));\n\n if (operation === \"divide\" && b === 0) {\n throw new Error(\"Cannot divide by zero\");\n }\n\n if (operation === \"add\") return { result: a + b };\n if (operation === \"subtract\") return { result: a - b };\n if (operation === \"multiply\") return { result: a * b };\n if (operation === \"round\") {\n const offset = Math.sign(a) * Number.EPSILON * Math.max(1, Math.abs(a));\n return { result: Number((a + offset).toFixed(precision)) };\n }\n return { result: a / b };\n },\n});\n", - "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n" + "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"allowImportingTsExtensions\": true,\n \"paths\": {\n \"@/*\": [\n \"./*\"\n ]\n }\n },\n \"include\": [\n \"**/*.ts\",\n \"**/*.tsx\"\n ],\n \"exclude\": [\n \"node_modules\"\n ]\n}\n" } }, "coding-agent": { @@ -39,14 +40,15 @@ "agents/coder.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"coder\",\n name: \"Code Agent\",\n description: \"Read, search, and edit project files.\",\n system: `You are an expert coding assistant. You can read, search, and modify code files in the project.\n\nWhen asked to make changes:\n1. First read the relevant files to understand the codebase\n2. Explain what you'll change and why\n3. Make the changes\n4. Verify the result\n\nAlways explain your reasoning before making edits.`,\n tools: true,\n maxSteps: 15,\n suggestions: [\n {\n type: \"prompt\",\n title: \"Explain the codebase\",\n prompt: \"Explain how this project is organized.\",\n },\n {\n type: \"prompt\",\n title: \"Make a change\",\n prompt: \"Make this code change: \",\n },\n ],\n});\n", "app/api/ag-ui/route.ts": "import { createAgUiHandler } from \"veryfront/agent\";\n\nexport const POST = createAgUiHandler(\"coder\");\n", "app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n \n Code Agent\n \n \n
\n
\n
\n
\n
\n \n \n \n
\n
\n

\n code-agent\n

\n

\n read, search, edit project files\n

\n
\n
\n \n ready\n
\n
\n
\n {children}\n
\n
\n \n );\n}\n", - "app/page.tsx": "'use client'\n\nimport { Chat } from 'veryfront/chat'\n\nexport default function CodeAgent(): React.JSX.Element {\n return (\n \n )\n}\n", + "app/markdown-renderer.tsx": "'use client'\n\nimport ReactMarkdown from 'react-markdown'\nimport remarkGfm from 'remark-gfm'\nimport type { MarkdownRendererProps } from 'veryfront/markdown'\n\n/**\n * Rich Markdown for assistant answers.\n *\n * `veryfront/markdown` presents plain source until a renderer is installed, so\n * this component supplies one. Swap in any renderer that accepts\n * `MarkdownRendererProps` to change how answers are parsed and rendered.\n */\nexport function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element {\n return {source}\n}\n", + "app/page.tsx": "'use client'\n\nimport { Chat } from 'veryfront/chat'\nimport { MarkdownRendererProvider } from 'veryfront/markdown'\nimport { MarkdownRenderer } from './markdown-renderer.tsx'\n\nexport default function CodeAgent(): React.JSX.Element {\n return (\n \n \n \n )\n}\n", "globals.css": "@import \"tailwindcss\";\n", "public/favicon.svg": "\n \n \n\n", "README.md": "# Coding Agent\n\nAn AI assistant that can read, understand, and modify project files.\n\n## What's included\n\n- Coder agent with file system tools\n- Read, list, and edit files through conversation\n- Safe search/replace editing pattern\n\n## Structure\n\n```\nagents/coder.ts Agent with coding instructions\ntools/\n read-file.ts Read file contents\n list-files.ts List directory contents\n edit-file.ts Search and replace in files\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Chat interface\n```\n\nThis starter is not production-ready.\n", "tools/edit-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { cwd, readTextFile, realPath, resolve, writeTextFile } from \"veryfront/fs\";\n\n/** True when `target` is the same as, or nested under, `root` (both canonical). */\nfunction isWithin(root: string, target: string): boolean {\n const r = root.replace(/\\\\/g, \"/\");\n const t = target.replace(/\\\\/g, \"/\");\n return t === r || t.startsWith(`${r}/`);\n}\n\nexport default tool({\n id: \"edit-file\",\n description: \"Edit a file by replacing a specific string with new content\",\n inputSchema: defineSchema((v) => v.object({\n path: v.string().describe(\"File path relative to the project root\"),\n search: v.string().describe(\"Exact string to find in the file\"),\n replace: v.string().describe(\"String to replace it with\"),\n }))(),\n execute: async ({ path, search, replace }) => {\n let projectDir: string;\n let absolute: string;\n try {\n // Canonicalize both sides so a symlink that points outside the project\n // is resolved to its real target before the containment check.\n projectDir = await realPath(cwd());\n absolute = await realPath(resolve(projectDir, path));\n } catch {\n return { error: `File not found: ${path}` };\n }\n if (!isWithin(projectDir, absolute)) {\n return { error: `Path escapes project directory: ${path}` };\n }\n\n const content = await readTextFile(absolute);\n if (!content.includes(search)) {\n return { error: \"Search string not found in file\" };\n }\n\n const updated = content.replace(search, replace);\n await writeTextFile(absolute, updated);\n return { path, success: true };\n },\n});\n", "tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { cwd, readDir, realPath, resolve } from \"veryfront/fs\";\n\n/** True when `target` is the same as, or nested under, `root` (both canonical). */\nfunction isWithin(root: string, target: string): boolean {\n const r = root.replace(/\\\\/g, \"/\");\n const t = target.replace(/\\\\/g, \"/\");\n return t === r || t.startsWith(`${r}/`);\n}\n\nexport default tool({\n id: \"list-files\",\n description: \"List files in a project directory\",\n inputSchema: defineSchema((v) => v.object({\n directory: v\n .string()\n .default(\".\")\n .describe(\"Directory path relative to project root\"),\n extensions: v\n .array(v.string())\n .optional()\n .describe(\"Filter by file extensions (e.g. ['.ts', '.tsx'])\"),\n }))(),\n execute: async ({ directory, extensions }) => {\n let projectDir: string;\n let absolute: string;\n try {\n // Canonicalize both sides so a symlink that points outside the project\n // is resolved to its real target before the containment check.\n projectDir = await realPath(cwd());\n absolute = await realPath(resolve(projectDir, directory));\n } catch {\n return { error: `Directory not found: ${directory}` };\n }\n if (!isWithin(projectDir, absolute)) {\n return { error: `Path escapes project directory: ${directory}` };\n }\n\n const entries = await readDir(absolute);\n\n let files = entries\n .filter((e) => e.isFile)\n .map((e) => e.name);\n\n if (extensions?.length) {\n files = files.filter((f) =>\n extensions.some((ext) => f.endsWith(ext))\n );\n }\n\n return { directory, files, count: files.length };\n },\n});\n", "tools/read-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { cwd, readTextFile, realPath, resolve } from \"veryfront/fs\";\n\n/** True when `target` is the same as, or nested under, `root` (both canonical). */\nfunction isWithin(root: string, target: string): boolean {\n const r = root.replace(/\\\\/g, \"/\");\n const t = target.replace(/\\\\/g, \"/\");\n return t === r || t.startsWith(`${r}/`);\n}\n\nexport default tool({\n id: \"read-file\",\n description: \"Read the contents of a file in the project\",\n inputSchema: defineSchema((v) => v.object({\n path: v.string().describe(\"File path relative to the project root\"),\n }))(),\n execute: async ({ path }) => {\n let projectDir: string;\n let absolute: string;\n try {\n // Canonicalize both sides so a symlink that points outside the project\n // is resolved to its real target before the containment check.\n projectDir = await realPath(cwd());\n absolute = await realPath(resolve(projectDir, path));\n } catch {\n return { error: `File not found: ${path}` };\n }\n if (!isWithin(projectDir, absolute)) {\n return { error: `Path escapes project directory: ${path}` };\n }\n const content = await readTextFile(absolute);\n return { path, content };\n },\n});\n", - "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n" + "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"allowImportingTsExtensions\": true,\n \"paths\": {\n \"@/*\": [\n \"./*\"\n ]\n }\n },\n \"include\": [\n \"**/*.ts\",\n \"**/*.tsx\"\n ],\n \"exclude\": [\n \"node_modules\"\n ]\n}\n" } }, "docs-agent": { @@ -56,7 +58,8 @@ "app/api/ingest/route.ts": "import { store } from \"../../../store.ts\";\n\nexport async function POST() {\n await store.indexContentDir();\n return Response.json({ ok: true });\n}\n", "app/api/uploads/route.ts": "import { createUploadHandler } from \"veryfront/embedding\";\nimport { store } from \"../../../store.ts\";\n\nexport const { POST, GET, DELETE } = createUploadHandler(store, {\n auth: { type: \"none\", allowUnauthenticated: true },\n});\n", "app/layout.tsx": "'use client'\n\nimport \"../globals.css\";\nimport {\n AppShell,\n ChatSidebar,\n ChatThemeScope,\n ConversationsProvider,\n Tabs,\n TabsItem,\n} from \"veryfront/chat\";\nimport { Head } from \"veryfront/head\";\nimport { useRouter } from \"veryfront/router\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }): React.ReactNode {\n const router = useRouter();\n const activeTab = router.pathname.startsWith(\"/uploads\") ? \"uploads\" : \"chat\";\n\n return (\n <>\n \n Docs Agent\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n router.push(value === \"uploads\" ? \"/uploads\" : \"/\")}\n >\n Chat\n Uploads\n \n
\n
\n \n {children}\n \n
\n
\n
\n
\n \n );\n}\n", - "app/page.tsx": "'use client'\n\nimport { Chat } from 'veryfront/chat'\n\nexport default function DocsChat() {\n return (\n \n )\n}\n", + "app/markdown-renderer.tsx": "'use client'\n\nimport ReactMarkdown from 'react-markdown'\nimport remarkGfm from 'remark-gfm'\nimport type { MarkdownRendererProps } from 'veryfront/markdown'\n\n/**\n * Rich Markdown for assistant answers.\n *\n * `veryfront/markdown` presents plain source until a renderer is installed, so\n * this component supplies one. Swap in any renderer that accepts\n * `MarkdownRendererProps` to change how answers are parsed and rendered.\n */\nexport function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element {\n return {source}\n}\n", + "app/page.tsx": "'use client'\n\nimport { Chat } from 'veryfront/chat'\nimport { MarkdownRendererProvider } from 'veryfront/markdown'\nimport { MarkdownRenderer } from './markdown-renderer.tsx'\n\nexport default function DocsChat(): React.JSX.Element {\n return (\n \n \n \n )\n}\n", "app/uploads/page.tsx": "'use client'\n\nimport { AttachmentsPanel, useUploadsRegistry } from 'veryfront/chat'\n\nconst UPLOAD_API = '/api/uploads'\nconst ACCEPT = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.csv,.txt,.md,.mdx,.html,.rtf,.epub,.json,.xml'\n\nexport default function UploadsPage(): React.JSX.Element {\n const uploads = useUploadsRegistry({ url: UPLOAD_API, storageKey: 'rag-uploads' })\n const error = uploads.uploadError\n ?? uploads.refreshError\n ?? uploads.removeError\n ?? uploads.storageError\n\n return (\n <>\n {error && (\n

\n {error.message}\n

\n )}\n \n \n )\n}\n", "content/architecture.md": "# Architecture\n\nAcme Platform uses a modular, event-driven architecture.\n\n## Core Components\n\n### API Gateway\nRoutes incoming requests to the appropriate microservice. Handles authentication, rate limiting, and request validation.\n\n### Event Bus\nAsynchronous message broker connecting all services. Supports pub/sub and point-to-point messaging patterns.\n\n### Data Layer\nMulti-tenant data storage with automatic sharding. Supports PostgreSQL for relational data and Redis for caching.\n\n## Request Flow\n\n1. Client sends request to API Gateway\n2. Gateway validates authentication token\n3. Request is routed to the target service\n4. Service processes request and publishes events\n5. Response is returned through the gateway\n\n## Scaling\n\nEach component scales independently. The API Gateway uses horizontal scaling with load balancing. Services auto-scale based on queue depth and CPU utilization.\n\n## Security\n\n- All inter-service communication uses mTLS\n- API tokens are rotated every 24 hours\n- Data at rest is encrypted with AES-256\n- Audit logs are retained for 90 days\n", "content/getting-started.md": "# Getting Started\n\nWelcome to Acme Platform. This guide covers initial setup and core concepts.\n\n## Installation\n\nInstall the CLI globally:\n\n```bash\nnpm install -g @acme/cli\n```\n\n## Creating a Project\n\nRun the init command to scaffold a new project:\n\n```bash\nacme init my-project\ncd my-project\n```\n\n## Project Structure\n\n- `src/`: Application source code\n- `config/`: Configuration files\n- `tests/`: Test suite\n- `docs/`: Documentation\n\n## Configuration\n\nCreate an `acme.config.ts` file in your project root:\n\n```ts\nexport default {\n name: \"my-project\",\n region: \"us-east-1\",\n features: [\"auth\", \"storage\"],\n};\n```\n\n## Next Steps\n\n- Read the [Architecture Guide](./architecture) to understand the system design\n- Check [API Reference](./api-reference) for available endpoints\n- Join our Discord community for support\n", @@ -84,12 +87,13 @@ "agents/writer.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"writer\",\n system:\n \"You are a writing specialist. \" +\n \"Take research notes and transform them into clear, engaging prose. \" +\n \"Use a professional but approachable tone.\",\n maxSteps: 3,\n});\n", "app/api/ag-ui/route.ts": "import { createAgUiHandler } from \"veryfront/agent\";\n\nexport const POST = createAgUiHandler(\"orchestrator\");\n", "app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n \n Multi-Agent System\n \n \n
\n
\n
\n
\n
\n O\n
\n
\n R\n
\n
\n W\n
\n
\n
\n

\n Agent Team\n

\n

\n Orchestrator, Researcher, Writer\n

\n
\n
\n
\n {children}\n
\n \n );\n}\n", - "app/page.tsx": "'use client'\n\nimport { Chat } from 'veryfront/chat'\n\nexport default function MultiAgentChat(): React.JSX.Element {\n return (\n \n )\n}\n", + "app/markdown-renderer.tsx": "'use client'\n\nimport ReactMarkdown from 'react-markdown'\nimport remarkGfm from 'remark-gfm'\nimport type { MarkdownRendererProps } from 'veryfront/markdown'\n\n/**\n * Rich Markdown for assistant answers.\n *\n * `veryfront/markdown` presents plain source until a renderer is installed, so\n * this component supplies one. Swap in any renderer that accepts\n * `MarkdownRendererProps` to change how answers are parsed and rendered.\n */\nexport function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element {\n return {source}\n}\n", + "app/page.tsx": "'use client'\n\nimport { Chat } from 'veryfront/chat'\nimport { MarkdownRendererProvider } from 'veryfront/markdown'\nimport { MarkdownRenderer } from './markdown-renderer.tsx'\n\nexport default function MultiAgentChat(): React.JSX.Element {\n return (\n \n \n \n )\n}\n", "globals.css": "@import \"tailwindcss\";\n", "public/favicon.svg": "\n \n \n\n", "README.md": "# Multi-Agent System\n\nA team of specialized agents that collaborate on tasks.\n\n## What's included\n\n- Orchestrator that delegates to researcher and writer agents\n- Agent-as-tool composition via `getAgentsAsTools()`\n- Web search tool (placeholder, configure your own API)\n\n## Structure\n\n```\nagents/\n orchestrator.ts Coordinates the team\n researcher.ts Gathers information\n writer.ts Produces polished content\ntools/web-search.ts Placeholder search tool\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Chat interface\n```\n\nThis starter is not production-ready.\n", "tools/web-search.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\nexport default tool({\n id: \"web-search\",\n description: \"Search the web for information on a topic\",\n inputSchema: defineSchema((v) => v.object({\n query: v.string().describe(\"Search query\"),\n }))(),\n execute: async ({ query: _query }) => {\n // Connect a real search API to use this tool.\n // Popular options: Tavily, SerpAPI, Brave Search\n throw new Error(\n \"No search API configured. \" +\n \"See https://veryfront.com/docs/code/guides/tools for setup instructions.\",\n );\n },\n});\n", - "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n" + "tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"noEmit\": true,\n \"allowImportingTsExtensions\": true,\n \"paths\": {\n \"@/*\": [\n \"./*\"\n ]\n }\n },\n \"include\": [\n \"**/*.ts\",\n \"**/*.tsx\"\n ],\n \"exclude\": [\n \"node_modules\"\n ]\n}\n" } }, "saas-starter": { diff --git a/docs/api-reference/veryfront/chat.md b/docs/api-reference/veryfront/chat.md index 541733d616..8a11dacd88 100644 --- a/docs/api-reference/veryfront/chat.md +++ b/docs/api-reference/veryfront/chat.md @@ -185,7 +185,7 @@ Result returned from use agent. | `InferenceBadge` | Render inference badge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/inference-badge.tsx#L15) | | `InlineCitation` | Render the default citation or compose its `Trigger` and `Card` parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/inline-citation.tsx#L308) | | `Loader` | Render loader. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/animations.tsx#L14) | -| `Markdown` | Present Markdown source using an injected rich renderer or the explicit dependency-free plain-source contract. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/markdown.tsx#L126) | +| `Markdown` | Present Markdown source using an injected rich renderer or the explicit dependency-free plain-source contract. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/markdown.tsx#L160) | | `Message` | Message - render `` for the default turn, or compose `Message.Root` + `Message.Header`/`Content`/`Actions`/… for a custom layout. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/message.tsx#L775) | | `MessageActionBar` | Context-free message actions with addressable `Copy`, `Copied`, `Regenerate`, and `Edit` icon leaves. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/message-actions.tsx#L211) | | `MessageContextProvider` | Render message context provider. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/message-context.tsx#L123) | diff --git a/docs/api-reference/veryfront/markdown.md b/docs/api-reference/veryfront/markdown.md index 486501aba0..f27f1463c0 100644 --- a/docs/api-reference/veryfront/markdown.md +++ b/docs/api-reference/veryfront/markdown.md @@ -28,7 +28,7 @@ import { Markdown } from "veryfront/markdown"; | Name | Description | Source | | -------------------------- | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| `Markdown` | Present Markdown source using an injected rich renderer or the explicit dependency-free plain-source contract. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/markdown.tsx#L126) | +| `Markdown` | Present Markdown source using an injected rich renderer or the explicit dependency-free plain-source contract. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/markdown.tsx#L160) | | `MarkdownRendererProvider` | Provide a trusted rich-Markdown renderer to a React subtree. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/markdown.tsx#L94) | ### Classes diff --git a/docs/guides/chat-ui.md b/docs/guides/chat-ui.md index 4f03228dc1..31542286b7 100644 --- a/docs/guides/chat-ui.md +++ b/docs/guides/chat-ui.md @@ -304,6 +304,59 @@ different requests would share the same store. Use chat context providers only when nested components need direct state access. Prefer preset props or composition components first. +## Render Markdown in chat + +`veryfront/markdown` presents plain escaped source until a renderer is +installed, so `` shows raw Markdown on its own. Every chat starter +scaffolds a renderer in `app/markdown-renderer.tsx` and installs it around +``, so a new project renders assistant answers with no extra setup. + +Add the same two pieces to an existing project. Install the parser: + +```bash +npm install react-markdown@9.0.3 remark-gfm@4.0.1 +``` + +Then create the renderer and install it for the chat subtree: + +```tsx +// app/markdown-renderer.tsx +"use client"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import type { MarkdownRendererProps } from "veryfront/markdown"; + +export function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element { + return {source}; +} +``` + +```tsx +// app/page.tsx +"use client"; +import { Chat } from "veryfront/chat"; +import { MarkdownRendererProvider } from "veryfront/markdown"; +import { MarkdownRenderer } from "./markdown-renderer.tsx"; + +export default function ChatPage() { + return ( + + + + ); +} +``` + +The provider covers assistant answers and reasoning. Chat applies its own prose +styling around whatever the renderer returns, so lists, headings, and inline +code match the rest of the chat surface. Pin the parser to an exact version: +these packages reach the browser through the module pipeline, where a floating +range resolves to whatever is latest at request time. + +Your renderer owns parsing, sanitization, and link policy. To add syntax +highlighting, tables, or math, extend it with the remark and rehype plugins you +want rather than changing anything in chat. + ## Present Markdown source safely `veryfront/markdown` is the dependency-free Markdown boundary used by chat diff --git a/src/react/components/chat/markdown.test.tsx b/src/react/components/chat/markdown.test.tsx index 3f037359b9..b64a8d5edf 100644 --- a/src/react/components/chat/markdown.test.tsx +++ b/src/react/components/chat/markdown.test.tsx @@ -159,6 +159,19 @@ describe("Markdown", () => { ); }); + it("styles renderer output but leaves plain source unstyled", () => { + function PassThrough({ source }: MarkdownRendererProps): React.ReactElement { + return
{source}
; + } + const rendered = renderToString(text); + const plain = renderToString(text); + + // `&` is HTML-escaped inside the class attribute. + assertStringIncludes(rendered, "[&_ul]:list-disc"); + assertStringIncludes(rendered, "[&_table]:w-full"); + assertEquals(plain.includes("_ul]:list-disc"), false); + }); + it("keeps long source within the chat column", () => { const html = renderToString( long-source-link-without-natural-break-points, diff --git a/src/react/components/chat/markdown.tsx b/src/react/components/chat/markdown.tsx index 6f58ad8c2d..6a6af4a7f8 100644 --- a/src/react/components/chat/markdown.tsx +++ b/src/react/components/chat/markdown.tsx @@ -119,6 +119,40 @@ export interface MarkdownProps { const MARKDOWN_CONTAINER_CLASS = "max-w-none min-w-0 overflow-hidden break-words text-base leading-relaxed text-[var(--foreground)] [overflow-wrap:anywhere]"; +// Element styling for semantic output. Declared as descendant selectors rather +// than through the `@tailwindcss/typography` plugin, so consumers do not have +// to install that plugin. Tailwind preflight strips list markers, so +// `list-disc`/`list-decimal` and padding are restored here. Applied only on the +// renderer branch: plain source has no elements to style. +const MARKDOWN_PROSE_CLASS = [ + // paragraph rhythm + "[&_p]:my-4", + // lists — restore the markers and indentation preflight removes + "[&_ul]:my-4 [&_ul]:list-disc [&_ul]:pl-6 [&_ol]:my-4 [&_ol]:list-decimal [&_ol]:pl-6 [&_li]:my-1.5 [&_li]:pl-1", + "[&_ul_ul]:my-1 [&_ol_ol]:my-1 [&_ul_ol]:my-1 [&_ol_ul]:my-1", + "[&_li>p]:my-0 [&_li_p]:my-2", + // headings + "[&_h1]:mt-6 [&_h1]:mb-2 [&_h1]:text-lg [&_h1]:font-semibold", + "[&_h2]:mt-5 [&_h2]:mb-2 [&_h2]:text-base [&_h2]:font-semibold", + "[&_h3]:mt-4 [&_h3]:mb-1 [&_h3]:text-sm [&_h3]:font-semibold", + "[&_h4]:mt-3 [&_h4]:mb-1 [&_h4]:text-sm [&_h4]:font-semibold", + // inline emphasis + "[&_strong]:font-semibold [&_em]:italic", + // inline code — `:not(pre)>code` targets bare inline code, because block code + // lives inside the code block's own `
`.
+  "[&_:not(pre)>code]:rounded-[var(--radius-xs)] [&_:not(pre)>code]:bg-[var(--accent)] [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.9em] [&_:not(pre)>code]:font-medium [&_:not(pre)>code]:text-[var(--foreground)]",
+  // horizontal rule
+  "[&_hr]:my-6 [&_hr]:border-0 [&_hr]:border-t [&_hr]:border-[var(--edge-medium)]",
+  // tables — a renderer emits bare ``, so the chat surface supplies the
+  // rules and padding rather than every renderer having to override the cells.
+  "[&_table]:my-4 [&_table]:w-full [&_table]:text-sm [&_table]:border-collapse",
+  "[&_thead_tr]:border-b [&_thead_tr]:border-[var(--edge)]",
+  "[&_tbody_tr]:border-b [&_tbody_tr]:border-[var(--edge)] [&_tbody_tr:last-child]:border-b-0",
+  "[&_th]:px-4 [&_th]:py-2 [&_th]:text-left [&_th]:font-medium [&_td]:px-4 [&_td]:py-2",
+  // margin reset for the container edges plus a width guard
+  "[&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_*]:max-w-full",
+].join(" ");
+
 /**
  * Present Markdown source using an injected rich renderer or the explicit
  * dependency-free plain-source contract.
@@ -145,7 +179,7 @@ export function Markdown({
   if (Renderer) {
     return (
       
*:first-child]:mt-0", + "[&>*:last-child]:mb-0", "[&>button]:w-full", "[&>span]:line-clamp-1", "[&>svg]:mt-0.5", "[&>svg]:transition-transform", + "[&_*]:max-w-full", "[&_.hljs]:bg-transparent", "[&_.hljs]:p-0", + "[&_:not(pre)>code]:bg-[var(--accent)]", + "[&_:not(pre)>code]:font-medium", + "[&_:not(pre)>code]:font-mono", + "[&_:not(pre)>code]:px-1", + "[&_:not(pre)>code]:py-0.5", + "[&_:not(pre)>code]:rounded-[var(--radius-xs)]", + "[&_:not(pre)>code]:text-[0.9em]", + "[&_:not(pre)>code]:text-[var(--foreground)]", + "[&_em]:italic", + "[&_h1]:font-semibold", + "[&_h1]:mb-2", + "[&_h1]:mt-6", + "[&_h1]:text-lg", + "[&_h2]:font-semibold", + "[&_h2]:mb-2", + "[&_h2]:mt-5", + "[&_h2]:text-base", + "[&_h3]:font-semibold", + "[&_h3]:mb-1", + "[&_h3]:mt-4", + "[&_h3]:text-sm", + "[&_h4]:font-semibold", + "[&_h4]:mb-1", + "[&_h4]:mt-3", + "[&_h4]:text-sm", + "[&_hr]:border-0", + "[&_hr]:border-[var(--edge-medium)]", + "[&_hr]:border-t", + "[&_hr]:my-6", + "[&_li>p]:my-0", + "[&_li]:my-1.5", + "[&_li]:pl-1", + "[&_li_p]:my-2", + "[&_ol]:list-decimal", + "[&_ol]:my-4", + "[&_ol]:pl-6", + "[&_ol_ol]:my-1", + "[&_ol_ul]:my-1", + "[&_p]:my-4", + "[&_strong]:font-semibold", "[&_svg]:!mr-0", "[&_svg]:group-hover:translate-x-0.5", "[&_svg]:ml-3.5", @@ -6160,6 +6207,26 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "[&_svg]:size-5", "[&_svg]:size-[1.125rem]", "[&_svg]:transition-transform", + "[&_table]:border-collapse", + "[&_table]:my-4", + "[&_table]:text-sm", + "[&_table]:w-full", + "[&_tbody_tr:last-child]:border-b-0", + "[&_tbody_tr]:border-[var(--edge)]", + "[&_tbody_tr]:border-b", + "[&_td]:px-4", + "[&_td]:py-2", + "[&_th]:font-medium", + "[&_th]:px-4", + "[&_th]:py-2", + "[&_th]:text-left", + "[&_thead_tr]:border-[var(--edge)]", + "[&_thead_tr]:border-b", + "[&_ul]:list-disc", + "[&_ul]:my-4", + "[&_ul]:pl-6", + "[&_ul_ol]:my-1", + "[&_ul_ul]:my-1", "[,", "[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--background),#0000_calc(50%+var(--spread)))]", "[...acc,", @@ -7591,6 +7658,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "blobKeys(journal.id);", "blobKeys(value.id).current;", "block", + "block's", "block.", "block:", "blocked,", @@ -8015,6 +8083,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "cause:", "cause;", "causing", + "cells.", "center", "centered", "centeredLeft", @@ -10205,6 +10274,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "emitter),", "emitting", "emoji,", + "emphasis", "empty", "empty,", "empty-state", @@ -11428,6 +11498,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "hauls", "havana", "have", + "having", "head>", "header", "header's", @@ -11461,6 +11532,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "headingId", "headingLevel", "headingLevel?:", + "headings", "heading}", "headless", "headless-hook", @@ -11745,6 +11817,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "including", "incoming,", "incoming:", + "indentation", "independent", "indeterminate", "indeterminate?:", @@ -12720,6 +12793,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "list).", "list);", "list,", + "list-decimal", + "list-disc", "list-request", "list.", "list.filter((t)", @@ -12762,6 +12837,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "listeners)", "listening.", "listing", + "lists", "lists,", "lists/tables),", "lists:", @@ -12923,6 +12999,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "mapping", "mapping,", "maps", + "margin", "margin:", "margins", "margins,", @@ -12945,6 +13022,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "markdownComponents?:", "marker", "marker.", + "markers", + "markers,", "marks", "markup", "markup).", @@ -13671,6 +13750,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "normally", "normally.", "not", + "not(pre)>code", "not).", "not,", "not-prose", @@ -14642,6 +14722,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "panelRef,", "panelRef.current;", "panels", + "paragraph", "parameters", "params", "params.set(key,", @@ -14927,6 +15008,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "pluggable", "pluggable;", "plugin", + "plugin,", + "plugin.", "plus", "png:", "point", @@ -15015,6 +15098,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "prefix", "prefix).", "prefixed", + "preflight", "presence:", "present", "present,", @@ -16217,6 +16301,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "rewriting", "rewritten.", "rgba(0,", + "rhythm", "rhythm).", "rich", "rich-Markdown", @@ -16351,6 +16436,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "rule", "rule.", "rule:", + "rules", "rules.", "run", "run();", @@ -17653,6 +17739,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "string]:", "strings", "strings/numbers", + "strips", "stroke:", "stroke=", "strokeLinecap:", @@ -17910,6 +17997,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "table", "table>", "tableOutput", + "tables", "tablist", "tabs", "tabs,",