qwen-code/packages/web-shell/client/customization.tsx

322 lines
8.6 KiB
TypeScript

import {
createContext,
useContext,
type ComponentType,
type ReactNode,
} from 'react';
import type { Components, Options } from 'react-markdown';
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;
export interface UserMessageContentRenderInfo {
content: string;
images?: readonly { data: string; mimeType: string }[];
}
export type UserMessageContentRenderer = (
info: UserMessageContentRenderInfo,
) => ReactNode;
export interface WebShellBottomStatusItem {
id: string;
label: ReactNode;
title?: string;
ariaLabel?: string;
onClick?: () => void;
}
export type WebShellBuiltinComposerTagKind =
| 'extension'
| 'mcp'
| 'file'
| 'skill';
export type WebShellComposerTagKind =
| WebShellBuiltinComposerTagKind
| (string & {});
export type WebShellComposerTagIconMap = Readonly<Record<string, string>>;
export interface WebShellComposerTag {
id: string;
label?: string;
value?: string;
removable?: boolean;
kind?: WebShellComposerTagKind;
serialized?: string;
}
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;
description?: string;
detail?: string;
insertText?: string;
composerTag?: WebShellComposerTag;
}
export interface WebShellAtProvider {
id: string;
label: string;
description?: string;
order?: number;
search(params: {
query: string;
signal: AbortSignal;
}): Promise<readonly WebShellAtItem[]>;
}
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<WebShellComposerToolbarStartRenderInfo>;
export type ComposerToolbarEndRenderer =
ComponentType<WebShellComposerToolbarRenderInfo>;
export type ComposerToolbarRightRenderer =
ComponentType<WebShellComposerToolbarRightRenderInfo>;
// ---- 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<WebShellFooterRenderInfo>;
// ---- 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;
renderUserMessageContent?: UserMessageContentRenderer;
renderComposerToolbarStart?: ComposerToolbarStartRenderer;
renderComposerToolbarEnd?: ComposerToolbarEndRenderer;
renderComposerToolbarRight?: ComposerToolbarRightRenderer;
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<WebShellCustomization>({});
export const WebShellCustomizationProvider =
WebShellCustomizationContext.Provider;
export function useWebShellCustomization(): WebShellCustomization {
return useContext(WebShellCustomizationContext);
}