fix(workspace): 统一折叠思考块代码以消除流式抖动
- 移除思考链代码块按长度判断的折叠策略,改为默认折叠\n- 为思考内容与 reasoning-only 路径添加代码块默认折叠渲染\n- 调整流式 Markdown 渲染参数,降低流式重排与闪烁
This commit is contained in:
parent
045b99dd13
commit
7ea2bceb78
|
|
@ -168,18 +168,56 @@ export type ReasoningContentProps = ComponentProps<
|
|||
};
|
||||
|
||||
export const ReasoningContent = memo(
|
||||
({ className, children, ...props }: ReasoningContentProps) => (
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
"mt-4 text-sm",
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Streamdown {...props}>{children}</Streamdown>
|
||||
</CollapsibleContent>
|
||||
),
|
||||
({ className, children, ...props }: ReasoningContentProps) => {
|
||||
const { isStreaming } = useReasoning();
|
||||
const thinkingComponents = {
|
||||
code: ({ className, children, ...codeProps }: ComponentProps<"code">) => {
|
||||
const isBlock =
|
||||
typeof className === "string" && className.includes("language-");
|
||||
if (!isBlock) {
|
||||
return (
|
||||
<code className={className} {...codeProps}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<details className="my-2 rounded-md border">
|
||||
<summary className="text-muted-foreground cursor-pointer px-3 py-2 text-xs select-none">
|
||||
Show code
|
||||
</summary>
|
||||
<pre className="bg-muted/40 overflow-x-auto border-t p-3 text-xs">
|
||||
<code className={className} {...codeProps}>
|
||||
{children}
|
||||
</code>
|
||||
</pre>
|
||||
</details>
|
||||
);
|
||||
},
|
||||
};
|
||||
return (
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
"mt-4 text-sm",
|
||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{isStreaming ? (
|
||||
<div className="whitespace-pre-wrap break-words">{children}</div>
|
||||
) : (
|
||||
<Streamdown
|
||||
isAnimating={false}
|
||||
parseIncompleteMarkdown={true}
|
||||
components={thinkingComponents}
|
||||
>
|
||||
{children}
|
||||
</Streamdown>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reasoning.displayName = "Reasoning";
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export type MarkdownContentProps = {
|
|||
/** Renders markdown content. */
|
||||
export function MarkdownContent({
|
||||
content,
|
||||
isLoading,
|
||||
rehypePlugins,
|
||||
className,
|
||||
remarkPlugins = streamdownPlugins.remarkPlugins,
|
||||
|
|
@ -66,6 +67,8 @@ export function MarkdownContent({
|
|||
return (
|
||||
<MessageResponse
|
||||
className={className}
|
||||
isAnimating={isLoading}
|
||||
parseIncompleteMarkdown={!isLoading}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={components}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
SquareTerminalIcon,
|
||||
WrenchIcon,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo, useState, type ComponentProps } from "react";
|
||||
import type { BundledLanguage } from "shiki";
|
||||
|
||||
import {
|
||||
|
|
@ -40,7 +40,6 @@ import { Tooltip } from "../tooltip";
|
|||
|
||||
import { MarkdownContent } from "./markdown-content";
|
||||
|
||||
const TOOL_CONTENT_COLLAPSE_THRESHOLD = 320;
|
||||
|
||||
export function MessageGroup({
|
||||
className,
|
||||
|
|
@ -83,7 +82,41 @@ export function MessageGroup({
|
|||
aboveLastToolCallSteps.length + (lastToolCallStep ? 1 : 0);
|
||||
const shouldShowToolSteps =
|
||||
!!lastToolCallStep && (showAbove || aboveLastToolCallSteps.length === 0);
|
||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
||||
// Disable per-word animation in reasoning/tool chain content to avoid
|
||||
// repeated DOM churn while streaming updates arrive.
|
||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(false);
|
||||
const thinkingComponents = useMemo(
|
||||
() => ({
|
||||
code: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<"code">) => {
|
||||
const isBlock =
|
||||
typeof className === "string" && className.includes("language-");
|
||||
if (!isBlock) {
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<details className="my-2 rounded-md border">
|
||||
<summary className="text-muted-foreground cursor-pointer px-3 py-2 text-xs select-none">
|
||||
{t.toolCalls.expandContent}
|
||||
</summary>
|
||||
<pre className="bg-muted/40 overflow-x-auto border-t p-3 text-xs">
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
</pre>
|
||||
</details>
|
||||
);
|
||||
},
|
||||
}),
|
||||
[t.toolCalls.expandContent],
|
||||
);
|
||||
return (
|
||||
<ChainOfThought
|
||||
className={cn("w-full gap-2 rounded-lg bg-white", className)}
|
||||
|
|
@ -130,6 +163,7 @@ export function MessageGroup({
|
|||
content={step.reasoning ?? ""}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={thinkingComponents}
|
||||
/>
|
||||
}
|
||||
></ChainOfThoughtStep>
|
||||
|
|
@ -185,6 +219,7 @@ export function MessageGroup({
|
|||
content={lastReasoningStep.reasoning ?? ""}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={thinkingComponents}
|
||||
/>
|
||||
}
|
||||
></ChainOfThoughtStep>
|
||||
|
|
@ -227,8 +262,8 @@ function ToolCall({
|
|||
language?: BundledLanguage;
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
const shouldCollapse = content.length > TOOL_CONTENT_COLLAPSE_THRESHOLD;
|
||||
const shouldShowCodeBlock = !shouldCollapse || expanded;
|
||||
// Always start collapsed in thinking blocks; user must explicitly expand.
|
||||
const shouldShowCodeBlock = expanded;
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
|
|
@ -417,28 +452,24 @@ function ToolCall({
|
|||
return t.toolCalls.executeCommand;
|
||||
}
|
||||
const command: string | undefined = (args as { command: string })?.command;
|
||||
const shouldCollapse =
|
||||
!!command && command.length > TOOL_CONTENT_COLLAPSE_THRESHOLD;
|
||||
return (
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
label={description}
|
||||
icon={SquareTerminalIcon}
|
||||
action={
|
||||
shouldCollapse ? (
|
||||
<Button
|
||||
className="h-7 px-3 text-xs"
|
||||
variant="ghost"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setIsCommandExpanded((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{isCommandExpanded
|
||||
? t.toolCalls.collapseContent
|
||||
: t.toolCalls.expandContent}
|
||||
</Button>
|
||||
) : undefined
|
||||
<Button
|
||||
className="h-7 px-3 text-xs"
|
||||
variant="ghost"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setIsCommandExpanded((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
{isCommandExpanded
|
||||
? t.toolCalls.collapseContent
|
||||
: t.toolCalls.expandContent}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{command && (
|
||||
|
|
|
|||
Loading…
Reference in New Issue