mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-28 02:13:59 +00:00
* feat(mcp): add 2026 protocol negotiation * feat(mcp): render MCP Apps in WebShell * fix(mcp): keep legacy tool discovery lenient * fix(mcp): keep Apps HTML out of TUI and honor tool visibility TUI and history compaction dumped mcp_app HTML as JSON, and discoverTools registered app-only tools for the model. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): stabilize AppBridge lifetime and close sandbox CSP gaps Theme toggles and transcript reseeds were tearing down MCP Apps; the host CSP also allowed any loopback port and form posts bypassed connect-src. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): list under-declared modern MCP capabilities over the wire v2 typed helpers return [] without a request when a capability is omitted. Use them only when the server declared the capability, and keep Apps unmounted in collapsed tool rows. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): keep Apps sandbox reachable and list past 64 pages Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): reject empty compacted html and keep MCP Apps expanded in multi-tool groups Fixes R3-1 and R3-2 review comments: R3-1: getMcpAppDisplay now rejects empty html strings (from compaction) so session replay shows fallbackText instead of mounting an empty iframe. R3-2: ToolGroup now checks for MCP apps across all tools (not just singleTool), auto-expands when any tool has an MCP app, and keeps MCP app rows expanded (summaryOnly=false, forceExpanded=true) even when adjacent tool calls are merged into the group. * feat(web-shell): fold thinking into the compact-mode tool summary (#9148) Compact mode used to drop thinking messages entirely, so a running turn gave no indication of the thinking step. Keep the thoughts and aggregate them with the adjacent tools into one summary: a streaming thought reads "Thinking…" with the running shimmer, and a completed thought settles into a click-to-expand row in its original interleaved position. The translate action is preserved on both the thinking block and the folded thought rows, and the merged group gets a synthetic id so its expanded state never leaks into non-compact mode. Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> * fix(mcp): address app discovery and sandbox regressions * fix(web-shell): keep MCP apps expanded in compact summaries * Revert "feat(web-shell): fold thinking into the compact-mode tool summary (#9148)" This reverts commit ab2eebc5d36f17a51ce94e423db5745dcbb273fe. * fix(web-shell): render compacted MCP App fallback and teardown before unload Compacted history keeps type:mcp_app with empty html; show fallbackText instead of a blank sandbox, and wait for ui/resource-teardown before unloading the iframe. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): bound the discover probe and raise the daemon bundle cap Silent legacy stdio servers inherited the 10-minute request timeout for server/discover. Cap the probe at 5s so fallback fits the discovery window, and raise the browser bundle budget after the main merge overflowed CI by 47 bytes. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): skip version-negotiation probe on remote transports SDK v2 rejects HTTP server/discover timeouts without falling back to initialize, and the 5s probe consumed the entire remote discovery window. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web-shell): keep MCP App iframe src across deferred teardown Deferred unload() was clearing src on the live iframe after a remount, so the new AppBridge never saw sandbox-proxy-ready. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): honor listing-level MCP App CSP and permissions registerAppResource puts ui.csp/permissions on resources/list, and resources/read does not merge that metadata into content entries. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): reuse session client for list and emit app fallback text Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): keep mcp list and IPv6 sandbox CSP valid Give qwen mcp list leftover handshake budget after the 5s discover probe, and stop emitting invalid [::1] CSP origins. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): keep modern list and short discovery budgets working Drop the era-illegal ping after mcp list connect, shrink the stdio discover probe to the discovery window, and document that remotes stay on legacy initialize until the SDK can fall back. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): keep the 2026 slice free of review-only extras Drop the global tools/list page cap, generated companion notices, and the review screenshot so this PR stays on stdio 2026 plus the WebShell Apps host. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): restore generated companion notices after the SDK v2 bump CI regenerates NOTICES.txt from the lockfile; the file has to ship with the new MCP client dependencies. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): isolate the Apps proxy from WebShell storage Drop allow-same-origin on the outer sandbox iframe so a default localhost daemon cannot read the WebShell session token. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mcp): harden fallback and app sandbox * fix(core): preserve large and app-only MCP catalogs * fix(mcp): preserve legacy negotiation compatibility * fix(mcp): default stdio negotiation to legacy --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: ytahdn <1294726970@qq.com> Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> Co-authored-by: YungSen Hsin <yungsenhsin@U-G0HXNQM1-2052.local>
293 lines
9.5 KiB
TypeScript
293 lines
9.5 KiB
TypeScript
import '../styles/globals.css';
|
|
import {
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
type CSSProperties,
|
|
type ReactElement,
|
|
} from 'react';
|
|
import type { DaemonTranscriptBlock } from '@qwen-code/sdk/daemon';
|
|
import {
|
|
CompactModeContext,
|
|
TodoDetailContext,
|
|
TodoTimelineContext,
|
|
} from '../App';
|
|
import {
|
|
WebShellCustomizationProvider,
|
|
type AssistantTurnFooterRenderer,
|
|
type ComposerTagRenderer,
|
|
type MarkdownTableMode,
|
|
type ToolHeaderExtraRenderer,
|
|
type UserMessageContentParser,
|
|
type UserMessageContentRenderer,
|
|
type WebShellComposerTagIconMap,
|
|
type WebShellMarkdownCustomization,
|
|
} from '../customization';
|
|
import { ErrorBoundary } from './ErrorBoundary';
|
|
import { MessageList } from './MessageList';
|
|
import { RootErrorFallback } from './RootErrorFallback';
|
|
import {
|
|
getTranslator,
|
|
I18nProvider,
|
|
normalizeLanguage,
|
|
type WebShellLanguage,
|
|
} from '../i18n';
|
|
import { transcriptBlocksToLocalizedMessages } from '../hooks/useMessages';
|
|
import { WebShellPortalRootContext } from '../portalRoot';
|
|
import { computeTodoDetails, computeTodoTimeline } from '../utils/todos';
|
|
import {
|
|
ThemeProvider,
|
|
WebShellThemeId,
|
|
type WebShellTheme,
|
|
} from '../themeContext';
|
|
import { TranscriptRenderModeProvider } from '../transcriptRenderMode';
|
|
import styles from '../App.module.css';
|
|
import { McpAppHostContext } from '../mcpAppHostContext';
|
|
|
|
const DEFAULT_CHAT_MAX_WIDTH = 1000;
|
|
const CHAT_SHELL_HORIZONTAL_PADDING = 40;
|
|
|
|
export interface WebShellTranscriptProps {
|
|
blocks: readonly DaemonTranscriptBlock[];
|
|
theme?: WebShellTheme;
|
|
language?: 'en' | 'zh-CN' | 'zh' | 'zh-cn';
|
|
className?: string;
|
|
style?: CSSProperties;
|
|
chatMaxWidth?: number;
|
|
workspaceCwd?: string;
|
|
compactThinking?: boolean;
|
|
collapseCompletedTurns?: boolean;
|
|
markdownTableMode?: MarkdownTableMode;
|
|
virtualScrollThreshold?: number;
|
|
markdown?: WebShellMarkdownCustomization;
|
|
composerTagIcons?: WebShellComposerTagIconMap;
|
|
renderToolHeaderExtra?: ToolHeaderExtraRenderer;
|
|
parseUserMessageContent?: UserMessageContentParser;
|
|
renderUserMessageContent?: UserMessageContentRenderer;
|
|
renderComposerTag?: ComposerTagRenderer;
|
|
renderComposerTagTooltip?: ComposerTagRenderer;
|
|
renderAssistantTurnFooter?: AssistantTurnFooterRenderer;
|
|
mcpAppBaseUrl?: string;
|
|
}
|
|
|
|
function resolveLanguage(
|
|
language: WebShellTranscriptProps['language'],
|
|
): WebShellLanguage {
|
|
if (language !== undefined) return normalizeLanguage(language);
|
|
if (typeof window === 'undefined') return 'en';
|
|
const params = new URLSearchParams(window.location.search);
|
|
return normalizeLanguage(
|
|
params.get('language') ?? params.get('lang') ?? navigator.language,
|
|
);
|
|
}
|
|
|
|
function getChatWidthStyle(chatMaxWidth: number | undefined): CSSProperties {
|
|
const width =
|
|
typeof chatMaxWidth === 'number' &&
|
|
Number.isFinite(chatMaxWidth) &&
|
|
chatMaxWidth > 0
|
|
? chatMaxWidth
|
|
: DEFAULT_CHAT_MAX_WIDTH;
|
|
const contentWidth = `${width}px`;
|
|
const shellWidth = `calc(${contentWidth} + ${CHAT_SHELL_HORIZONTAL_PADDING}px)`;
|
|
return {
|
|
'--chat-regular-content-width': contentWidth,
|
|
'--chat-regular-shell-width': shellWidth,
|
|
'--chat-content-width': contentWidth,
|
|
'--chat-shell-width': shellWidth,
|
|
} as CSSProperties;
|
|
}
|
|
|
|
function WebShellTranscriptContent({
|
|
blocks,
|
|
theme = WebShellThemeId.Dark,
|
|
language,
|
|
className,
|
|
style,
|
|
chatMaxWidth,
|
|
workspaceCwd = '',
|
|
compactThinking = false,
|
|
collapseCompletedTurns = true,
|
|
markdownTableMode = 'basic',
|
|
virtualScrollThreshold,
|
|
markdown,
|
|
composerTagIcons,
|
|
renderToolHeaderExtra,
|
|
parseUserMessageContent,
|
|
renderUserMessageContent,
|
|
renderComposerTag,
|
|
renderComposerTagTooltip,
|
|
renderAssistantTurnFooter,
|
|
mcpAppBaseUrl,
|
|
}: WebShellTranscriptProps): ReactElement {
|
|
const resolvedLanguage = resolveLanguage(language);
|
|
const t = useMemo(() => getTranslator(resolvedLanguage), [resolvedLanguage]);
|
|
const messages = useMemo(
|
|
() => transcriptBlocksToLocalizedMessages(blocks, t),
|
|
[blocks, t],
|
|
);
|
|
const todoDetails = useMemo(() => computeTodoDetails(messages), [messages]);
|
|
const todoTimeline = useMemo(() => computeTodoTimeline(messages), [messages]);
|
|
const customization = useMemo(
|
|
() => ({
|
|
composerTagIcons,
|
|
renderToolHeaderExtra,
|
|
parseUserMessageContent,
|
|
renderUserMessageContent,
|
|
renderComposerTag,
|
|
renderComposerTagTooltip,
|
|
renderAssistantTurnFooter,
|
|
compactThinking,
|
|
collapseCompletedTurns,
|
|
markdownTableMode,
|
|
markdown,
|
|
}),
|
|
[
|
|
collapseCompletedTurns,
|
|
compactThinking,
|
|
composerTagIcons,
|
|
markdown,
|
|
markdownTableMode,
|
|
parseUserMessageContent,
|
|
renderAssistantTurnFooter,
|
|
renderComposerTag,
|
|
renderComposerTagTooltip,
|
|
renderToolHeaderExtra,
|
|
renderUserMessageContent,
|
|
],
|
|
);
|
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null);
|
|
const portalVariableNamesRef = useRef<Set<string>>(new Set());
|
|
const rootClassName = [
|
|
styles.app,
|
|
styles.appChat,
|
|
theme === WebShellThemeId.Light ? styles.themeLight : styles.themeDark,
|
|
theme === WebShellThemeId.Dark ? 'dark' : undefined,
|
|
className,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
const rootStyle = useMemo(
|
|
() => ({ ...style, ...getChatWidthStyle(chatMaxWidth) }),
|
|
[chatMaxWidth, style],
|
|
);
|
|
|
|
useLayoutEffect(() => {
|
|
if (typeof document === 'undefined') return;
|
|
const root = document.createElement('div');
|
|
root.dataset.webShellPortalRoot = '';
|
|
root.dataset.webShellShadcn = '';
|
|
document.body.appendChild(root);
|
|
setPortalRoot(root);
|
|
return () => {
|
|
root.remove();
|
|
setPortalRoot(null);
|
|
};
|
|
}, []);
|
|
|
|
useLayoutEffect(() => {
|
|
const root = rootRef.current;
|
|
if (!root || !portalRoot) return;
|
|
let frameId: number | null = null;
|
|
const syncVariables = () => {
|
|
frameId = null;
|
|
const computedStyle = getComputedStyle(root);
|
|
const nextNames = new Set<string>();
|
|
portalRoot.dataset.webShellShadcn = '';
|
|
portalRoot.classList.toggle('dark', theme === WebShellThemeId.Dark);
|
|
portalRoot.lang = resolvedLanguage;
|
|
for (let index = 0; index < computedStyle.length; index += 1) {
|
|
const name = computedStyle[index];
|
|
if (!name.startsWith('--')) continue;
|
|
nextNames.add(name);
|
|
portalRoot.style.setProperty(
|
|
name,
|
|
computedStyle.getPropertyValue(name),
|
|
);
|
|
}
|
|
for (const name of portalVariableNamesRef.current) {
|
|
if (!nextNames.has(name)) portalRoot.style.removeProperty(name);
|
|
}
|
|
portalVariableNamesRef.current = nextNames;
|
|
};
|
|
const scheduleSync = () => {
|
|
if (frameId === null) frameId = requestAnimationFrame(syncVariables);
|
|
};
|
|
syncVariables();
|
|
const observer = new MutationObserver(scheduleSync);
|
|
let element: HTMLElement | null = root;
|
|
while (element) {
|
|
observer.observe(element, {
|
|
attributes: true,
|
|
attributeFilter: ['class', 'style', 'data-theme', 'lang'],
|
|
});
|
|
element = element.parentElement;
|
|
}
|
|
window.addEventListener('resize', scheduleSync);
|
|
return () => {
|
|
observer.disconnect();
|
|
window.removeEventListener('resize', scheduleSync);
|
|
if (frameId !== null) cancelAnimationFrame(frameId);
|
|
};
|
|
}, [portalRoot, resolvedLanguage, rootClassName, rootStyle, theme]);
|
|
|
|
return (
|
|
<ThemeProvider value={theme}>
|
|
<I18nProvider language={resolvedLanguage}>
|
|
<McpAppHostContext.Provider value={mcpAppBaseUrl}>
|
|
<WebShellPortalRootContext.Provider value={portalRoot}>
|
|
<TranscriptRenderModeProvider value="readonly">
|
|
<WebShellCustomizationProvider value={customization}>
|
|
<TodoTimelineContext.Provider value={todoTimeline}>
|
|
<TodoDetailContext.Provider value={todoDetails}>
|
|
<CompactModeContext.Provider value={false}>
|
|
<div
|
|
ref={rootRef}
|
|
className={rootClassName}
|
|
style={rootStyle}
|
|
data-web-shell-root
|
|
data-web-shell-shadcn
|
|
lang={resolvedLanguage}
|
|
>
|
|
<div
|
|
className={`${styles.content} ${styles.contentHasMessages}`}
|
|
>
|
|
<MessageList
|
|
messages={messages}
|
|
pendingApproval={null}
|
|
isResponding={false}
|
|
workspaceCwd={workspaceCwd}
|
|
virtualScrollThreshold={virtualScrollThreshold}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CompactModeContext.Provider>
|
|
</TodoDetailContext.Provider>
|
|
</TodoTimelineContext.Provider>
|
|
</WebShellCustomizationProvider>
|
|
</TranscriptRenderModeProvider>
|
|
</WebShellPortalRootContext.Provider>
|
|
</McpAppHostContext.Provider>
|
|
</I18nProvider>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
export function WebShellTranscript(
|
|
props: WebShellTranscriptProps,
|
|
): ReactElement {
|
|
const language = resolveLanguage(props.language);
|
|
return (
|
|
<ErrorBoundary
|
|
label="web-shell-transcript-root"
|
|
resetKeys={[props.blocks, language]}
|
|
fallback={(error, reset) => (
|
|
<RootErrorFallback error={error} onRetry={reset} language={language} />
|
|
)}
|
|
>
|
|
<WebShellTranscriptContent {...props} />
|
|
</ErrorBoundary>
|
|
);
|
|
}
|