diff --git a/app/globals.css b/app/globals.css index 7cbee69..04df8b0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,6 +1,6 @@ @import "tailwindcss"; @import "tw-animate-css"; -@source "../node_modules/streamdown/dist/*.js"; +/* @source "../node_modules/streamdown/dist/*.js"; */ @custom-variant dark (&:is(.dark *)); @@ -277,17 +277,17 @@ } [data-streamdown="code-block"] { - @apply my-3 overflow-hidden rounded-lg border border-primary-border bg-primary-bg [corner-shape:squircle]; + @apply my-3 p-0 gap-0 flex flex-col rounded-lg border-0 ring ring-primary-border shadow-xs; } [data-streamdown="code-block-header"] { - @apply flex items-center justify-between border-b border-primary-border bg-primary-bg p-3 text-xs text-ink-primary; + @apply flex items-center justify-between [&>span]:font-sans h-auto px-3 py-4 -my-2 bg-tertiary-bg border-0 text-xs text-ink-primary; } [data-streamdown="code-block-body"] { - @apply overflow-x-auto p-3 bg-primary-bg text-sm leading-relaxed; + @apply overflow-x-auto border-0 p-3 bg-primary-bg ring ring-tertiary-border text-sm leading-relaxed; } [data-streamdown="inline-code"] { - @apply rounded-sm bg-primary-bg px-1 py-0.5 text-sm text-ink-primary; + @apply rounded-sm bg-primary-bg border border-primary-border px-1 py-0.5 text-sm text-ink-secondary; } [data-streamdown="link"] { @@ -327,39 +327,29 @@ } [data-streamdown="table-wrapper"] { - @apply my-3 overflow-x-auto; + @apply my-3 border-0 p-0 gap-0 flex flex-col ring ring-secondary-border overflow-x-auto [&_div]:border-0; } [data-streamdown="table"] { - @apply w-full text-sm; + @apply w-full text-sm border-0 p-0 gap-0 bg-tertiary-bg; } [data-streamdown="table-header"] { - @apply border-b border-primary-border bg-primary-bg; + @apply [&_tr]:border-0 [&_tr]:bg-tertiary-bg [&_th]:px-4 [&_th]:py-2 [&>tr]:-my-2; +} + +[data-streamdown="table-body"] { + @apply rounded-xl overflow-hidden; + clip-path: inset(0 round 12px); } [data-streamdown="table-header-cell"] { - @apply px-3 py-1.5 text-left font-medium text-ink-primary; + @apply px-3 py-1.5 text-left font-medium text-ink-primary overflow-hidden; } [data-streamdown="table-row"] { - @apply border-b border-primary-border; + @apply border-b border-primary-border overflow-hidden; } [data-streamdown="table-cell"] { - @apply px-3 py-1.5 text-ink-primary; + @apply px-3 py-1.5 text-ink-primary bg-primary-bg overflow-hidden; } [data-streamdown="image"] { @apply max-w-full rounded-md; } - -/* ─── Shiki dual-theme code blocks ─── - CodeBlock highlights with { light: github-light, dark: github-dark } and - defaultColor:false, so shiki emits per-token CSS variables instead of a fixed - color. Activate light by default and swap to dark under the .dark class. Only - the text color switches — the
keeps its own bg-secondary surface. */
-.shiki,
-.shiki span {
- color: var(--shiki-light);
-}
-
-.dark .shiki,
-.dark .shiki span {
- color: var(--shiki-dark);
-}
diff --git a/app/layout.tsx b/app/layout.tsx
index 941fcc1..aace71f 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,7 +1,7 @@
import type { Metadata } from "next";
import { IBM_Plex_Mono, Inter } from "next/font/google";
import { Providers } from "@/components/providers";
-import "streamdown/styles.css";
+// import "streamdown/styles.css";
import "./globals.css";
const inter = Inter({
variable: "--font-inter",
diff --git a/bun.lock b/bun.lock
index 1fa79c8..c9fef0a 100644
--- a/bun.lock
+++ b/bun.lock
@@ -10,6 +10,7 @@
"@ai-sdk/react": "^3.0.220",
"@base-ui/react": "^1.6.0",
"@intentface/chat": "workspace:*",
+ "@streamdown/code": "^1.1.1",
"@tiptap/core": "^3.27.1",
"@tiptap/extension-document": "^3.27.1",
"@tiptap/extension-paragraph": "^3.27.1",
@@ -449,24 +450,26 @@
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.93.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6QN3DEaEw3eWioWEFRgNsTvYq8czYSnpkjB2za+/WdLN0g5FzOl2ZEfNiPrBWIPnSmjUmDWtWVWcSjwY7fX5/Q=="],
- "@shikijs/core": ["@shikijs/core@4.3.0", "", { "dependencies": { "@shikijs/primitive": "4.3.0", "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ=="],
+ "@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
- "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.0", "", { "dependencies": { "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-hTv/KiFf2tpiqlACPiztGGurEARWIutB8YUhcrA1pUC7VzzwKO+g5crUocrLztrZ5ro5Z4hbXg7bYclETn3gSQ=="],
+ "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
- "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.0", "", { "dependencies": { "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1vMdN3gHfnKfLYwecUI2ITJI4RhHt96xEaJumVn7Heb0IlJ8WQMIH0Voak+2j22BpSNKdnOfB/pCTPnPm2gq7A=="],
+ "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
- "@shikijs/langs": ["@shikijs/langs@4.3.0", "", { "dependencies": { "@shikijs/types": "4.3.0" } }, "sha512-rnlqFbBRSys9bT4gl/5rw9RnS0W/I84ZldXPkO7cvlEMoV85TyF/aU01N7/NbSR776RNLjrJKjfFUXJR6wN1Cg=="],
+ "@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
"@shikijs/primitive": ["@shikijs/primitive@4.3.0", "", { "dependencies": { "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CPkz64PTa5diRW1ggzMZH9VM/du4RNChYgVtgqrFcgruvIybmCvySv8GkiHSczUHXYuuR8TdKEwFx+UnZMpgdg=="],
- "@shikijs/themes": ["@shikijs/themes@4.3.0", "", { "dependencies": { "@shikijs/types": "4.3.0" } }, "sha512-Avgt05YiT+Y3prjIc9lmQxhJzHBcCfR6cjiFW4OyaMBbt2A6trX5rfjUzx+Vj/mE9qpArYjatnqo9XPjQNW/AQ=="],
+ "@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
- "@shikijs/types": ["@shikijs/types@4.3.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oc8b9U2SYvofKZk8e/737nIX0qwf6eV2vHFATeObAu7r+mUVpLs8Re0BmVkIjAWAYgkmG/CzLNo7rzuBzRu/wQ=="],
+ "@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+ "@streamdown/code": ["@streamdown/code@1.1.1", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-i7HTNuDgZWb+VdrNVOam9gQhIc5MSSDXKWXgbUrn/4vSRaSMM+Rtl10MQj4wLWPNpF7p80waJsAqFP8HZfb0Jg=="],
+
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.3.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.2" } }, "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg=="],
@@ -1277,7 +1280,7 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
- "shiki": ["shiki@4.3.0", "", { "dependencies": { "@shikijs/core": "4.3.0", "@shikijs/engine-javascript": "4.3.0", "@shikijs/engine-oniguruma": "4.3.0", "@shikijs/langs": "4.3.0", "@shikijs/themes": "4.3.0", "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-NKKjWzR6LIGL3sXBrWDw9sDS9cxx42/DkysaNqJEeOWE8Kix5gpak0bc00OfDVEO4oyXSyz8+aRaqKoBD1yo7A=="],
+ "shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
@@ -1413,6 +1416,8 @@
"@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
+ "@shikijs/primitive/@shikijs/types": ["@shikijs/types@4.3.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oc8b9U2SYvofKZk8e/737nIX0qwf6eV2vHFATeObAu7r+mUVpLs8Re0BmVkIjAWAYgkmG/CzLNo7rzuBzRu/wQ=="],
+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
@@ -1435,6 +1440,8 @@
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
+ "fumadocs-core/shiki": ["shiki@4.3.0", "", { "dependencies": { "@shikijs/core": "4.3.0", "@shikijs/engine-javascript": "4.3.0", "@shikijs/engine-oniguruma": "4.3.0", "@shikijs/langs": "4.3.0", "@shikijs/themes": "4.3.0", "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-NKKjWzR6LIGL3sXBrWDw9sDS9cxx42/DkysaNqJEeOWE8Kix5gpak0bc00OfDVEO4oyXSyz8+aRaqKoBD1yo7A=="],
+
"fumadocs-mdx/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
"mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
@@ -1455,6 +1462,18 @@
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
+ "fumadocs-core/shiki/@shikijs/core": ["@shikijs/core@4.3.0", "", { "dependencies": { "@shikijs/primitive": "4.3.0", "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ=="],
+
+ "fumadocs-core/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.0", "", { "dependencies": { "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-hTv/KiFf2tpiqlACPiztGGurEARWIutB8YUhcrA1pUC7VzzwKO+g5crUocrLztrZ5ro5Z4hbXg7bYclETn3gSQ=="],
+
+ "fumadocs-core/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.0", "", { "dependencies": { "@shikijs/types": "4.3.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1vMdN3gHfnKfLYwecUI2ITJI4RhHt96xEaJumVn7Heb0IlJ8WQMIH0Voak+2j22BpSNKdnOfB/pCTPnPm2gq7A=="],
+
+ "fumadocs-core/shiki/@shikijs/langs": ["@shikijs/langs@4.3.0", "", { "dependencies": { "@shikijs/types": "4.3.0" } }, "sha512-rnlqFbBRSys9bT4gl/5rw9RnS0W/I84ZldXPkO7cvlEMoV85TyF/aU01N7/NbSR776RNLjrJKjfFUXJR6wN1Cg=="],
+
+ "fumadocs-core/shiki/@shikijs/themes": ["@shikijs/themes@4.3.0", "", { "dependencies": { "@shikijs/types": "4.3.0" } }, "sha512-Avgt05YiT+Y3prjIc9lmQxhJzHBcCfR6cjiFW4OyaMBbt2A6trX5rfjUzx+Vj/mE9qpArYjatnqo9XPjQNW/AQ=="],
+
+ "fumadocs-core/shiki/@shikijs/types": ["@shikijs/types@4.3.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oc8b9U2SYvofKZk8e/737nIX0qwf6eV2vHFATeObAu7r+mUVpLs8Re0BmVkIjAWAYgkmG/CzLNo7rzuBzRu/wQ=="],
+
"next/postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
"read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
diff --git a/components/ai/composer.tsx b/components/ai/composer.tsx
index ea130db..556ae48 100644
--- a/components/ai/composer.tsx
+++ b/components/ai/composer.tsx
@@ -84,7 +84,7 @@ const ComposerContainer = ({ className, ...props }: ComposerContainerProps) => (
// Positioned so it paints above the context window peeking out from
// behind its top edge. Edge is shadow-drawn (shadow-border), not a
// border, matching the playground cards.
- "relative bg-primary-bg rounded-4xl border border-primary-border shadow-xs [corner-shape:squircle] cursor-text transition-colors",
+ "relative bg-primary-bg rounded-2xl border border-primary-border shadow-xs cursor-text transition-colors",
className,
)}
{...props}
@@ -188,6 +188,7 @@ type ComposerTextareaProps = {
const ComposerTextarea = ({ className, disabled = false, ...props }: ComposerTextareaProps) => (
(
// coordinates); portaled to the body, so it never reserves layout in the composer.
// Match the composer Container's width via the positioner's --anchor-width var.
"absolute z-50 w-(--anchor-width) overflow-hidden",
- "rounded-4xl border border-primary-border bg-primary-bg [corner-shape:squircle]",
+ "rounded-2xl border border-primary-border bg-primary-bg",
"transition-[opacity,transform] duration-150 ease-out",
"data-starting-style:opacity-0 data-ending-style:opacity-0",
// Slide from the anchored edge: default (above) drops in from below; flipped
@@ -396,7 +398,7 @@ const ComposerPopover = ({ className, ...props }: ComposerPopoverProps) => {
// off the anchor token with a stronger shadow. `absolute` is the base positioning
// context (the positioner then writes left/top in page coordinates); z-50 keeps
// the portaled popover above the thread.
- "absolute z-50 w-72 overflow-hidden border border-primary-border bg-primary-bg rounded-4xl shadow-lg [corner-shape:squircle]",
+ "absolute z-50 w-72 overflow-hidden border border-primary-border bg-primary-bg rounded-2xl shadow-lg",
"transition-[opacity,transform,filter] duration-150 ease-out",
"data-closed:opacity-0 data-closed:blur-[3px]",
// Slide from the anchored edge — opens upward by default, downward when flipped.
diff --git a/components/docs/intentface.css b/components/docs/intentface.css
index 3808cfd..8151b74 100644
--- a/components/docs/intentface.css
+++ b/components/docs/intentface.css
@@ -1,5 +1,3 @@
-@source "../node_modules/streamdown/dist/*.js";
-
@custom-variant dark (&:is(.dark *));
:root {
diff --git a/components/ui/markdown.tsx b/components/ui/markdown.tsx
index 31cf575..0d84a5c 100644
--- a/components/ui/markdown.tsx
+++ b/components/ui/markdown.tsx
@@ -1,3 +1,4 @@
+import { code } from "@streamdown/code";
import type { ComponentProps } from "react";
import type { Components } from "streamdown";
import { Streamdown } from "streamdown";
@@ -63,12 +64,14 @@ const Markdown = ({
...props
}: ComponentProps) => (
*:first-child]:mt-0 [&>*:last-child]:mb-0",
+ "px-px text-md [&_p]:whitespace-pre-wrap [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
className,
)}
+ shikiTheme={["github-light", "vesper"]}
{...props}
/>
);
diff --git a/package.json b/package.json
index beb3d7c..cf0886d 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@ai-sdk/react": "^3.0.220",
"@base-ui/react": "^1.6.0",
"@intentface/chat": "workspace:*",
+ "@streamdown/code": "^1.1.1",
"@tiptap/core": "^3.27.1",
"@tiptap/extension-document": "^3.27.1",
"@tiptap/extension-paragraph": "^3.27.1",
diff --git a/packages/chat/src/chip-markdown.ts b/packages/chat/src/chip-markdown.ts
index b5e3d8a..0852e83 100644
--- a/packages/chat/src/chip-markdown.ts
+++ b/packages/chat/src/chip-markdown.ts
@@ -51,6 +51,13 @@ export const encodeChipMarkdown = (chip: ChipData): string => {
};
export const parseChipSegments = (text: string): ChipSegment[] => {
+ // Fast path — every chip token contains this literal, and most message text
+ // carries no chips; an includes() probe is far cheaper than a regex scan of
+ // the full text (which the streaming row would otherwise repeat per token).
+ if (!text.includes("](chip:")) {
+ return text.length > 0 ? [{ type: "text", text }] : [];
+ }
+
const segments: ChipSegment[] = [];
let lastIndex = 0;
for (const match of text.matchAll(CHIP_REF_PATTERN)) {
diff --git a/packages/chat/src/message.tsx b/packages/chat/src/message.tsx
index ff626bc..c2a0d84 100644
--- a/packages/chat/src/message.tsx
+++ b/packages/chat/src/message.tsx
@@ -99,20 +99,27 @@ const MessageText = ({
renderChip,
...elementProps
}: MessageTextProps) => {
- const segments = parseChipSegments(children);
-
- const segmentNodes = segments.map((segment, index) =>
- segment.type === "text" ? (
- {renderText ? renderText(segment.text, index) : segment.text}
- ) : (
-
- {renderChip ? (
- renderChip(segment, index)
+ // Settled rows re-render for reasons other than their text (data-last flips,
+ // parent context) — memoizing on the text keeps them from re-parsing and
+ // rebuilding the segment tree each time.
+ const segmentNodes = useMemo(
+ () =>
+ parseChipSegments(children).map((segment, index) =>
+ segment.type === "text" ? (
+
+ {renderText ? renderText(segment.text, index) : segment.text}
+
) : (
- {segment.label}
- )}
-
- ),
+
+ {renderChip ? (
+ renderChip(segment, index)
+ ) : (
+ {segment.label}
+ )}
+
+ ),
+ ),
+ [children, renderText, renderChip],
);
return useRenderElement(