Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion studio/frontend/src/components/assistant-ui/markdown-text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import { HugeiconsIcon } from "@hugeicons/react";
import { createMathPlugin } from "@streamdown/math";
import { mermaid } from "@streamdown/mermaid";
import { useEffect, useMemo, useRef, useState } from "react";
import { Block, type BlockProps, Streamdown } from "streamdown";
import {
Block,
type BlockProps,
Streamdown,
type StreamdownProps,
} from "streamdown";
import { createCodePlugin } from "./code-plugin";
import "katex/dist/katex.min.css";
import { AudioPlayer } from "./audio-player";
Expand All @@ -36,6 +41,15 @@ const code = createCodePlugin({
});
const { withSmoothContextProvider } = INTERNAL;

// Streamdown 2.5 schedules ordinary streaming blocks in an interruptible React
// transition. A continuous token stream can starve that transition for seconds.
// Its animated path commits every block update directly; zero duration preserves
// that scheduling behavior without adding a visible text animation.
const STREAMDOWN_IMMEDIATE_UPDATES = {
duration: 0,
stagger: 0,
} satisfies NonNullable<StreamdownProps["animated"]>;

const STREAMDOWN_COMPONENTS = {
a: ({ href, children, ...props }: React.ComponentProps<"a">) => (
<a
Expand Down Expand Up @@ -395,6 +409,7 @@ const MarkdownTextImpl = () => {
key={messageId}
mode="streaming"
isAnimating={status.type === "running"}
animated={STREAMDOWN_IMMEDIATE_UPDATES}
plugins={{ code, math, mermaid }}
components={STREAMDOWN_COMPONENTS}
urlTransform={safeMarkdownUrl}
Expand Down
102 changes: 102 additions & 0 deletions studio/frontend/tests/markdown-streaming-scheduling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";

import ts from "typescript";

const MARKDOWN_TEXT_PATH = new URL(
"../src/components/assistant-ui/markdown-text.tsx",
import.meta.url,
);
const source = ts.createSourceFile(
MARKDOWN_TEXT_PATH.pathname,
readFileSync(MARKDOWN_TEXT_PATH, "utf8"),
ts.ScriptTarget.ESNext,
true,
ts.ScriptKind.TSX,
);

function findImmediateUpdateConfig(): ts.ObjectLiteralExpression | null {
let config: ts.ObjectLiteralExpression | null = null;
const visit = (node: ts.Node): void => {
if (
ts.isVariableDeclaration(node) &&
node.name.getText(source) === "STREAMDOWN_IMMEDIATE_UPDATES" &&
node.initializer &&
ts.isSatisfiesExpression(node.initializer) &&
ts.isObjectLiteralExpression(node.initializer.expression)
) {
config = node.initializer.expression;
}
node.forEachChild(visit);
};
source.forEachChild(visit);
return config;
}

function findChatStreamdown(): ts.JsxOpeningLikeElement | null {
let streamdown: ts.JsxOpeningLikeElement | null = null;
const visit = (node: ts.Node): void => {
if (
(ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) &&
node.tagName.getText(source) === "Streamdown"
) {
streamdown = node;
}
node.forEachChild(visit);
};
source.forEachChild(visit);
return streamdown;
}

function jsxAttribute(
opening: ts.JsxOpeningLikeElement,
name: string,
): ts.JsxAttribute | null {
for (const attribute of opening.attributes.properties) {
if (
ts.isJsxAttribute(attribute) &&
attribute.name.getText(source) === name
) {
return attribute;
}
}
return null;
}

test("chat streaming bypasses Streamdown's starvable transition without visible animation", () => {
const config = findImmediateUpdateConfig();
assert.ok(config, "STREAMDOWN_IMMEDIATE_UPDATES is missing");

const values = new Map(
config.properties.flatMap((property) => {
if (
!(
ts.isPropertyAssignment(property) &&
ts.isNumericLiteral(property.initializer)
)
) {
return [];
}
return [
[property.name.getText(source), Number(property.initializer.text)],
];
}),
);
assert.equal(values.get("duration"), 0);
assert.equal(values.get("stagger"), 0);

const streamdown = findChatStreamdown();
assert.ok(streamdown, "chat <Streamdown> is missing");
assert.equal(
jsxAttribute(streamdown, "mode")?.initializer?.getText(source),
'"streaming"',
);
assert.equal(
jsxAttribute(streamdown, "animated")?.initializer?.getText(source),
"{STREAMDOWN_IMMEDIATE_UPDATES}",
);
});
Loading