mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
Merge pull request #7940 from SivanCola/fix/thinking-trace-markdown
Render thinking traces as Markdown without stream lag / 流畅渲染思考过程 Markdown
This commit is contained in:
12 files changed
+164
-37
No files matched your search
@@ -257,6 +257,12 @@ console.log("\nmermaid rendering");
|
||||
});
|
||||
eq(pendingFrame, undefined, "tail growth alone schedules no further parse");
|
||||
|
||||
await act(async () => {
|
||||
root.render(<MarkdownTextProbe text={"...\nreplacement window"} streaming />);
|
||||
await flushTimers();
|
||||
});
|
||||
eq(rootEl.textContent, "", "a rolling Markdown window drops its stale parsed prefix before paint");
|
||||
|
||||
await act(async () => {
|
||||
root.render(<MarkdownTextProbe text="complete" streaming={false} />);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
// Run: tsx src/__tests__/message-reasoning-panel.test.tsx
|
||||
|
||||
import { JSDOM } from "jsdom";
|
||||
import { registerHooks } from "node:module";
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { LocaleProvider } from "../lib/i18n";
|
||||
import { AssistantMessage } from "../components/Message";
|
||||
|
||||
registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
if (specifier.endsWith(".css")) {
|
||||
return nextResolve("./asset-stub-for-tests.ts", { ...context, parentURL: import.meta.url });
|
||||
}
|
||||
return nextResolve(specifier, context);
|
||||
},
|
||||
});
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
@@ -30,6 +40,7 @@ globalThis.window = dom.window as unknown as Window & typeof globalThis;
|
||||
globalThis.document = dom.window.document;
|
||||
Object.defineProperty(globalThis, "navigator", { configurable: true, value: { ...dom.window.navigator, language: "en-US" } });
|
||||
globalThis.Node = dom.window.Node;
|
||||
globalThis.Element = dom.window.Element;
|
||||
globalThis.HTMLElement = dom.window.HTMLElement;
|
||||
globalThis.Event = dom.window.Event;
|
||||
globalThis.MouseEvent = dom.window.MouseEvent;
|
||||
@@ -49,7 +60,7 @@ await act(async () => {
|
||||
kind: "assistant",
|
||||
id: "a1",
|
||||
text: "",
|
||||
reasoning: "line one\nline two",
|
||||
reasoning: "**important trace**\n\n- line one\n- line two\n\n`inline code`",
|
||||
streaming: false,
|
||||
reasoningComplete: true,
|
||||
reasoningDurationMs: 2_600,
|
||||
@@ -67,9 +78,13 @@ ok(!document.querySelector(".reasoning__body"), "completed reasoning is collapse
|
||||
|
||||
await act(async () => {
|
||||
header?.dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true }));
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
ok(document.querySelector(".reasoning__body")?.textContent?.includes("line two") ?? false, "clicking the header expands the reasoning body");
|
||||
ok(document.querySelector(".reasoning__body strong")?.textContent === "important trace", "reasoning renders Markdown emphasis");
|
||||
ok(document.querySelectorAll(".reasoning__body li").length === 2, "reasoning renders Markdown lists");
|
||||
ok(document.querySelector(".reasoning__body .md-code")?.textContent === "inline code", "reasoning renders Markdown inline code");
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
|
||||
@@ -41,5 +41,17 @@ eq(
|
||||
"can opt out of streaming truncation",
|
||||
);
|
||||
|
||||
eq(
|
||||
displayReasoningText("abcdefgh", { streaming: true, maxChars: 4, maxLines: 10, stableWindowChars: 3 }),
|
||||
"...\ndefgh",
|
||||
"keeps a stable append-only character window between coarse rebases",
|
||||
);
|
||||
|
||||
eq(
|
||||
displayReasoningText("a\nb\nc\nd\ne", { streaming: true, maxChars: 100, maxLines: 2, stableWindowLines: 2 }),
|
||||
"...\nc\nd\ne",
|
||||
"keeps a stable append-only line window between coarse rebases",
|
||||
);
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
if (failed > 0) process.exit(1);
|
||||
@@ -5,6 +5,7 @@
|
||||
// response preview / notices), including the terminal phase visuals.
|
||||
|
||||
import { JSDOM } from "jsdom";
|
||||
import { registerHooks } from "node:module";
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
@@ -13,6 +14,15 @@ import { ToolCard } from "../components/ToolCard";
|
||||
import { LocaleProvider } from "../lib/i18n";
|
||||
import type { Item, SubagentProgress } from "../lib/useController";
|
||||
|
||||
registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
if (specifier.endsWith(".css")) {
|
||||
return nextResolve("./asset-stub-for-tests.ts", { ...context, parentURL: import.meta.url });
|
||||
}
|
||||
return nextResolve(specifier, context);
|
||||
},
|
||||
});
|
||||
|
||||
type ToolItem = Extract<Item, { kind: "tool" }>;
|
||||
|
||||
// jsdom has no layout engine: stub the GSAP tween surface the collapse hook
|
||||
@@ -94,7 +104,7 @@ function makeItem(phase: SubagentProgress["phase"], over: Partial<SubagentProgre
|
||||
status: phase === "completed" || phase === "failed" ? "done" : phase === "cancelled" ? "stopped" : "running",
|
||||
subagentProgress: {
|
||||
phase,
|
||||
reasoning: "thinking step by step",
|
||||
reasoning: "**thinking** step by step\n\n- inspect\n- verify",
|
||||
text: "draft answer preview",
|
||||
notice: "heads up",
|
||||
lastActivityAt: now - 3_000,
|
||||
@@ -131,6 +141,7 @@ console.log("\nsubagent progress card");
|
||||
// Expanded body shows reasoning / response / notices without ordinary output.
|
||||
const head = document.querySelector(".tool__head") as HTMLButtonElement | null;
|
||||
ok(!!head, "card head renders");
|
||||
ok(!document.querySelector(".tool__subagent-preview-text .md"), "collapsed reasoning preview skips Markdown rendering");
|
||||
await act(async () => {
|
||||
head?.click();
|
||||
await flushTimers();
|
||||
@@ -138,6 +149,8 @@ console.log("\nsubagent progress card");
|
||||
ok(!!document.querySelector(".tool__subagent-preview"), "expanded body renders the preview block");
|
||||
ok(document.querySelector(".tool__subagent-preview-label")?.textContent === "Reasoning", "reasoning section label");
|
||||
ok(document.body.textContent?.includes("thinking step by step"), "reasoning preview text visible");
|
||||
ok(document.querySelector(".tool__subagent-preview-text strong")?.textContent === "thinking", "reasoning preview renders Markdown emphasis");
|
||||
ok(document.querySelectorAll(".tool__subagent-preview-text li").length === 2, "reasoning preview renders Markdown lists");
|
||||
ok(document.body.textContent?.includes("draft answer preview"), "response preview text visible");
|
||||
ok(document.body.textContent?.includes("heads up"), "notice preview text visible");
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ const FRAME_MS = 1000 / 60;
|
||||
interface SimResult {
|
||||
frames: number;
|
||||
commits: number;
|
||||
markdownParses: number;
|
||||
answerMarkdownParses: number;
|
||||
reasoningMarkdownParses: number;
|
||||
itemsIdentityBreaks: number;
|
||||
bumpSkipViolations: number;
|
||||
state: typeof initialState;
|
||||
@@ -49,15 +50,20 @@ function simulate(spec: UIPerfScenario, base?: typeof initialState): SimResult {
|
||||
|
||||
let frames = 0;
|
||||
let commits = 0;
|
||||
let markdownParses = 0;
|
||||
let answerMarkdownParses = 0;
|
||||
let reasoningMarkdownParses = 0;
|
||||
let itemsIdentityBreaks = 0;
|
||||
let bumpSkipViolations = 0;
|
||||
let text = "";
|
||||
let renderedLen = 0;
|
||||
let lastParseAt = -Infinity;
|
||||
let reasoning = "";
|
||||
let answerRenderedLen = 0;
|
||||
let reasoningRenderedLen = 0;
|
||||
let lastAnswerParseAt = -Infinity;
|
||||
let lastReasoningParseAt = -Infinity;
|
||||
let pendingChunks = 0;
|
||||
let index = 0;
|
||||
let firstBatch = true;
|
||||
let reasoningFinalized = false;
|
||||
|
||||
while (index < chunks.length) {
|
||||
frames += 1;
|
||||
@@ -68,6 +74,7 @@ function simulate(spec: UIPerfScenario, base?: typeof initialState): SimResult {
|
||||
index += 1;
|
||||
pendingChunks -= 1;
|
||||
if (chunk.kind === "text") text += chunk.delta;
|
||||
else reasoning += chunk.delta;
|
||||
batch.push({ tabId: "a", e: { kind: chunk.kind, text: chunk.delta } as WireEvent });
|
||||
}
|
||||
if (batch.length === 0) continue;
|
||||
@@ -85,17 +92,31 @@ function simulate(spec: UIPerfScenario, base?: typeof initialState): SimResult {
|
||||
firstBatch = false;
|
||||
}
|
||||
const nowMs = frames * FRAME_MS;
|
||||
if (nowMs - lastParseAt >= streamingMarkdownCommitInterval(text.length)) {
|
||||
if (spec.reasoningVisible && reasoning.length > 0 && text.length > 0 && !reasoningFinalized) {
|
||||
reasoningMarkdownParses += 1;
|
||||
reasoningRenderedLen = reasoning.length;
|
||||
reasoningFinalized = true;
|
||||
}
|
||||
if (spec.reasoningVisible && !reasoningFinalized && nowMs - lastReasoningParseAt >= streamingMarkdownCommitInterval(reasoning.length)) {
|
||||
const target = streamingCommitTarget(reasoning);
|
||||
if (target.length > reasoningRenderedLen) {
|
||||
reasoningMarkdownParses += 1;
|
||||
reasoningRenderedLen = target.length;
|
||||
lastReasoningParseAt = nowMs;
|
||||
}
|
||||
}
|
||||
if (nowMs - lastAnswerParseAt >= streamingMarkdownCommitInterval(text.length)) {
|
||||
const target = streamingCommitTarget(text);
|
||||
if (target.length > renderedLen) {
|
||||
markdownParses += 1;
|
||||
renderedLen = target.length;
|
||||
lastParseAt = nowMs;
|
||||
if (target.length > answerRenderedLen) {
|
||||
answerMarkdownParses += 1;
|
||||
answerRenderedLen = target.length;
|
||||
lastAnswerParseAt = nowMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
markdownParses += 1; // end-of-stream finalization parse
|
||||
return { frames, commits, markdownParses, itemsIdentityBreaks, bumpSkipViolations, state };
|
||||
if (text.length > answerRenderedLen) answerMarkdownParses += 1;
|
||||
if (spec.reasoningVisible && reasoning.length > reasoningRenderedLen) reasoningMarkdownParses += 1;
|
||||
return { frames, commits, answerMarkdownParses, reasoningMarkdownParses, itemsIdentityBreaks, bumpSkipViolations, state };
|
||||
}
|
||||
|
||||
const byId = new Map(UI_PERF_SCENARIOS.map((s) => [s.id, s]));
|
||||
@@ -111,8 +132,8 @@ const scenario = (id: string): UIPerfScenario => {
|
||||
const r = simulate(spec);
|
||||
ok(r.commits <= r.frames + 1, `01: one reducer pass per frame at most (${r.commits} commits / ${r.frames} frames)`);
|
||||
ok(
|
||||
r.markdownParses <= spec.paragraphs + 3,
|
||||
`01: markdown parses bounded by blocks, not ticks (${r.markdownParses} for ${spec.paragraphs} paragraphs)`,
|
||||
r.answerMarkdownParses <= spec.paragraphs + 3,
|
||||
`01: markdown parses bounded by blocks, not ticks (${r.answerMarkdownParses} for ${spec.paragraphs} paragraphs)`,
|
||||
);
|
||||
ok(r.state.live !== undefined && r.state.live.text.length >= spec.textChars, "01: full answer reached the live stream");
|
||||
}
|
||||
@@ -123,7 +144,8 @@ const scenario = (id: string): UIPerfScenario => {
|
||||
const r = simulate(spec);
|
||||
const seconds = r.frames / 60;
|
||||
ok(r.commits / seconds <= 61, `02: state commits stay at or under display FPS (${(r.commits / seconds).toFixed(1)}/s)`);
|
||||
ok(r.markdownParses <= spec.paragraphs + 3, `02: reasoning stream causes no markdown parses (${r.markdownParses})`);
|
||||
ok(r.reasoningMarkdownParses <= 2, `02: visible reasoning Markdown stays within its parse budget (${r.reasoningMarkdownParses})`);
|
||||
ok(r.answerMarkdownParses <= spec.paragraphs + 3, `02: answer Markdown keeps its existing parse budget (${r.answerMarkdownParses})`);
|
||||
ok(r.state.live !== undefined && r.state.live.reasoning.length >= spec.reasoningChars, "02: full reasoning reached the live stream");
|
||||
eq(r.state.live?.reasoningComplete, true, "02: answer text after reasoning completed it");
|
||||
}
|
||||
@@ -134,12 +156,12 @@ const scenario = (id: string): UIPerfScenario => {
|
||||
const r = simulate(spec);
|
||||
ok(r.commits <= r.frames + 1, `03: commits bounded by frames (${r.commits}/${r.frames})`);
|
||||
ok(
|
||||
r.markdownParses >= spec.codeFences,
|
||||
`03: open fences keep committing so streamed code stays highlighted (${r.markdownParses} parses)`,
|
||||
r.answerMarkdownParses >= spec.codeFences,
|
||||
`03: open fences keep committing so streamed code stays highlighted (${r.answerMarkdownParses} parses)`,
|
||||
);
|
||||
ok(
|
||||
r.markdownParses <= Math.ceil(r.frames / 3) + spec.paragraphs + spec.codeFences,
|
||||
`03: parse cadence capped by the 50ms tier even inside fences (${r.markdownParses} parses / ${r.frames} frames)`,
|
||||
r.answerMarkdownParses <= Math.ceil(r.frames / 3) + spec.paragraphs + spec.codeFences,
|
||||
`03: parse cadence capped by the 50ms tier even inside fences (${r.answerMarkdownParses} parses / ${r.frames} frames)`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,6 +190,7 @@ const scenario = (id: string): UIPerfScenario => {
|
||||
{
|
||||
const r = simulate(scenario("UI-PERF-06"));
|
||||
eq(r.bumpSkipViolations, 0, "06: background streaming never trips the whole-App bump — liveStore only");
|
||||
eq(r.reasoningMarkdownParses, 0, "06: background reasoning performs no Markdown parsing");
|
||||
}
|
||||
|
||||
process.stdout.write(`\n${passed} passed, ${failed} failed\n`);
|
||||
|
||||
@@ -191,6 +191,13 @@ export function useRenderedMarkdownText(text: string, streaming: boolean): strin
|
||||
cancelFinalizationRef.current?.();
|
||||
cancelFinalizationRef.current = null;
|
||||
finalizingTextRef.current = null;
|
||||
// A bounded live preview occasionally advances its window and drops an
|
||||
// old prefix. Discard the stale parsed tree before paint; the complete
|
||||
// replacement stays visible through StreamingMarkdownTail and is parsed
|
||||
// later under the normal adaptive budget.
|
||||
if (renderedText !== "" && !text.startsWith(renderedText)) {
|
||||
setRenderedText("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
lastCommitAtRef.current = 0;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useT } from "../lib/i18n";
|
||||
import { ImageViewer } from "./ImageViewer";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
import { useGSAPCollapse } from "../lib/useGSAPCollapse";
|
||||
import { displayReasoningText } from "../lib/reasoningDisplay";
|
||||
import { displayReasoningText, STREAMING_REASONING_WINDOW_STEP_CHARS, STREAMING_REASONING_WINDOW_STEP_LINES } from "../lib/reasoningDisplay";
|
||||
import { stripMemoryCompilerExecution } from "../lib/memoryCompilerDisplay";
|
||||
import { visibleTranscriptMemoryCitations } from "../lib/memoryCitationVisibility";
|
||||
import { invocationSegmentsFromMessage, type InvocationMetadataMap } from "../lib/invocationDisplay";
|
||||
@@ -914,8 +914,8 @@ function ReasoningPanel({
|
||||
const isReasoningRunning = item.streaming && !item.reasoningComplete;
|
||||
const visibleReasoning = reasoningOpen
|
||||
? displayReasoningText(item.reasoning, {
|
||||
streaming: item.streaming,
|
||||
truncateStreaming: truncateStreamingReasoning,
|
||||
streaming: isReasoningRunning,
|
||||
truncateStreaming: truncateStreamingReasoning, stableWindowChars: STREAMING_REASONING_WINDOW_STEP_CHARS, stableWindowLines: STREAMING_REASONING_WINDOW_STEP_LINES,
|
||||
})
|
||||
: "";
|
||||
const label = isReasoningRunning ? t("msg.thinkingRunning") : t("msg.thinking");
|
||||
@@ -936,7 +936,7 @@ function ReasoningPanel({
|
||||
<ChevronRight className={`reasoning__chevron${reasoningOpen ? " reasoning__chevron--open" : ""}`} size={12} />
|
||||
</button>
|
||||
{reasoningOpen && (
|
||||
<div ref={reasoningBodyRef} className="reasoning__body">{visibleReasoning}</div>
|
||||
<div ref={reasoningBodyRef} className="reasoning__body"><Markdown text={visibleReasoning} streaming={isReasoningRunning} /></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useGSAPCollapse } from "../lib/useGSAPCollapse";
|
||||
import { isTerminalSubagentPhase, type Item, type SubagentPhase } from "../lib/useController";
|
||||
import type { Translator } from "../lib/i18n";
|
||||
import { ReadOnlyBatch } from "./ReadOnlyBatch";
|
||||
import { Markdown } from "./Markdown";
|
||||
|
||||
type ToolItem = Extract<Item, { kind: "tool" }>;
|
||||
|
||||
@@ -357,7 +358,13 @@ export const ToolCard = memo(function ToolCard({ item, subcalls, tabId, displayN
|
||||
{sp.reasoning && (
|
||||
<div className="tool__subagent-preview-section">
|
||||
<div className="tool__subagent-preview-label">{t("subagent.preview.reasoning")}</div>
|
||||
<pre className="tool__subagent-preview-text">{sp.reasoning}</pre>
|
||||
{open ? (
|
||||
<div className="tool__subagent-preview-text tool__subagent-preview-text--markdown">
|
||||
<Markdown text={sp.reasoning} streaming={sp.phase === "reasoning"} />
|
||||
</div>
|
||||
) : (
|
||||
<pre className="tool__subagent-preview-text">{sp.reasoning}</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{sp.text && (
|
||||
|
||||
@@ -19,8 +19,9 @@ import { useEntranceAnimation } from "../lib/useEntranceAnimation";
|
||||
import { useScrollManager } from "../lib/useScrollManager";
|
||||
import { buildTurnGroups, compactQuestionText, createWarmLayerState, lastQuestionTurn, questionAnchorId, questionTurnsById, scrollVersion, warmColdPageForTurn, warmLayerWithColdPageAtLeast, warmLayerWithExpandedTurn, warmLayerWithNextColdPage, warmPagination, warmUserPreview, type QuestionAnchor, type TurnGroup, type WarmLayerState } from "../lib/transcriptGrouping";
|
||||
import { appendTurnActionCopyText } from "../lib/turnActionCopy";
|
||||
import { displayReasoningText } from "../lib/reasoningDisplay";
|
||||
import { displayReasoningText, STREAMING_REASONING_WINDOW_STEP_CHARS, STREAMING_REASONING_WINDOW_STEP_LINES } from "../lib/reasoningDisplay";
|
||||
import { observeScrollContentSize } from "../lib/scrollContentObserver";
|
||||
import { Markdown } from "./Markdown";
|
||||
|
||||
type ToolItem = Extract<Item, { kind: "tool" }>;
|
||||
type AssistantItem = Extract<Item, { kind: "assistant" }>;
|
||||
@@ -81,7 +82,7 @@ const LiveAssistantMessage = memo(function LiveAssistantMessage({
|
||||
);
|
||||
});
|
||||
|
||||
function InlineAssistantReasoning({ item }: { item: AssistantItem }) {
|
||||
function InlineAssistantReasoning({ item, active }: { item: AssistantItem; active: boolean }) {
|
||||
const t = useT();
|
||||
const live = useContext(LiveStreamContext);
|
||||
const [open, setOpen] = useState(true);
|
||||
@@ -94,13 +95,12 @@ function InlineAssistantReasoning({ item }: { item: AssistantItem }) {
|
||||
reasoningComplete: live.reasoningComplete,
|
||||
}
|
||||
: item;
|
||||
const reasoning = shown.reasoning.trim();
|
||||
const reasoning = shown.reasoning.trim(); const running = shown.streaming && !shown.reasoningComplete;
|
||||
if (!reasoning) return null;
|
||||
const visibleReasoning = displayReasoningText(shown.reasoning, {
|
||||
streaming: shown.streaming,
|
||||
truncateStreaming: true,
|
||||
streaming: running,
|
||||
truncateStreaming: true, stableWindowChars: STREAMING_REASONING_WINDOW_STEP_CHARS, stableWindowLines: STREAMING_REASONING_WINDOW_STEP_LINES,
|
||||
});
|
||||
const running = shown.streaming && !shown.reasoningComplete;
|
||||
return (
|
||||
<div className={`turn-collapse__reasoning-phase${open ? " turn-collapse__reasoning-phase--open" : ""}`}>
|
||||
<button
|
||||
@@ -114,7 +114,7 @@ function InlineAssistantReasoning({ item }: { item: AssistantItem }) {
|
||||
<span>{running ? t("msg.thinkingRunning") : t("msg.thinking")}</span>
|
||||
<ChevronRight className={`reasoning__chevron${open ? " reasoning__chevron--open" : ""}`} size={12} />
|
||||
</button>
|
||||
<div ref={bodyRef} className="turn-collapse__inline-reasoning">{visibleReasoning}</div>
|
||||
<div ref={bodyRef} className="turn-collapse__inline-reasoning">{active && open ? <Markdown text={visibleReasoning} streaming={running} /> : visibleReasoning}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1525,7 +1525,7 @@ function TurnCollapse({ items, durationMs, mode, subcalls, tabId, creationMode =
|
||||
case "assistant":
|
||||
// Answer text renders outside the fold (partitionTurnItems strips it),
|
||||
// so the fold only ever shows the reasoning segment.
|
||||
body.push(<InlineAssistantReasoning key={`${it.id}-reasoning`} item={it as AssistantItem} />);
|
||||
body.push(<InlineAssistantReasoning key={`${it.id}-reasoning`} item={it as AssistantItem} active={open} />);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
export const STREAMING_REASONING_TAIL_CHARS = 12_000;
|
||||
export const STREAMING_REASONING_TAIL_LINES = 240;
|
||||
export const STREAMING_REASONING_WINDOW_STEP_CHARS = 2_000;
|
||||
export const STREAMING_REASONING_WINDOW_STEP_LINES = 40;
|
||||
|
||||
type ReasoningDisplayOptions = {
|
||||
streaming: boolean;
|
||||
truncateStreaming?: boolean;
|
||||
maxChars?: number;
|
||||
maxLines?: number;
|
||||
stableWindowChars?: number;
|
||||
stableWindowLines?: number;
|
||||
};
|
||||
|
||||
export function displayReasoningText(
|
||||
@@ -15,6 +19,8 @@ export function displayReasoningText(
|
||||
truncateStreaming = true,
|
||||
maxChars = STREAMING_REASONING_TAIL_CHARS,
|
||||
maxLines = STREAMING_REASONING_TAIL_LINES,
|
||||
stableWindowChars = 0,
|
||||
stableWindowLines = 0,
|
||||
}: ReasoningDisplayOptions,
|
||||
): string {
|
||||
if (!streaming || !truncateStreaming) return reasoning;
|
||||
@@ -23,15 +29,27 @@ export function displayReasoningText(
|
||||
let truncated = false;
|
||||
|
||||
if (maxChars > 0 && text.length > maxChars) {
|
||||
text = text.slice(-maxChars);
|
||||
truncated = true;
|
||||
const overflow = text.length - maxChars;
|
||||
const start = stableWindowChars > 0
|
||||
? Math.floor(overflow / stableWindowChars) * stableWindowChars
|
||||
: overflow;
|
||||
if (start > 0) {
|
||||
text = text.slice(start);
|
||||
truncated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxLines > 0) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
if (lines.length > maxLines) {
|
||||
text = lines.slice(-maxLines).join("\n");
|
||||
truncated = true;
|
||||
const overflow = lines.length - maxLines;
|
||||
const start = stableWindowLines > 0
|
||||
? Math.floor(overflow / stableWindowLines) * stableWindowLines
|
||||
: overflow;
|
||||
if (start > 0) {
|
||||
text = lines.slice(start).join("\n");
|
||||
truncated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface UIPerfScenario {
|
||||
title: string;
|
||||
chunksPerSec: number;
|
||||
reasoningChars: number;
|
||||
reasoningVisible: boolean;
|
||||
textChars: number;
|
||||
paragraphs: number;
|
||||
codeFences: number;
|
||||
@@ -26,6 +27,7 @@ export const UI_PERF_SCENARIOS: UIPerfScenario[] = [
|
||||
title: "normal answer: 2KB markdown prose",
|
||||
chunksPerSec: 80,
|
||||
reasoningChars: 0,
|
||||
reasoningVisible: false,
|
||||
textChars: 2_000,
|
||||
paragraphs: 8,
|
||||
codeFences: 0,
|
||||
@@ -36,6 +38,7 @@ export const UI_PERF_SCENARIOS: UIPerfScenario[] = [
|
||||
title: "DeepSeek reasoning: 16KB reasoning + 8KB answer",
|
||||
chunksPerSec: 150,
|
||||
reasoningChars: 16_000,
|
||||
reasoningVisible: true,
|
||||
textChars: 8_000,
|
||||
paragraphs: 16,
|
||||
codeFences: 0,
|
||||
@@ -46,6 +49,7 @@ export const UI_PERF_SCENARIOS: UIPerfScenario[] = [
|
||||
title: "code heavy: 10 fences in 20KB markdown",
|
||||
chunksPerSec: 120,
|
||||
reasoningChars: 0,
|
||||
reasoningVisible: false,
|
||||
textChars: 20_000,
|
||||
paragraphs: 10,
|
||||
codeFences: 10,
|
||||
@@ -56,6 +60,7 @@ export const UI_PERF_SCENARIOS: UIPerfScenario[] = [
|
||||
title: "interaction under streaming: typing, wheel, selection, tool cards",
|
||||
chunksPerSec: 150,
|
||||
reasoningChars: 8_000,
|
||||
reasoningVisible: true,
|
||||
textChars: 8_000,
|
||||
paragraphs: 12,
|
||||
codeFences: 2,
|
||||
@@ -67,6 +72,7 @@ export const UI_PERF_SCENARIOS: UIPerfScenario[] = [
|
||||
title: "long session: 100 turns, 30 tool calls, diffs and code",
|
||||
chunksPerSec: 100,
|
||||
reasoningChars: 0,
|
||||
reasoningVisible: false,
|
||||
textChars: 4_000,
|
||||
paragraphs: 10,
|
||||
codeFences: 2,
|
||||
@@ -78,6 +84,7 @@ export const UI_PERF_SCENARIOS: UIPerfScenario[] = [
|
||||
title: "background agent: tab A streams while tab B is active",
|
||||
chunksPerSec: 150,
|
||||
reasoningChars: 4_000,
|
||||
reasoningVisible: false,
|
||||
textChars: 4_000,
|
||||
paragraphs: 8,
|
||||
codeFences: 0,
|
||||
|
||||
@@ -4963,6 +4963,25 @@ a[href] {
|
||||
line-height: 1.72;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.reasoning__body > .md,
|
||||
.turn-collapse__inline-reasoning > .md,
|
||||
.tool__subagent-preview-text--markdown > .md {
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
white-space: normal;
|
||||
}
|
||||
.reasoning__body > .md p,
|
||||
.turn-collapse__inline-reasoning > .md p,
|
||||
.tool__subagent-preview-text--markdown > .md p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.reasoning__body > .md :where(h1, h2, h3, h4),
|
||||
.turn-collapse__inline-reasoning > .md :where(h1, h2, h3, h4),
|
||||
.tool__subagent-preview-text--markdown > .md :where(h1, h2, h3, h4) {
|
||||
margin: 12px 0 6px;
|
||||
font-size: 1em;
|
||||
}
|
||||
/* ── markdown prose ──────────────────────────────────────────────────────── */
|
||||
.md {
|
||||
font-family: var(--font-content-family);
|
||||
|
||||
Reference in new issue
Block a user