From b7aea8b0ef9defba7f9cc817f67420977c2d3203 Mon Sep 17 00:00:00 2001
From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Date: Wed, 9 Sep 2026 11:42:46 +0800
Subject: [PATCH] fix(session-ui): fade in streamed inline code (#44899)
---
.../src/components/markdown-solid.test.tsx | 26 +++++++++++++++
.../src/components/markdown-solid.tsx | 32 +++++++++++++++---
.../session-ui/src/components/markdown.css | 4 +--
.../src/components/markdown.stories.tsx | 33 +++++++++++++++++++
4 files changed, 88 insertions(+), 7 deletions(-)
diff --git a/packages/session-ui/src/components/markdown-solid.test.tsx b/packages/session-ui/src/components/markdown-solid.test.tsx
index 2fd7b95c5a7..9e76f91a3d5 100644
--- a/packages/session-ui/src/components/markdown-solid.test.tsx
+++ b/packages/session-ui/src/components/markdown-solid.test.tsx
@@ -41,6 +41,32 @@ test("keeps completed block text compact", () => {
])
})
+test("marks streamed inline code for animation", () => {
+ expect(parseMarkdownNodes("
file.ts:29-43
", true, true)).toEqual([
+ {
+ key: "0",
+ type: "element",
+ tag: "p",
+ attributes: {},
+ children: [
+ {
+ key: "0.0",
+ type: "element",
+ tag: "code",
+ attributes: {},
+ children: Array.from("file.ts:29-43", (text, index) => ({
+ key: `0.0.0:${index}`,
+ type: "word",
+ text,
+ animate: true,
+ })),
+ animate: true,
+ },
+ ],
+ },
+ ])
+})
+
test("marks words for animation only when requested", () => {
expect(
parseMarkdownNodes("Hello
", true).flatMap((node) => (node.type === "element" ? node.children : [node]))[0],
diff --git a/packages/session-ui/src/components/markdown-solid.tsx b/packages/session-ui/src/components/markdown-solid.tsx
index e302c0123b7..ed077684625 100644
--- a/packages/session-ui/src/components/markdown-solid.tsx
+++ b/packages/session-ui/src/components/markdown-solid.tsx
@@ -3,7 +3,14 @@ import { createStore, reconcile } from "solid-js/store"
import { Dynamic, render } from "solid-js/web"
type MarkdownNode =
- | { key: string; type: "element"; tag: string; attributes: Record; children: MarkdownNode[] }
+ | {
+ key: string
+ type: "element"
+ tag: string
+ attributes: Record
+ children: MarkdownNode[]
+ animate?: true
+ }
| { key: string; type: "text"; text: string }
| { key: string; type: "word"; text: string; animate?: true }
@@ -38,8 +45,12 @@ function MarkdownDomNode(props: { node: MarkdownNode; animate: () => boolean })
)
}
+ let ref: HTMLElement | undefined
+ onMount(() => {
+ if (props.animate() && node.animate) ref?.setAttribute("data-markdown-enter", "")
+ })
return (
-
+
{(node) => }
)
@@ -51,10 +62,10 @@ export function parseMarkdownNodes(html: string, words: boolean, animate = false
return Array.from(template.content.childNodes).flatMap((node, index) => parseNode(node, `${index}`, words, animate))
}
-function parseNode(node: Node, key: string, words: boolean, animate: boolean): MarkdownNode[] {
+function parseNode(node: Node, key: string, words: boolean, animate: boolean, inlineCode = false): MarkdownNode[] {
if (node instanceof Text) {
if (!words) return [{ key, type: "text", text: node.data }]
- return node.data.split(/(\s+)/).flatMap((text, index): MarkdownNode[] => {
+ return (inlineCode ? Array.from(node.data) : node.data.split(/(\s+)/)).flatMap((text, index): MarkdownNode[] => {
if (!text) return []
if (/^\s+$/.test(text)) return [{ key: `${key}:${index}`, type: "text", text }]
return [{ key: `${key}:${index}`, type: "word", text, ...(animate ? { animate: true as const } : {}) }]
@@ -67,7 +78,18 @@ function parseNode(node: Node, key: string, words: boolean, animate: boolean): M
type: "element",
tag: node.tagName.toLowerCase(),
attributes: Object.fromEntries(Array.from(node.attributes).map((attribute) => [attribute.name, attribute.value])),
- children: Array.from(node.childNodes).flatMap((child, index) => parseNode(child, `${key}.${index}`, words, animate)),
+ children: Array.from(node.childNodes).flatMap((child, index) =>
+ parseNode(
+ child,
+ `${key}.${index}`,
+ words,
+ animate,
+ inlineCode || (node.tagName === "CODE" && node.parentElement?.tagName !== "PRE"),
+ ),
+ ),
+ ...(words && animate && node.tagName === "CODE" && node.parentElement?.tagName !== "PRE"
+ ? { animate: true as const }
+ : {}),
},
]
}
diff --git a/packages/session-ui/src/components/markdown.css b/packages/session-ui/src/components/markdown.css
index 17a6b530e97..7b613ec3403 100644
--- a/packages/session-ui/src/components/markdown.css
+++ b/packages/session-ui/src/components/markdown.css
@@ -20,7 +20,7 @@
display: inline;
}
- [data-markdown-word][data-markdown-enter] {
+ :is([data-markdown-word], code)[data-markdown-enter] {
animation: markdown-word-enter 180ms ease-out both;
}
@@ -347,7 +347,7 @@
}
@media (prefers-reduced-motion: reduce) {
- [data-component="markdown"] [data-markdown-word][data-markdown-enter] {
+ [data-component="markdown"] :is([data-markdown-word], code)[data-markdown-enter] {
animation: none;
}
}
diff --git a/packages/session-ui/src/components/markdown.stories.tsx b/packages/session-ui/src/components/markdown.stories.tsx
index b1a175022d9..9ec7810765e 100644
--- a/packages/session-ui/src/components/markdown.stories.tsx
+++ b/packages/session-ui/src/components/markdown.stories.tsx
@@ -52,3 +52,36 @@ export const StreamingResponse = {
),
}
+
+function StreamingInlineCodeMarkdown() {
+ const chunks = [
+ "Updated ",
+ "`apps/cloud",
+ "flare/src/",
+ "editor/Cloud",
+ "Auth.ts:29",
+ "-43` and ",
+ "`packages/",
+ "session-ui/src/",
+ "components/markdown",
+ "-solid.tsx`, ",
+ "then verified ",
+ "the changes with ",
+ "`bun type",
+ "check` and ",
+ "`bun te",
+ "st`.",
+ ]
+ const [count, setCount] = createSignal(1)
+ const timer = setInterval(() => setCount((value) => (value >= chunks.length ? 1 : value + 1)), 220)
+ onCleanup(() => clearInterval(timer))
+ return
+}
+
+export const StreamingInlineCode = {
+ render: () => (
+
+
+
+ ),
+}