feat(webui): render markdown in generic and web-fetch tool outputs (#3469)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run

* feat(webui): render markdown in generic and web-fetch tool outputs

Agent and other tool call outputs were wrapped in raw <pre> blocks,
leaving markdown-formatted text (code blocks, lists, bold) as unrendered
characters in HTML exports. Route long GenericToolCall output and
WebFetchToolCall success output through MarkdownRenderer, and add
collapse/expand affordance to long generic output for readability.

Closes #2520

Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com>

* fix(webui): move tool-output mask-image to inline style, disable file-link in webfetch

Tailwind's static scanner can't emit classes built from template-string
interpolation (max-h-[${n}px], [mask-image:...] with runtime values),
so the collapsed-state fade on GenericToolCall and WebFetchToolCall
output cards was not actually applied at runtime — only the inline
maxHeight was collapsing content. Move both max-height and mask-image
to inline style, and add -webkit-mask-image for Safari.

Also pass enableFileLinks={false} to MarkdownRenderer for WebFetch
output so raw fetched text keeps file-like strings (README.md, URLs
ending in .md, etc.) as literal output instead of converting them to
local workspace links.

Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com>

* fix(webui): disable file-link in generic tool-call output

Align GenericToolCall markdown rendering with WebFetchToolCall by
passing enableFileLinks={false}. Without a click handler wired, file-like
strings (e.g. README.md) would render as clickable links that do nothing,
which is a misleading affordance in the tool-call display/export context.

Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>
This commit is contained in:
qqqys 2026-04-21 15:53:32 +08:00 committed by GitHub
parent afbb5e71db
commit 00896f8605
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 19 deletions

View file

@ -6,7 +6,7 @@
* Generic tool call component - handles all tool call types as fallback
*/
import type { FC } from 'react';
import { useState, type FC } from 'react';
import {
ToolCallContainer,
ToolCallCard,
@ -17,6 +17,45 @@ import {
} from './shared/index.js';
import type { BaseToolCallProps } from './shared/index.js';
import { getToolDisplayLabel } from './labelUtils.js';
import { MarkdownRenderer } from '../messages/MarkdownRenderer/MarkdownRenderer.js';
const COLLAPSED_HEIGHT = 200;
const EXPAND_THRESHOLD = 400;
const CollapsibleOutput: FC<{ content: string }> = ({ content }) => {
const [isExpanded, setIsExpanded] = useState(false);
const isLongContent = content.length > EXPAND_THRESHOLD;
return (
<div className="flex flex-col gap-[3px]">
<div
className="text-[13px] opacity-90 overflow-hidden"
style={
!isExpanded && isLongContent
? {
maxHeight: `${COLLAPSED_HEIGHT}px`,
maskImage: `linear-gradient(to bottom, var(--app-primary-background) 140px, transparent ${COLLAPSED_HEIGHT}px)`,
WebkitMaskImage: `linear-gradient(to bottom, var(--app-primary-background) 140px, transparent ${COLLAPSED_HEIGHT}px)`,
}
: undefined
}
>
<MarkdownRenderer content={content} enableFileLinks={false} />
</div>
{isLongContent && (
<div className="flex justify-center border-t border-[var(--app-input-border)] pt-1">
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="text-[var(--app-secondary-foreground)] text-[0.8em] hover:text-[var(--app-primary-foreground)] cursor-pointer bg-transparent border-none px-2 py-1 rounded hover:bg-[var(--app-input-background)] transition-colors"
>
{isExpanded ? '▲ Collapse' : '▼ Show more'}
</button>
</div>
)}
</div>
);
};
/**
* Generic tool call component that can display any tool call type
@ -55,18 +94,13 @@ export const GenericToolCall: FC<BaseToolCallProps> = ({
const isLong = output.length > 150;
if (isLong) {
const truncatedOutput =
output.length > 300 ? output.substring(0, 300) + '...' : output;
return (
<ToolCallCard icon="🔧">
<ToolCallRow label={displayLabel}>
<div>{operationText}</div>
</ToolCallRow>
<ToolCallRow label="Output">
<div className="whitespace-pre-wrap font-mono text-[13px] opacity-90">
{truncatedOutput}
</div>
<CollapsibleOutput content={output} />
</ToolCallRow>
</ToolCallCard>
);

View file

@ -16,6 +16,7 @@ import {
} from './shared/index.js';
import type { BaseToolCallProps } from './shared/index.js';
import { getToolDisplayLabel } from './labelUtils.js';
import { MarkdownRenderer } from '../messages/MarkdownRenderer/MarkdownRenderer.js';
type WebVariant = 'fetch' | 'search';
@ -70,24 +71,28 @@ const OutputCard: FC<{
OUT
</div>
<div
className={`whitespace-pre-wrap break-words m-0 p-1 overflow-hidden ${
!isExpanded && isLongContent
? `max-h-[${COLLAPSED_HEIGHT}px] [mask-image:linear-gradient(to_bottom,var(--app-primary-background)_80px,transparent_${COLLAPSED_HEIGHT}px)]`
: ''
className={`break-words m-0 p-1 overflow-hidden ${
isError ? 'whitespace-pre-wrap' : ''
}`}
style={
!isExpanded && isLongContent
? { maxHeight: `${COLLAPSED_HEIGHT}px` }
? {
maxHeight: `${COLLAPSED_HEIGHT}px`,
maskImage: `linear-gradient(to bottom, var(--app-primary-background) 80px, transparent ${COLLAPSED_HEIGHT}px)`,
WebkitMaskImage: `linear-gradient(to bottom, var(--app-primary-background) 80px, transparent ${COLLAPSED_HEIGHT}px)`,
}
: undefined
}
>
<pre
className={`m-0 overflow-hidden font-mono text-[0.85em] ${
isError ? 'text-[#c74e39]' : ''
}`}
>
{content}
</pre>
{isError ? (
<pre className="m-0 overflow-hidden font-mono text-[0.85em] text-[#c74e39]">
{content}
</pre>
) : (
<div className="text-[0.85em]">
<MarkdownRenderer content={content} enableFileLinks={false} />
</div>
)}
</div>
</div>