import { getSourceEntries } from './sources/sourceEntries'; import '../styles/globals.css'; import 'katex/dist/katex.min.css'; import { useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type ReactElement, } from 'react'; import type { DaemonTranscriptBlock, SessionSource, DaemonSessionAttachmentReference, } from '@qwen-code/sdk/daemon'; import { CompactModeContext, TodoContextsProvider } from '../WebShellContexts'; import { WebShellCustomizationProvider, type AssistantTurnFooterRenderer, type WebShellArtifactCustomization, type ComposerTagRenderer, type MarkdownTableMode, type ToolHeaderExtraRenderer, type UserMessageContentParser, type UserMessageContentRenderer, type WebShellComposerTagIconMap, type WebShellMarkdownCustomization, type WebShellSource, type WebShellSourceReference, type WebShellSourceIconResolver, } 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 '../adapters/localizedMessages'; import { WebShellPortalRootContext } from '../portalRoot'; import { computeTodoDetails, computeTodoTimeline } from '../utils/todos'; import { ThemeProvider, WebShellThemeId, type WebShellTheme, } from '../themeContext'; import { TranscriptDocumentExpandedProvider, 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[]; renderMode?: 'readonly' | 'document'; theme?: WebShellTheme; language?: 'en' | 'zh-CN' | 'zh' | 'zh-cn' | 'ru' | 'ru-ru'; className?: string; style?: CSSProperties; chatMaxWidth?: number; workspaceCwd?: string; compactThinking?: boolean; documentExpanded?: boolean; collapseCompletedTurns?: boolean; markdownTableMode?: MarkdownTableMode; virtualScrollThreshold?: number; markdown?: WebShellMarkdownCustomization; sourceSessionId?: string; sources?: readonly SessionSource[]; sourceAttachments?: readonly DaemonSessionAttachmentReference[]; sourceReferences?: readonly WebShellSourceReference[]; getAssistantSourcesIcon?: WebShellSourceIconResolver; onSourceOpen?: (source: WebShellSource) => void; composerTagIcons?: WebShellComposerTagIconMap; renderToolHeaderExtra?: ToolHeaderExtraRenderer; artifact?: WebShellArtifactCustomization; 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, renderMode = 'readonly', theme = WebShellThemeId.Dark, language, className, style, chatMaxWidth, workspaceCwd = '', compactThinking = false, documentExpanded = true, collapseCompletedTurns, markdownTableMode = 'basic', virtualScrollThreshold, markdown, sourceSessionId, sources, sourceAttachments, sourceReferences, getAssistantSourcesIcon, onSourceOpen, composerTagIcons, artifact, renderToolHeaderExtra, parseUserMessageContent, renderUserMessageContent, renderComposerTag, renderComposerTagTooltip, renderAssistantTurnFooter, mcpAppBaseUrl, }: WebShellTranscriptProps): ReactElement { const documentMode = renderMode === 'document'; const effectiveCollapseCompletedTurns = !documentMode && (collapseCompletedTurns ?? true); const effectiveMarkdownTableMode = documentMode ? 'basic' : markdownTableMode; const resolvedLanguage = resolveLanguage(language); const t = useMemo(() => getTranslator(resolvedLanguage), [resolvedLanguage]); const messages = useMemo( () => transcriptBlocksToLocalizedMessages(blocks, t, documentMode), [blocks, documentMode, t], ); const sourceEntries = useMemo( () => getSourceEntries(sources ?? [], sourceAttachments ?? []), [sources, sourceAttachments], ); const todoDetails = useMemo(() => computeTodoDetails(messages), [messages]); const todoTimeline = useMemo(() => computeTodoTimeline(messages), [messages]); const customization = useMemo( () => ({ artifact, composerTagIcons, renderToolHeaderExtra, parseUserMessageContent, renderUserMessageContent, renderComposerTag, renderComposerTagTooltip, renderAssistantTurnFooter, compactThinking, collapseCompletedTurns: effectiveCollapseCompletedTurns, markdownTableMode: effectiveMarkdownTableMode, markdown, sourceReferences, getAssistantSourcesIcon, }), [ artifact, effectiveCollapseCompletedTurns, compactThinking, composerTagIcons, markdown, sourceReferences, getAssistantSourcesIcon, effectiveMarkdownTableMode, parseUserMessageContent, renderAssistantTurnFooter, renderComposerTag, renderComposerTagTooltip, renderToolHeaderExtra, renderUserMessageContent, ], ); const rootRef = useRef(null); const [portalRoot, setPortalRoot] = useState(null); const portalVariableNamesRef = useRef>(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(); 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 ( {/* Embedded read-only transcript API (not the main chat): keep tool groups unmerged so hosts see the full record, independent of the main app's always-on compact view. */}
); } export function WebShellTranscript( props: WebShellTranscriptProps, ): ReactElement { const language = resolveLanguage(props.language); return ( ( )} > ); }