import { createContext, useContext, type ComponentType, type ReactNode, } from 'react'; import type { Components, Options } from 'react-markdown'; import type { DaemonInputAnnotation } from '@qwen-code/sdk/daemon'; import type { DaemonStreamingState } from '@qwen-code/webui/daemon-react-sdk'; import type { ACPToolCall } from './adapters/types'; import type { WelcomeHeaderProps } from './components/WelcomeHeader'; import type { WebShellTheme } from './themeContext'; export type MarkdownContentSource = 'assistant' | 'thinking'; export interface MarkdownRenderContext { source: MarkdownContentSource; } export interface WebShellCodeBlockRenderInfo { /** * Raw fenced-code language from the markdown class name, restricted to safe * fence-language characters. */ language: string; /** * Canonical Shiki language id after applying built-in aliases, or `text` * when the language is unsupported by the fallback highlighter. */ resolvedLanguage: string; className?: string; code: string; /** True while the assistant message is still streaming partial content. */ isStreaming: boolean; source: MarkdownContentSource; theme: WebShellTheme; } /** * Return a React node to replace the default code block rendering. Return * `null`, `undefined`, or `false` to decline and fall back to the built-in code * block renderer. Expensive renderers should debounce or defer work while * `info.isStreaming` is true. */ export type CodeBlockRenderer = ( info: WebShellCodeBlockRenderInfo, ) => ReactNode | null | undefined; export interface WebShellMarkdownCustomization { transformMarkdown?: ( markdown: string, context: MarkdownRenderContext, ) => string; renderCodeBlock?: CodeBlockRenderer; /** * Custom markdown components override Web Shell's built-ins. In particular, * `components.code` replaces the default code renderer, so `renderCodeBlock` * will not be called for that source. */ components?: Components; remarkPlugins?: Options['remarkPlugins']; rehypePlugins?: Options['rehypePlugins']; } export type MarkdownTableMode = 'basic' | 'advanced'; export type ToolHeaderKind = | 'agent' | 'ask' | 'edit' | 'fetch' | 'read' | 'shell' | 'todo' | 'write' | 'other'; export interface ToolHeaderExtraRenderInfo { kind: ToolHeaderKind; tool: ACPToolCall; displayName: string; description: string; elapsed: string; workspaceCwd?: string; } export type ToolHeaderExtraRenderer = ( info: ToolHeaderExtraRenderInfo, ) => ReactNode; export type WelcomeHeaderRenderer = (props: WelcomeHeaderProps) => ReactNode; export type WelcomeFooterRenderer = (props: WelcomeHeaderProps) => ReactNode; /** Context passed to the chat header renderer. */ export interface ChatHeaderRenderInfo { /** Current session id, if connected. */ sessionId?: string; /** Display name for the current session. */ sessionName?: string; /** Workspace cwd for the current session. */ workspaceCwd?: string; } /** * Custom renderer shown at the top of the chat view, above the message list. * Only rendered when a session is active (not in the welcome/empty state). */ export type ChatHeaderRenderer = (info: ChatHeaderRenderInfo) => ReactNode; export interface UserMessageContentRenderInfo { content: string; images?: readonly { data: string; mimeType: string }[]; inputAnnotations?: readonly DaemonInputAnnotation[]; } export type UserMessageContentRenderer = ( info: UserMessageContentRenderInfo, ) => ReactNode; export interface WebShellBottomStatusItem { id: string; label: ReactNode; title?: string; ariaLabel?: string; onClick?: () => void; } export type WebShellIconSource = string; export interface WebShellAssistantMessageInfo { id: string; content: string; isStreaming?: boolean; timestamp?: number; } export interface WebShellAssistantTurnFooterRenderInfo { /** User-message id for the head of the completed turn. */ turnId: string; message: WebShellAssistantMessageInfo; } export type AssistantTurnFooterRenderer = ( info: WebShellAssistantTurnFooterRenderInfo, ) => ReactNode | null | undefined; export type WebShellBuiltinComposerTagKind = | 'extension' | 'mcp' | 'file' | 'skill'; export type WebShellComposerTagKind = | WebShellBuiltinComposerTagKind | (string & {}); export type WebShellComposerTagIconMap = Readonly>; export interface WebShellComposerTag { id: string; label?: string; value?: string; removable?: boolean; kind?: WebShellComposerTagKind; icon?: WebShellIconSource; metadata?: unknown; serialized?: string; } export type WebShellComposerTagPlacementContext = 'composer' | 'user-message'; export interface WebShellComposerTagRenderInfo { tag: WebShellComposerTag; placement: WebShellComposerTagPlacementContext; readonly: boolean; anchorRect?: DOMRectReadOnly; } /** * Custom composer tag content. Inline composer tags are mounted from * CodeMirror-managed React roots, so JSX returned for inline tags must not * depend on React context from the surrounding app tree. */ export type ComposerTagRenderer = ( info: WebShellComposerTagRenderInfo, ) => ReactNode | null | undefined; export type ComposerTagClickHandler = ( info: WebShellComposerTagRenderInfo, ) => void; export type WebShellUserMessagePart = | { type: 'text'; text: string } | { type: 'tag'; tag: WebShellComposerTag; sourceRange?: readonly [number, number]; }; export type UserMessageContentParser = ( content: string, ) => readonly WebShellUserMessagePart[] | undefined | null; export type WebShellComposerTagPlacement = 'top' | 'inline'; export interface WebShellComposerTagOptions { placement?: WebShellComposerTagPlacement; } export interface WebShellComposerTextOptions { mode?: 'append' | 'replace'; } export interface WebShellComposerInput { text?: string; tags?: readonly WebShellComposerTag[]; tagPlacement?: WebShellComposerTagPlacement; submit?: boolean; } export interface WebShellAtItem { id: string; label: string; subtitle?: string; description?: string; detail?: string; icon?: WebShellIconSource; iconMode?: 'mask' | 'image'; iconColor?: string; iconSpin?: boolean; iconTooltip?: string; insertText?: string; composerTag?: WebShellComposerTag; } export type WebShellBuiltinAtProviderId = | 'files' | 'extensions' | 'mcp-resources'; export type WebShellBuiltinAtProvidersConfig = | boolean | readonly WebShellBuiltinAtProviderId[] | { enabled?: boolean; include?: readonly WebShellBuiltinAtProviderId[]; exclude?: readonly WebShellBuiltinAtProviderId[]; }; export interface WebShellAtProviderTab { id: string; label: ReactNode; textValue?: string; disabled?: boolean; } export interface WebShellAtItemRenderInfo { item: WebShellAtItem; provider: WebShellAtProvider; selected: boolean; } export type WebShellAtItemRenderer = ( info: WebShellAtItemRenderInfo, ) => ReactNode | null | undefined; export interface WebShellAtProvider { id: string; label: ReactNode; textValue?: string; description?: string; order?: number; tabs?: readonly WebShellAtProviderTab[]; renderItem?: WebShellAtItemRenderer; search(params: { query: string; signal: AbortSignal; tabId?: string; }): Promise; } export interface WebShellComposerApi { insertText(text: string, options?: WebShellComposerTextOptions): void; setText(text: string): void; addTags( tags: readonly WebShellComposerTag[], options?: WebShellComposerTagOptions, ): void; removeTag(id: string): void; /** Clears text and/or top tags. Inline tags are part of the editor text. */ clear(options?: { text?: boolean; tags?: boolean }): void; submit(input?: WebShellComposerInput): void; } export interface WebShellComposerToolbarRenderInfo { disabled: boolean; isRunning: boolean; currentMode: string; currentModel: string; sessionName?: string; } export type WebShellComposerToolbarStartRenderInfo = WebShellComposerToolbarRenderInfo; export type WebShellComposerToolbarRightRenderInfo = WebShellComposerToolbarRenderInfo; export type ComposerToolbarStartRenderer = ComponentType; export type ComposerToolbarEndRenderer = ComponentType; export type ComposerToolbarRightRenderer = ComponentType; export type ComposerHeaderRenderer = ComponentType; export type ComposerFooterRenderer = ComponentType; // ---- Background task info (public type for footer renderer) ---- interface WebShellTaskBase { id: string; label: string; description: string; runtimeMs: number; startTime: number; endTime?: number; error?: string; } export interface WebShellAgentTask extends WebShellTaskBase { kind: 'agent'; status: 'running' | 'paused' | 'completed' | 'failed' | 'cancelled'; subagentType?: string; isBackgrounded: boolean; prompt?: string; } export interface WebShellShellTask extends WebShellTaskBase { kind: 'shell'; status: 'running' | 'completed' | 'failed' | 'cancelled'; command: string; cwd: string; pid?: number; exitCode?: number; } export interface WebShellMonitorTask extends WebShellTaskBase { kind: 'monitor'; status: 'running' | 'completed' | 'failed' | 'cancelled'; command: string; pid?: number; exitCode?: number; } export type WebShellTaskInfo = | WebShellAgentTask | WebShellShellTask | WebShellMonitorTask; // ---- Model info (public type for footer renderer) ---- export interface WebShellModelInfo { id: string; label?: string; contextWindow?: number; } // ---- Skill info (public type for footer renderer) ---- export interface WebShellSkillInfo { name: string; description: string; } // ---- Footer renderer ---- export interface WebShellFooterRenderInfo { connected: boolean; mode: string; model: string; streamingState: DaemonStreamingState; contextUsageRatio: number; activeGoal: { condition: string; setAt: number } | null; tasks: readonly WebShellTaskInfo[]; availableModes: readonly string[]; availableModels: readonly WebShellModelInfo[]; skills: readonly WebShellSkillInfo[]; onSelectMode: (mode: string) => void; onSelectModel: (model: string) => void; } export type FooterRenderer = ComponentType; // ---- Loading phrases ---- /** * Resolves the witty phrases cycled while a prompt is streaming. Receives the * resolved UI language. Return phrases to override the built-in defaults, an * empty array to hide the phrase entirely, or `undefined`/`null` to fall back * to the built-in defaults for that language. */ export type LoadingPhrasesResolver = ( language: string, ) => readonly string[] | undefined | null; export interface WebShellCustomization { renderToolHeaderExtra?: ToolHeaderExtraRenderer; renderWelcomeHeader?: WelcomeHeaderRenderer; renderWelcomeFooter?: WelcomeFooterRenderer; parseUserMessageContent?: UserMessageContentParser; renderUserMessageContent?: UserMessageContentRenderer; composerTagIcons?: WebShellComposerTagIconMap; renderComposerTag?: ComposerTagRenderer; renderComposerTagTooltip?: ComposerTagRenderer; onComposerTagClick?: ComposerTagClickHandler; renderAssistantTurnFooter?: AssistantTurnFooterRenderer; renderComposerToolbarStart?: ComposerToolbarStartRenderer; renderComposerToolbarEnd?: ComposerToolbarEndRenderer; renderComposerToolbarRight?: ComposerToolbarRightRenderer; renderComposerHeader?: ComposerHeaderRenderer; renderComposerFooter?: ComposerFooterRenderer; renderFooter?: FooterRenderer; compactThinking?: boolean; /** * Auto-collapse each completed turn's intermediate steps (thinking, tool * calls, mid-turn assistant text) behind a toggle on the prompt row, leaving * just the prompt and the final answer. The active turn always stays * expanded. Defaults to enabled when unset. */ collapseCompletedTurns?: boolean; markdownTableMode?: MarkdownTableMode; markdown?: WebShellMarkdownCustomization; loadingPhrases?: LoadingPhrasesResolver; } const WebShellCustomizationContext = createContext({}); export const WebShellCustomizationProvider = WebShellCustomizationContext.Provider; export function useWebShellCustomization(): WebShellCustomization { return useContext(WebShellCustomizationContext); }