mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
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>
201 lines
6.1 KiB
TypeScript
201 lines
6.1 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* WebFetch/WebSearch tool call component
|
|
* Displays web fetch and search operations with URL/query and output
|
|
*/
|
|
|
|
import { useState, type FC } from 'react';
|
|
import {
|
|
ToolCallContainer,
|
|
safeTitle,
|
|
groupContent,
|
|
mapToolStatusToContainerStatus,
|
|
} 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';
|
|
|
|
/** Default collapsed height in pixels */
|
|
const COLLAPSED_HEIGHT = 120;
|
|
|
|
/** Threshold for showing expand button (content longer than this will be collapsible) */
|
|
const EXPAND_THRESHOLD = 300;
|
|
|
|
/**
|
|
* Get the URL or query from tool call data
|
|
* @param variant - 'fetch' or 'search'
|
|
* @param title - Tool call title
|
|
* @param rawInput - Raw input object
|
|
* @returns URL or query string
|
|
*/
|
|
const getWebTarget = (
|
|
variant: WebVariant,
|
|
title: unknown,
|
|
rawInput?: unknown,
|
|
): string => {
|
|
// Try to extract URL or query from rawInput
|
|
if (rawInput && typeof rawInput === 'object') {
|
|
const input = rawInput as Record<string, unknown>;
|
|
if (variant === 'fetch' && input['url']) {
|
|
return String(input['url']);
|
|
}
|
|
if (variant === 'search' && input['query']) {
|
|
return String(input['query']);
|
|
}
|
|
}
|
|
return safeTitle(title);
|
|
};
|
|
|
|
/**
|
|
* Output card component with expand/collapse functionality
|
|
* @param props - Component props
|
|
* @returns JSX element
|
|
*/
|
|
const OutputCard: FC<{
|
|
content: string;
|
|
isError?: boolean;
|
|
}> = ({ content, isError = false }) => {
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const isLongContent = content.length > EXPAND_THRESHOLD;
|
|
|
|
return (
|
|
<div className="border-[0.5px] border-[var(--app-input-border)] rounded-[5px] bg-[var(--app-tool-background)] my-2 max-w-full text-[1em] items-start">
|
|
<div className="flex flex-col gap-[3px] p-1">
|
|
<div className="grid grid-cols-[max-content_1fr] p-1">
|
|
<div className="text-[var(--app-secondary-foreground)] text-left opacity-50 py-1 px-2 pl-1 font-mono text-[0.85em]">
|
|
OUT
|
|
</div>
|
|
<div
|
|
className={`break-words m-0 p-1 overflow-hidden ${
|
|
isError ? 'whitespace-pre-wrap' : ''
|
|
}`}
|
|
style={
|
|
!isExpanded && isLongContent
|
|
? {
|
|
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
|
|
}
|
|
>
|
|
{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>
|
|
|
|
{/* Expand/Collapse button */}
|
|
{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>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* WebFetch/WebSearch tool call implementation
|
|
* @param props - Component props including toolCall, variant, isFirst, isLast
|
|
* @returns JSX element
|
|
*/
|
|
const WebFetchToolCallImpl: FC<BaseToolCallProps & { variant: WebVariant }> = ({
|
|
toolCall,
|
|
variant,
|
|
isFirst,
|
|
isLast,
|
|
}) => {
|
|
const { title, content, rawInput, toolCallId } = toolCall;
|
|
|
|
const webTarget = getWebTarget(variant, title, rawInput);
|
|
const label = getToolDisplayLabel({ kind: toolCall.kind, title });
|
|
|
|
// Group content by type
|
|
const { textOutputs, errors } = groupContent(content);
|
|
|
|
// Map tool status to container status
|
|
const containerStatus =
|
|
errors.length > 0
|
|
? 'error'
|
|
: mapToolStatusToContainerStatus(toolCall.status);
|
|
|
|
// Error case
|
|
if (errors.length > 0) {
|
|
return (
|
|
<ToolCallContainer
|
|
label={label}
|
|
status={containerStatus}
|
|
toolCallId={toolCallId}
|
|
isFirst={isFirst}
|
|
isLast={isLast}
|
|
labelSuffix={webTarget}
|
|
>
|
|
<OutputCard content={errors.join('\n')} isError />
|
|
</ToolCallContainer>
|
|
);
|
|
}
|
|
|
|
// Success with output
|
|
if (textOutputs.length > 0) {
|
|
const output = textOutputs.join('\n');
|
|
|
|
return (
|
|
<ToolCallContainer
|
|
label={label}
|
|
status={containerStatus}
|
|
toolCallId={toolCallId}
|
|
isFirst={isFirst}
|
|
isLast={isLast}
|
|
labelSuffix={webTarget}
|
|
>
|
|
<OutputCard content={output} />
|
|
</ToolCallContainer>
|
|
);
|
|
}
|
|
|
|
// No output yet - show just the URL/query (loading state)
|
|
return (
|
|
<ToolCallContainer
|
|
label={label}
|
|
status={containerStatus}
|
|
toolCallId={toolCallId}
|
|
isFirst={isFirst}
|
|
isLast={isLast}
|
|
labelSuffix={webTarget}
|
|
/>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* WebFetchToolCall - displays web fetch/search tool calls
|
|
* Shows URL/query and output with OUT card
|
|
* @param props - Component props
|
|
* @returns JSX element
|
|
*/
|
|
export const WebFetchToolCall: FC<BaseToolCallProps> = (props) => {
|
|
const normalizedKind = props.toolCall.kind.toLowerCase();
|
|
const variant: WebVariant =
|
|
normalizedKind === 'web_search' || normalizedKind === 'websearch'
|
|
? 'search'
|
|
: 'fetch'; // 'fetch', 'web_fetch', 'webfetch' all map to 'fetch'
|
|
return <WebFetchToolCallImpl {...props} variant={variant} />;
|
|
};
|