feat(webui): add webview container and isolate styles for VSCode integration

- Introduce WebviewContainer component for style isolation in VSCode webviews
- Rename CSS variables from --app-* to --qwen-app-* to prevent conflicts
- Add dedicated webview.css with isolated styles
- Update exports to include webview.css in package
- Modify all components to use new CSS variable names
- Update VSCode IDE companion to use new webview container
- Add style isolation to prevent conflicts with VSCode environment

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Fixes webview UI issues in VSCode IDE Companion by providing proper style encapsulation.
This commit is contained in:
yiliang114 2026-01-17 10:53:32 +08:00
parent 9a47ad5e62
commit ff43a278dc
64 changed files with 564 additions and 216 deletions

View file

@ -5,10 +5,17 @@
*/
import esbuild from 'esbuild';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '..', '..');
const rootRequire = createRequire(resolve(repoRoot, 'package.json'));
/**
* @type {import('esbuild').Plugin}
*/
@ -31,6 +38,42 @@ const esbuildProblemMatcherPlugin = {
},
};
/**
* Ensure a single React copy in the webview bundle by resolving from repo root.
* Prevents mixing React 18/19 element types when nested node_modules exist.
* @type {import('esbuild').Plugin}
*/
const resolveFromRoot = (moduleId) => {
try {
return rootRequire.resolve(moduleId);
} catch {
return null;
}
};
const reactDedupPlugin = {
name: 'react-dedup',
setup(build) {
const aliases = [
'react',
'react-dom',
'react-dom/client',
'react/jsx-runtime',
'react/jsx-dev-runtime',
];
for (const alias of aliases) {
build.onResolve({ filter: new RegExp(`^${alias}$`) }, () => {
const resolved = resolveFromRoot(alias);
if (!resolved) {
return undefined;
}
return { path: resolved };
});
}
},
};
/**
* @type {import('esbuild').Plugin}
*/
@ -128,7 +171,7 @@ async function main() {
platform: 'browser',
outfile: 'dist/webview.js',
logLevel: 'silent',
plugins: [cssInjectPlugin, esbuildProblemMatcherPlugin],
plugins: [reactDedupPlugin, cssInjectPlugin, esbuildProblemMatcherPlugin],
jsx: 'automatic', // Use new JSX transform (React 17+)
define: {
'process.env.NODE_ENV': production ? '"production"' : '"development"',

View file

@ -29,6 +29,7 @@ export class WebViewProvider {
private pendingPermissionResolve: ((optionId: string) => void) | null = null;
// Track current ACP mode id to influence permission/diff behavior
private currentModeId: ApprovalModeValue | null = null;
private authState: boolean | null = null;
constructor(
private context: vscode.ExtensionContext,
@ -416,6 +417,10 @@ export class WebViewProvider {
if (message.type === 'openDiff' && this.isAutoMode()) {
return;
}
if (message.type === 'webviewReady') {
this.handleWebviewReady();
return;
}
// Allow webview to request updating the VS Code tab title
if (message.type === 'updatePanelTitle') {
const title = String(
@ -874,10 +879,72 @@ export class WebViewProvider {
}
}
/**
* Track authentication state based on outbound messages to the webview.
*/
private updateAuthStateFromMessage(message: unknown): void {
if (!message || typeof message !== 'object') {
return;
}
const msg = message as {
type?: string;
data?: { authenticated?: boolean | null };
};
switch (msg.type) {
case 'authState':
if (typeof msg.data?.authenticated === 'boolean') {
this.authState = msg.data.authenticated;
} else {
this.authState = null;
}
break;
case 'agentConnected':
case 'loginSuccess':
this.authState = true;
break;
case 'agentConnectionError':
case 'loginError':
this.authState = false;
break;
default:
break;
}
}
/**
* Sync important initialization state when the webview signals readiness.
*/
private handleWebviewReady(): void {
if (this.currentModeId) {
this.sendMessageToWebView({
type: 'modeChanged',
data: { modeId: this.currentModeId },
});
}
if (typeof this.authState === 'boolean') {
this.sendMessageToWebView({
type: 'authState',
data: { authenticated: this.authState },
});
return;
}
if (this.agentInitialized) {
const authenticated = Boolean(this.agentManager.currentSessionId);
this.sendMessageToWebView({
type: 'authState',
data: { authenticated },
});
}
}
/**
* Send message to WebView
*/
private sendMessageToWebView(message: unknown): void {
this.updateAuthStateFromMessage(message);
const panel = this.panelManager.getPanel();
panel?.webview.postMessage(message);
}
@ -983,6 +1050,7 @@ export class WebViewProvider {
resetAgentState(): void {
console.log('[WebViewProvider] Resetting agent state');
this.agentInitialized = false;
this.authState = null;
// Disconnect existing connection
this.agentManager.disconnect();
}
@ -1017,6 +1085,10 @@ export class WebViewProvider {
if (message.type === 'openDiff' && this.isAutoMode()) {
return;
}
if (message.type === 'webviewReady') {
this.handleWebviewReady();
return;
}
if (message.type === 'updatePanelTitle') {
const title = String(
(message.data as { title?: unknown } | undefined)?.title ?? '',
@ -1174,6 +1246,7 @@ export class WebViewProvider {
console.log('[WebViewProvider] Restoring state:', state);
this.messageHandler.setCurrentConversationId(state.conversationId);
this.agentInitialized = state.agentInitialized;
this.authState = null;
console.log(
'[WebViewProvider] State restored. agentInitialized:',
this.agentInitialized,

View file

@ -7,7 +7,7 @@
* This allows local ApprovalModeValue to work with webui's EditModeInfo
*/
import type React from 'react';
import type { FC } from 'react';
import { InputForm as BaseInputForm, getEditModeIcon } from '@qwen-code/webui';
import type {
InputFormProps as BaseInputFormProps,
@ -44,7 +44,7 @@ const getEditModeInfo = (editMode: ApprovalModeValue): EditModeInfo => {
* This is an adapter that accepts the local ApprovalModeValue type
* and converts it to webui's EditModeInfo format.
*/
export const InputForm: React.FC<InputFormProps> = ({ editMode, ...rest }) => {
export const InputForm: FC<InputFormProps> = ({ editMode, ...rest }) => {
const editModeInfo = getEditModeInfo(editMode);
return <BaseInputForm editModeInfo={editModeInfo} {...rest} />;

View file

@ -7,7 +7,7 @@
* Uses webui Onboarding component with platform-specific icon URL
*/
import type React from 'react';
import type { FC } from 'react';
import { Onboarding as BaseOnboarding } from '@qwen-code/webui';
import { generateIconUrl } from '../../utils/resourceUrl.js';
@ -19,7 +19,7 @@ interface OnboardingPageProps {
* VSCode Onboarding wrapper
* Provides platform-specific icon URL to the webui Onboarding component
*/
export const Onboarding: React.FC<OnboardingPageProps> = ({ onLogin }) => {
export const Onboarding: FC<OnboardingPageProps> = ({ onLogin }) => {
const iconUri = generateIconUrl('icon.png');
return <BaseOnboarding iconUrl={iconUri} onGetStarted={onLogin} />;

View file

@ -9,7 +9,7 @@
* It re-exports the router and types from the toolcalls module.
*/
import type React from 'react';
import type { FC } from 'react';
import type { ToolCallData } from '@qwen-code/webui';
import { ToolCallRouter } from './index.js';
@ -20,7 +20,7 @@ export type {
ToolCallContent,
} from '@qwen-code/webui';
export const ToolCall: React.FC<{
export const ToolCall: FC<{
toolCall: ToolCallData;
isFirst?: boolean;
isLast?: boolean;

View file

@ -7,7 +7,7 @@
* All UI components are now imported from @qwen-code/webui
*/
import type React from 'react';
import type { FC } from 'react';
import {
shouldShowToolCall,
// All ToolCall components from webui
@ -25,9 +25,7 @@ import type { BaseToolCallProps } from '@qwen-code/webui';
/**
* Factory function that returns the appropriate tool call component based on kind
*/
export const getToolCallComponent = (
kind: string,
): React.FC<BaseToolCallProps> => {
export const getToolCallComponent = (kind: string): FC<BaseToolCallProps> => {
const normalizedKind = kind.toLowerCase();
// Route to specialized components

View file

@ -7,8 +7,8 @@
* This allows webui components to work with VSCode's messaging system
*/
import type React from 'react';
import { useMemo, useCallback, useEffect, useRef } from 'react';
import type { FC, ReactNode } from 'react';
import { PlatformProvider } from '@qwen-code/webui';
import type { PlatformContextValue } from '@qwen-code/webui';
import { useVSCode } from '../hooks/useVSCode.js';
@ -18,7 +18,7 @@ import { generateIconUrl } from '../utils/resourceUrl.js';
* Props for VSCodePlatformProvider
*/
interface VSCodePlatformProviderProps {
children: React.ReactNode;
children: ReactNode;
}
/**
@ -27,7 +27,7 @@ interface VSCodePlatformProviderProps {
* This component bridges the VSCode API with the platform-agnostic webui components.
* It wraps children with PlatformProvider and provides VSCode-specific implementations.
*/
export const VSCodePlatformProvider: React.FC<VSCodePlatformProviderProps> = ({
export const VSCodePlatformProvider: FC<VSCodePlatformProviderProps> = ({
children,
}) => {
const vscode = useVSCode();

View file

@ -886,6 +886,8 @@ export const useWebViewMessages = ({
useEffect(() => {
window.addEventListener('message', handleMessage);
// Notify extension that the webview is ready to receive initialization state.
vscode.postMessage({ type: 'webviewReady', data: {} });
return () => window.removeEventListener('message', handleMessage);
}, [handleMessage]);
}, [handleMessage, vscode]);
};

View file

@ -15,7 +15,9 @@ export default {
content: [
'./src/webview/**/**/*.{js,jsx,ts,tsx}',
// Include webui components to prevent Tailwind JIT from tree-shaking their classes
'./node_modules/@qwen-code/webui/dist/**/*.js',
// Use relative path for pnpm workspace - node_modules symlinks are in root
'../webui/src/**/*.{js,jsx,ts,tsx}',
'../webui/dist/**/*.js',
],
theme: {
extend: {

View file

@ -18,7 +18,8 @@
"require": "./dist/components/icons/index.cjs"
},
"./tailwind.preset": "./tailwind.preset.cjs",
"./styles.css": "./dist/styles.css"
"./styles.css": "./dist/styles.css",
"./webview.css": "./dist/webview.css"
},
"files": [
"dist",

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { useEffect, useRef, useState } from 'react';
import type { FC, RefObject } from 'react';
export interface PermissionOption {
name: string;
@ -41,7 +41,7 @@ export interface PermissionDrawerProps {
onClose?: () => void;
}
const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
const PermissionDrawer: FC<PermissionDrawerProps> = ({
isOpen,
options,
toolCall,
@ -290,10 +290,10 @@ interface CustomMessageInputRowProps {
setCustomMessage: (val: string) => void;
onFocusRow: () => void;
onSubmitReject: () => void;
inputRef: React.RefObject<HTMLInputElement | null>;
inputRef: RefObject<HTMLInputElement | null>;
}
const CustomMessageInputRow: React.FC<CustomMessageInputRowProps> = ({
const CustomMessageInputRow: FC<CustomMessageInputRowProps> = ({
isFocused,
customMessage,
setCustomMessage,
@ -309,7 +309,7 @@ const CustomMessageInputRow: React.FC<CustomMessageInputRowProps> = ({
onClick={() => inputRef.current?.focus()}
>
<input
ref={inputRef as React.LegacyRef<HTMLInputElement> | undefined}
ref={inputRef as unknown as RefObject<HTMLInputElement>}
type="text"
placeholder="Tell Qwen what to do instead"
spellCheck={false}

View file

@ -0,0 +1,17 @@
import type { PropsWithChildren } from 'react';
import type React from 'react';
interface WebviewContainerProps extends PropsWithChildren {
className?: string;
}
/**
* A container component that provides style isolation for VSCode webviews
* This component wraps content in a namespace to prevent style conflicts
*/
const WebviewContainer: React.FC<WebviewContainerProps> = ({
children,
className = '',
}) => <div className={`qwen-webui-container ${className}`}>{children}</div>;
export default WebviewContainer;

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
interface CloseIconProps {
size?: number;
@ -12,7 +12,7 @@ interface CloseIconProps {
className?: string;
}
const CloseIcon: React.FC<CloseIconProps> = ({
const CloseIcon: FC<CloseIconProps> = ({
size = 24,
color = 'currentColor',
className = '',

View file

@ -6,14 +6,14 @@
* Edit mode related icons
*/
import type React from 'react';
import type { FC } from 'react';
import type { IconProps } from './types.js';
/**
* Edit pencil icon (16x16)
* Used for "Ask before edits" mode
*/
export const EditPencilIcon: React.FC<IconProps> = ({
export const EditPencilIcon: FC<IconProps> = ({
size = 16,
className,
...props
@ -40,7 +40,7 @@ export const EditPencilIcon: React.FC<IconProps> = ({
* Auto/fast-forward icon (16x16)
* Used for "Edit automatically" mode
*/
export const AutoEditIcon: React.FC<IconProps> = ({
export const AutoEditIcon: FC<IconProps> = ({
size = 16,
className,
...props
@ -63,7 +63,7 @@ export const AutoEditIcon: React.FC<IconProps> = ({
* Plan mode/bars icon (16x16)
* Used for "Plan mode"
*/
export const PlanModeIcon: React.FC<IconProps> = ({
export const PlanModeIcon: FC<IconProps> = ({
size = 16,
className,
...props
@ -86,7 +86,7 @@ export const PlanModeIcon: React.FC<IconProps> = ({
* Code brackets icon (20x20)
* Used for active file indicator
*/
export const CodeBracketsIcon: React.FC<IconProps> = ({
export const CodeBracketsIcon: FC<IconProps> = ({
size = 20,
className,
...props
@ -113,7 +113,7 @@ export const CodeBracketsIcon: React.FC<IconProps> = ({
* Hide context (eye slash) icon (20x20)
* Used to indicate the active selection will NOT be auto-loaded into context
*/
export const HideContextIcon: React.FC<IconProps> = ({
export const HideContextIcon: FC<IconProps> = ({
size = 20,
className,
...props
@ -141,7 +141,7 @@ export const HideContextIcon: React.FC<IconProps> = ({
* Slash command icon (20x20)
* Used for command menu button
*/
export const SlashCommandIcon: React.FC<IconProps> = ({
export const SlashCommandIcon: FC<IconProps> = ({
size = 20,
className,
...props
@ -168,11 +168,7 @@ export const SlashCommandIcon: React.FC<IconProps> = ({
* Link/attachment icon (20x20)
* Used for attach context button
*/
export const LinkIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
export const LinkIcon: FC<IconProps> = ({ size = 20, className, ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
@ -195,7 +191,7 @@ export const LinkIcon: React.FC<IconProps> = ({
* Open diff icon (16x16)
* Used for opening diff in VS Code
*/
export const OpenDiffIcon: React.FC<IconProps> = ({
export const OpenDiffIcon: FC<IconProps> = ({
size = 16,
className,
...props
@ -218,11 +214,7 @@ export const OpenDiffIcon: React.FC<IconProps> = ({
* Undo edit icon (16x16)
* Used for undoing edits in diff views
*/
export const UndoIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
export const UndoIcon: FC<IconProps> = ({ size = 16, className, ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
@ -247,11 +239,7 @@ export const UndoIcon: React.FC<IconProps> = ({
* Redo edit icon (16x16)
* Used for redoing edits in diff views
*/
export const RedoIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
export const RedoIcon: FC<IconProps> = ({ size = 16, className, ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
@ -276,7 +264,7 @@ export const RedoIcon: React.FC<IconProps> = ({
* Replace all icon (16x16)
* Used for replacing all occurrences in search/replace
*/
export const ReplaceAllIcon: React.FC<IconProps> = ({
export const ReplaceAllIcon: FC<IconProps> = ({
size = 16,
className,
...props
@ -305,11 +293,7 @@ export const ReplaceAllIcon: React.FC<IconProps> = ({
* Copy icon (16x16)
* Used for copying content
*/
export const CopyIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
export const CopyIcon: FC<IconProps> = ({ size = 16, className, ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
@ -342,7 +326,7 @@ export const CopyIcon: React.FC<IconProps> = ({
* Paste icon (16x16)
* Used for pasting content
*/
export const PasteIcon: React.FC<IconProps> = ({
export const PasteIcon: FC<IconProps> = ({
size = 16,
className,
...props
@ -371,7 +355,7 @@ export const PasteIcon: React.FC<IconProps> = ({
* Select all icon (16x16)
* Used for selecting all content
*/
export const SelectAllIcon: React.FC<IconProps> = ({
export const SelectAllIcon: FC<IconProps> = ({
size = 16,
className,
...props

View file

@ -6,18 +6,14 @@
* File and document related icons
*/
import type React from 'react';
import type { FC } from 'react';
import type { IconProps } from './types.js';
/**
* File document icon (16x16)
* Used for file completion menu
*/
export const FileIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
export const FileIcon: FC<IconProps> = ({ size = 16, className, ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
@ -32,7 +28,7 @@ export const FileIcon: React.FC<IconProps> = ({
</svg>
);
export const FileListIcon: React.FC<IconProps> = ({
export const FileListIcon: FC<IconProps> = ({
size = 16,
className,
...props
@ -55,7 +51,7 @@ export const FileListIcon: React.FC<IconProps> = ({
* Save document icon (16x16)
* Used for save session button
*/
export const SaveDocumentIcon: React.FC<IconProps> = ({
export const SaveDocumentIcon: FC<IconProps> = ({
size = 16,
className,
...props
@ -83,7 +79,7 @@ export const SaveDocumentIcon: React.FC<IconProps> = ({
* Folder icon (16x16)
* Useful for directory entries in completion lists
*/
export const FolderIcon: React.FC<IconProps> = ({
export const FolderIcon: FC<IconProps> = ({
size = 16,
className,
...props

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
interface IconProps {
name: string;
@ -13,7 +13,7 @@ interface IconProps {
className?: string;
}
const Icon: React.FC<IconProps> = ({
const Icon: FC<IconProps> = ({
name,
size = 24,
color = 'currentColor',

View file

@ -6,14 +6,14 @@
* Navigation and action icons
*/
import type React from 'react';
import type { FC } from 'react';
import type { IconProps } from './types.js';
/**
* Chevron down icon (20x20)
* Used for dropdown arrows
*/
export const ChevronDownIcon: React.FC<IconProps> = ({
export const ChevronDownIcon: FC<IconProps> = ({
size = 20,
className,
...props
@ -40,11 +40,7 @@ export const ChevronDownIcon: React.FC<IconProps> = ({
* Plus icon (20x20)
* Used for new session button
*/
export const PlusIcon: React.FC<IconProps> = ({
size = 20,
className,
...props
}) => (
export const PlusIcon: FC<IconProps> = ({ size = 20, className, ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
@ -63,7 +59,7 @@ export const PlusIcon: React.FC<IconProps> = ({
* Small plus icon (16x16)
* Used for default attachment type
*/
export const PlusSmallIcon: React.FC<IconProps> = ({
export const PlusSmallIcon: FC<IconProps> = ({
size = 16,
className,
...props
@ -86,7 +82,7 @@ export const PlusSmallIcon: React.FC<IconProps> = ({
* Arrow up icon (20x20)
* Used for send message button
*/
export const ArrowUpIcon: React.FC<IconProps> = ({
export const ArrowUpIcon: FC<IconProps> = ({
size = 20,
className,
...props
@ -113,7 +109,7 @@ export const ArrowUpIcon: React.FC<IconProps> = ({
* Close X icon (14x14)
* Used for close buttons in banners and dialogs
*/
export const CloseIcon: React.FC<IconProps> = ({
export const CloseIcon: FC<IconProps> = ({
size = 14,
className,
...props
@ -137,7 +133,7 @@ export const CloseIcon: React.FC<IconProps> = ({
</svg>
);
export const CloseSmallIcon: React.FC<IconProps> = ({
export const CloseSmallIcon: FC<IconProps> = ({
size = 16,
className,
...props
@ -160,7 +156,7 @@ export const CloseSmallIcon: React.FC<IconProps> = ({
* Search/magnifying glass icon (20x20)
* Used for search input
*/
export const SearchIcon: React.FC<IconProps> = ({
export const SearchIcon: FC<IconProps> = ({
size = 20,
className,
...props
@ -187,7 +183,7 @@ export const SearchIcon: React.FC<IconProps> = ({
* Refresh/reload icon (16x16)
* Used for refresh session list
*/
export const RefreshIcon: React.FC<IconProps> = ({
export const RefreshIcon: FC<IconProps> = ({
size = 16,
className,
...props

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
interface SendIconProps {
size?: number;
@ -12,7 +12,7 @@ interface SendIconProps {
className?: string;
}
const SendIcon: React.FC<SendIconProps> = ({
const SendIcon: FC<SendIconProps> = ({
size = 24,
color = 'currentColor',
className = '',

View file

@ -6,7 +6,7 @@
* Special UI icons
*/
import type React from 'react';
import type { FC } from 'react';
import type { IconProps } from './types.js';
interface ThinkingIconProps extends IconProps {
@ -16,7 +16,7 @@ interface ThinkingIconProps extends IconProps {
enabled?: boolean;
}
export const ThinkingIcon: React.FC<ThinkingIconProps> = ({
export const ThinkingIcon: FC<ThinkingIconProps> = ({
size = 16,
className,
enabled = false,
@ -49,7 +49,7 @@ export const ThinkingIcon: React.FC<ThinkingIconProps> = ({
</svg>
);
export const TerminalIcon: React.FC<IconProps> = ({
export const TerminalIcon: FC<IconProps> = ({
size = 20,
className,
...props

View file

@ -6,14 +6,14 @@
* Status and state related icons
*/
import type React from 'react';
import type { FC } from 'react';
import type { IconProps } from './types.js';
/**
* Plan completed icon (14x14)
* Used for completed plan items
*/
export const PlanCompletedIcon: React.FC<IconProps> = ({
export const PlanCompletedIcon: FC<IconProps> = ({
size = 14,
className,
...props
@ -43,7 +43,7 @@ export const PlanCompletedIcon: React.FC<IconProps> = ({
* Plan in progress icon (14x14)
* Used for in-progress plan items
*/
export const PlanInProgressIcon: React.FC<IconProps> = ({
export const PlanInProgressIcon: FC<IconProps> = ({
size = 14,
className,
...props
@ -73,7 +73,7 @@ export const PlanInProgressIcon: React.FC<IconProps> = ({
* Plan pending icon (14x14)
* Used for pending plan items
*/
export const PlanPendingIcon: React.FC<IconProps> = ({
export const PlanPendingIcon: FC<IconProps> = ({
size = 14,
className,
...props
@ -103,7 +103,7 @@ export const PlanPendingIcon: React.FC<IconProps> = ({
* Warning triangle icon (20x20)
* Used for warning messages
*/
export const WarningTriangleIcon: React.FC<IconProps> = ({
export const WarningTriangleIcon: FC<IconProps> = ({
size = 20,
className,
...props
@ -130,11 +130,7 @@ export const WarningTriangleIcon: React.FC<IconProps> = ({
* User profile icon (16x16)
* Used for login command
*/
export const UserIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
export const UserIcon: FC<IconProps> = ({ size = 16, className, ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
@ -149,7 +145,7 @@ export const UserIcon: React.FC<IconProps> = ({
</svg>
);
export const SymbolIcon: React.FC<IconProps> = ({
export const SymbolIcon: FC<IconProps> = ({
size = 16,
className,
...props
@ -168,7 +164,7 @@ export const SymbolIcon: React.FC<IconProps> = ({
</svg>
);
export const SelectionIcon: React.FC<IconProps> = ({
export const SelectionIcon: FC<IconProps> = ({
size = 16,
className,
...props

View file

@ -6,18 +6,14 @@
* Stop icon for canceling operations
*/
import type React from 'react';
import type { FC } from 'react';
import type { IconProps } from './types.js';
/**
* Stop/square icon (16x16)
* Used for stop/cancel operations
*/
export const StopIcon: React.FC<IconProps> = ({
size = 16,
className,
...props
}) => (
export const StopIcon: FC<IconProps> = ({ size = 16, className, ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"

View file

@ -6,9 +6,9 @@
* Common icon props interface
*/
import type React from 'react';
import type { SVGProps } from 'react';
export interface IconProps extends React.SVGProps<SVGSVGElement> {
export interface IconProps extends SVGProps<SVGSVGElement> {
/**
* Icon size (width and height)
* @default 16

View file

@ -7,7 +7,7 @@
* Displays current session title with navigation controls
*/
import type React from 'react';
import type { FC } from 'react';
import { ChevronDownIcon } from '../icons/NavigationIcons.js';
import { PlusIcon } from '../icons/NavigationIcons.js';
@ -40,7 +40,7 @@ export interface ChatHeaderProps {
* />
* ```
*/
export const ChatHeader: React.FC<ChatHeaderProps> = ({
export const ChatHeader: FC<ChatHeaderProps> = ({
currentSessionTitle,
onLoadSessions,
onNewSession,

View file

@ -7,7 +7,7 @@
* Supports keyboard navigation and mouse interaction
*/
import type React from 'react';
import type { FC } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { CompletionItem } from '../../types/completion.js';
@ -49,7 +49,7 @@ export interface CompletionMenuProps {
* />
* ```
*/
export const CompletionMenu: React.FC<CompletionMenuProps> = ({
export const CompletionMenu: FC<CompletionMenuProps> = ({
items,
onSelect,
onClose,

View file

@ -4,14 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
interface ContainerProps {
children: React.ReactNode;
className?: string;
}
const Container: React.FC<ContainerProps> = ({ children, className = '' }) => (
const Container: FC<ContainerProps> = ({ children, className = '' }) => (
<div className={`container mx-auto px-4 ${className}`}>{children}</div>
);

View file

@ -7,7 +7,7 @@
* Displays token usage information with tooltip
*/
import type React from 'react';
import type { FC } from 'react';
import { Tooltip } from '../ui/Tooltip.js';
/**
@ -61,7 +61,7 @@ const formatNumber = (value: number): string => {
* />
* ```
*/
export const ContextIndicator: React.FC<ContextIndicatorProps> = ({
export const ContextIndicator: FC<ContextIndicatorProps> = ({
contextUsage,
}) => {
if (!contextUsage) {

View file

@ -7,7 +7,7 @@
* Shows logo and welcome message based on authentication state
*/
import type React from 'react';
import type { FC } from 'react';
import { usePlatform } from '../../context/PlatformContext.js';
/**
@ -41,7 +41,7 @@ export interface EmptyStateProps {
* />
* ```
*/
export const EmptyState: React.FC<EmptyStateProps> = ({
export const EmptyState: FC<EmptyStateProps> = ({
isAuthenticated = false,
loadingMessage,
logoUrl,

View file

@ -8,7 +8,7 @@
* Supports clicking to open files and jump to specified line and column numbers
*/
import type React from 'react';
import type { FC } from 'react';
import { usePlatform } from '../../context/PlatformContext.js';
/**
@ -77,7 +77,7 @@ function buildFullPath(
* <FileLink path="/src/components/Button.tsx" line={10} column={5} showFullPath={true} />
* ```
*/
export const FileLink: React.FC<FileLinkProps> = ({
export const FileLink: FC<FileLinkProps> = ({
path,
line,
column,

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
const Footer: React.FC = () => <footer>Footer Component Placeholder</footer>;
const Footer: FC = () => <footer>Footer Component Placeholder</footer>;
export default Footer;

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
const Header: React.FC = () => <header>Header Component Placeholder</header>;
const Header: FC = () => <header>Header Component Placeholder</header>;
export default Header;

View file

@ -5,7 +5,7 @@
*/
import type { Meta, StoryObj, StoryFn, Decorator } from '@storybook/react-vite';
import type React from 'react';
import type { FC } from 'react';
import { useRef } from 'react';
import { InputForm, getEditModeIcon } from './InputForm.js';
import type { InputFormProps } from './InputForm.js';
@ -15,7 +15,7 @@ type InputFormStoryProps = Omit<InputFormProps, 'inputFieldRef'>;
/**
* Wrapper component to provide inputFieldRef
*/
const InputFormWrapper: React.FC<InputFormStoryProps> = (props) => {
const InputFormWrapper: FC<InputFormStoryProps> = (props) => {
const inputFieldRef = useRef<HTMLDivElement>(null);
return <InputForm {...props} inputFieldRef={inputFieldRef} />;
};

View file

@ -7,7 +7,7 @@
* Platform-agnostic version with configurable edit modes
*/
import type React from 'react';
import type { FC } from 'react';
import type { ReactNode } from 'react';
import {
EditPencilIcon,
@ -144,7 +144,7 @@ export interface InputFormProps {
* />
* ```
*/
export const InputForm: React.FC<InputFormProps> = ({
export const InputForm: FC<InputFormProps> = ({
inputText,
inputFieldRef,
isStreaming,

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
const Main: React.FC = () => <main>Main Component Placeholder</main>;
const Main: FC = () => <main>Main Component Placeholder</main>;
export default Main;

View file

@ -7,7 +7,7 @@
* Platform-specific logic (icon URL) passed via props
*/
import type React from 'react';
import type { FC } from 'react';
export interface OnboardingProps {
/** URL of the application icon */
@ -26,7 +26,7 @@ export interface OnboardingProps {
* Onboarding - Welcome screen for new users
* Pure presentational component
*/
export const Onboarding: React.FC<OnboardingProps> = ({
export const Onboarding: FC<OnboardingProps> = ({
iconUrl,
onGetStarted,
appName = 'Qwen Code',

View file

@ -7,7 +7,7 @@
* Displays sessions grouped by date with search and infinite scroll
*/
import type React from 'react';
import type { FC } from 'react';
import { Fragment } from 'react';
import {
getTimeAgo,
@ -64,7 +64,7 @@ export interface SessionSelectorProps {
* />
* ```
*/
export const SessionSelector: React.FC<SessionSelectorProps> = ({
export const SessionSelector: FC<SessionSelectorProps> = ({
visible,
sessions,
currentSessionId,

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
const Sidebar: React.FC = () => <aside>Sidebar Component Placeholder</aside>;
const Sidebar: FC = () => <aside>Sidebar Component Placeholder</aside>;
export default Sidebar;

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
import { MessageContent } from '../MessageContent.js';
import './AssistantMessage.css';
@ -28,7 +28,7 @@ export interface AssistantMessageProps {
* AssistantMessage component - renders AI responses with styling
* Supports different states: default, success, error, warning, loading
*/
export const AssistantMessage: React.FC<AssistantMessageProps> = ({
export const AssistantMessage: FC<AssistantMessageProps> = ({
content,
timestamp: _timestamp,
onFileClick,

View file

@ -6,7 +6,7 @@
* MarkdownRenderer component - renders markdown content with syntax highlighting and clickable file paths
*/
import type React from 'react';
import type { FC } from 'react';
import { useMemo, useCallback } from 'react';
import MarkdownIt from 'markdown-it';
import type { Options as MarkdownItOptions } from 'markdown-it';
@ -59,7 +59,7 @@ const createMarkdownInstance = (): MarkdownIt =>
/**
* MarkdownRenderer component - renders markdown content with enhanced features
*/
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
export const MarkdownRenderer: FC<MarkdownRendererProps> = ({
content,
onFileClick,
enableFileLinks = true,

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
interface MessageProps {
id: string;
@ -14,7 +14,7 @@ interface MessageProps {
className?: string;
}
const Message: React.FC<MessageProps> = ({
const Message: FC<MessageProps> = ({
content,
sender,
timestamp,

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
import { memo } from 'react';
import { MarkdownRenderer } from './MarkdownRenderer/MarkdownRenderer.js';
@ -14,7 +14,7 @@ export interface MessageContentProps {
enableFileLinks?: boolean;
}
const MessageContentBase: React.FC<MessageContentProps> = ({
const MessageContentBase: FC<MessageContentProps> = ({
content,
onFileClick,
enableFileLinks,

View file

@ -4,10 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
const MessageInput: React.FC = () => (
<div>MessageInput Component Placeholder</div>
);
const MessageInput: FC = () => <div>MessageInput Component Placeholder</div>;
export default MessageInput;

View file

@ -4,10 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
const MessageList: React.FC = () => (
<div>MessageList Component Placeholder</div>
);
const MessageList: FC = () => <div>MessageList Component Placeholder</div>;
export default MessageList;

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
import { MessageContent } from './MessageContent.js';
export interface ThinkingMessageProps {
@ -13,7 +13,7 @@ export interface ThinkingMessageProps {
onFileClick?: (path: string) => void;
}
export const ThinkingMessage: React.FC<ThinkingMessageProps> = ({
export const ThinkingMessage: FC<ThinkingMessageProps> = ({
content,
timestamp: _timestamp,
onFileClick,

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
import { MessageContent } from './MessageContent.js';
export interface FileContext {
@ -21,7 +21,7 @@ export interface UserMessageProps {
fileContext?: FileContext;
}
export const UserMessage: React.FC<UserMessageProps> = ({
export const UserMessage: FC<UserMessageProps> = ({
content,
timestamp: _timestamp,
onFileClick,

View file

@ -4,14 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
interface InterruptedMessageProps {
text?: string;
}
// A lightweight status line similar to WaitingMessage but without the left status icon.
export const InterruptedMessage: React.FC<InterruptedMessageProps> = ({
export const InterruptedMessage: FC<InterruptedMessageProps> = ({
text = 'Interrupted',
}) => (
<div className="flex gap-0 items-start text-left py-2 flex-col opacity-85">

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
import { useEffect, useMemo, useState } from 'react';
interface WaitingMessageProps {
@ -24,9 +24,7 @@ const DEFAULT_LOADING_PHRASES = [
'Almost there...',
];
export const WaitingMessage: React.FC<WaitingMessageProps> = ({
loadingMessage,
}) => {
export const WaitingMessage: FC<WaitingMessageProps> = ({ loadingMessage }) => {
// Build a phrase list that starts with the provided message (if any), then witty fallbacks
const phrases = useMemo(() => {
const set = new Set<string>();

View file

@ -6,7 +6,7 @@
* Display-only checkbox component for plan entries
*/
import type React from 'react';
import type { FC } from 'react';
export interface CheckboxDisplayProps {
checked?: boolean;
@ -23,7 +23,7 @@ export interface CheckboxDisplayProps {
* - Supports indeterminate (middle) state using a data- attribute.
* - Intended for read-only display (disabled by default).
*/
export const CheckboxDisplay: React.FC<CheckboxDisplayProps> = ({
export const CheckboxDisplay: FC<CheckboxDisplayProps> = ({
checked = false,
indeterminate = false,
disabled = true,

View file

@ -6,7 +6,7 @@
* Generic tool call component - handles all tool call types as fallback
*/
import type React from 'react';
import type { FC } from 'react';
import {
ToolCallContainer,
ToolCallCard,
@ -22,7 +22,7 @@ import type { BaseToolCallProps } from './shared/index.js';
* Used as fallback for unknown tool call kinds
* Minimal display: show description and outcome
*/
export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
export const GenericToolCall: FC<BaseToolCallProps> = ({ toolCall }) => {
const { kind, title, content, locations, toolCallId } = toolCall;
const operationText = safeTitle(title);

View file

@ -7,7 +7,7 @@
* Pure UI component - platform interactions via usePlatform hook
*/
import type React from 'react';
import type { FC } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { FileLink } from '../layout/FileLink.js';
import {
@ -23,7 +23,7 @@ import type {
/**
* Simple container for Read tool calls
*/
const ReadToolCallContainer: React.FC<ToolCallContainerProps> = ({
const ReadToolCallContainer: FC<ToolCallContainerProps> = ({
label,
status = 'success',
children,
@ -56,7 +56,7 @@ const ReadToolCallContainer: React.FC<ToolCallContainerProps> = ({
* ReadToolCall - displays file reading operations
* Shows: Read filename (no content preview)
*/
export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
export const ReadToolCall: FC<BaseToolCallProps> = ({ toolCall }) => {
const { kind, content, locations, toolCallId } = toolCall;
const platform = usePlatform();
const openedDiffsRef = useRef<Map<string, string>>(new Map());

View file

@ -6,7 +6,7 @@
* Search tool call component - specialized for search operations
*/
import type React from 'react';
import type { FC } from 'react';
import {
safeTitle,
groupContent,
@ -18,7 +18,7 @@ import { FileLink } from '../layout/FileLink.js';
/**
* Inline container for compact search results display
*/
const InlineContainer: React.FC<{
const InlineContainer: FC<{
status: 'success' | 'error' | 'warning' | 'loading' | 'default';
labelSuffix?: string;
children?: React.ReactNode;
@ -68,7 +68,7 @@ const InlineContainer: React.FC<{
/**
* Card layout for multi-result or error display
*/
const SearchCard: React.FC<{
const SearchCard: FC<{
status: 'success' | 'error' | 'warning' | 'loading' | 'default';
children: React.ReactNode;
isFirst?: boolean;
@ -109,7 +109,7 @@ const SearchCard: React.FC<{
/**
* Row component for search card layout
*/
const SearchRow: React.FC<{ label: string; children: React.ReactNode }> = ({
const SearchRow: FC<{ label: string; children: React.ReactNode }> = ({
label,
children,
}) => (
@ -126,7 +126,7 @@ const SearchRow: React.FC<{ label: string; children: React.ReactNode }> = ({
/**
* Local locations list component
*/
const LocationsListLocal: React.FC<{
const LocationsListLocal: FC<{
locations: Array<{ path: string; line?: number | null }>;
}> = ({ locations }) => (
<div className="flex flex-col gap-1 max-w-full">
@ -156,7 +156,7 @@ const getDisplayLabel = (kind: string): string => {
* Specialized component for Search tool calls
* Optimized for displaying search operations and results
*/
export const SearchToolCall: React.FC<BaseToolCallProps> = ({
export const SearchToolCall: FC<BaseToolCallProps> = ({
toolCall,
isFirst,
isLast,

View file

@ -7,7 +7,7 @@
* Pure UI component - platform interactions via usePlatform hook
*/
import type React from 'react';
import type { FC } from 'react';
import {
ToolCallContainer,
CopyButton,
@ -27,7 +27,7 @@ type ShellVariant = 'execute' | 'bash';
/**
* Custom container for Execute variant with different styling
*/
const ExecuteToolCallContainer: React.FC<ToolCallContainerProps> = ({
const ExecuteToolCallContainer: FC<ToolCallContainerProps> = ({
label,
status = 'success',
children,
@ -92,9 +92,10 @@ const getInputCommand = (
/**
* Shell tool call implementation
*/
const ShellToolCallImpl: React.FC<
BaseToolCallProps & { variant: ShellVariant }
> = ({ toolCall, variant }) => {
const ShellToolCallImpl: FC<BaseToolCallProps & { variant: ShellVariant }> = ({
toolCall,
variant,
}) => {
const { title, content, rawInput, toolCallId } = toolCall;
const classPrefix = variant;
const platform = usePlatform();
@ -263,7 +264,7 @@ const ShellToolCallImpl: React.FC<
* ShellToolCall - displays bash/execute command tool calls
* Shows command input and output with IN/OUT cards
*/
export const ShellToolCall: React.FC<BaseToolCallProps> = (props) => {
export const ShellToolCall: FC<BaseToolCallProps> = (props) => {
const normalizedKind = props.toolCall.kind.toLowerCase();
const variant: ShellVariant =
normalizedKind === 'execute' ? 'execute' : 'bash';

View file

@ -6,7 +6,7 @@
* Think tool call component - specialized for thinking/reasoning operations
*/
import type React from 'react';
import type { FC } from 'react';
import {
ToolCallContainer,
ToolCallCard,
@ -20,7 +20,7 @@ import type { BaseToolCallProps } from './shared/index.js';
* Optimized for displaying AI reasoning and thought processes
* Minimal display: just show the thoughts (no context)
*/
export const ThinkToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
export const ThinkToolCall: FC<BaseToolCallProps> = ({ toolCall }) => {
const { content } = toolCall;
// Group content by type

View file

@ -6,7 +6,7 @@
* UpdatedPlan tool call component - specialized for plan update operations
*/
import type React from 'react';
import type { FC } from 'react';
import { groupContent, safeTitle } from './shared/index.js';
import type {
BaseToolCallProps,
@ -20,7 +20,7 @@ import { CheckboxDisplay } from './CheckboxDisplay.js';
/**
* Custom container for UpdatedPlanToolCall with specific styling
*/
const PlanToolCallContainer: React.FC<ToolCallContainerProps> = ({
const PlanToolCallContainer: FC<ToolCallContainerProps> = ({
label,
status = 'success',
children,
@ -113,9 +113,7 @@ const parsePlanEntries = (textOutputs: string[]): PlanEntry[] => {
* Specialized component for UpdatedPlan tool calls
* Optimized for displaying plan update operations
*/
export const UpdatedPlanToolCall: React.FC<BaseToolCallProps> = ({
toolCall,
}) => {
export const UpdatedPlanToolCall: FC<BaseToolCallProps> = ({ toolCall }) => {
const { content, status } = toolCall;
const { errors, textOutputs } = groupContent(content);

View file

@ -6,7 +6,7 @@
* Write tool call component - specialized for file writing operations
*/
import type React from 'react';
import type { FC } from 'react';
import {
ToolCallContainer,
groupContent,
@ -19,7 +19,7 @@ import { FileLink } from '../layout/FileLink.js';
* Specialized component for Write tool calls
* Shows: Write filename + error message + content preview
*/
export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
export const WriteToolCall: FC<BaseToolCallProps> = ({ toolCall }) => {
const { content, locations, rawInput, toolCallId } = toolCall;
// Group content by type

View file

@ -7,7 +7,7 @@
* Platform-agnostic version using webui components
*/
import type React from 'react';
import type { FC } from 'react';
import { FileLink } from '../../layout/FileLink.js';
import './LayoutComponents.css';
@ -33,7 +33,7 @@ export interface ToolCallContainerProps {
* ToolCallContainer - Main container for tool call displays
* Features timeline connector line and status bullet
*/
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
export const ToolCallContainer: FC<ToolCallContainerProps> = ({
label,
status = 'success',
children,
@ -73,7 +73,7 @@ interface ToolCallCardProps {
/**
* ToolCallCard - Legacy card wrapper for complex layouts like diffs
*/
export const ToolCallCard: React.FC<ToolCallCardProps> = ({
export const ToolCallCard: FC<ToolCallCardProps> = ({
icon: _icon,
children,
}) => (
@ -93,10 +93,7 @@ interface ToolCallRowProps {
/**
* ToolCallRow - A single row in the tool call grid (legacy - for complex layouts)
*/
export const ToolCallRow: React.FC<ToolCallRowProps> = ({
label,
children,
}) => (
export const ToolCallRow: FC<ToolCallRowProps> = ({ label, children }) => (
<div className="grid grid-cols-[80px_1fr] gap-medium min-w-0">
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
{label}
@ -138,10 +135,7 @@ const getStatusColorClass = (
/**
* StatusIndicator - Status indicator with colored dot
*/
export const StatusIndicator: React.FC<StatusIndicatorProps> = ({
status,
text,
}) => (
export const StatusIndicator: FC<StatusIndicatorProps> = ({ status, text }) => (
<div className="inline-block font-medium relative" title={status}>
<span
className={`inline-block w-1.5 h-1.5 rounded-full mr-1.5 align-middle ${getStatusColorClass(status)}`}
@ -160,7 +154,7 @@ interface CodeBlockProps {
/**
* CodeBlock - Code block for displaying formatted code or output
*/
export const CodeBlock: React.FC<CodeBlockProps> = ({ children }) => (
export const CodeBlock: FC<CodeBlockProps> = ({ children }) => (
<pre className="font-mono text-[var(--app-monospace-font-size)] bg-[var(--app-primary-background)] border border-[var(--app-input-border)] rounded-small p-medium overflow-x-auto mt-1 whitespace-pre-wrap break-words max-h-[300px] overflow-y-auto">
{children}
</pre>
@ -179,7 +173,7 @@ interface LocationsListProps {
/**
* LocationsList - List of file locations with clickable links
*/
export const LocationsList: React.FC<LocationsListProps> = ({ locations }) => (
export const LocationsList: FC<LocationsListProps> = ({ locations }) => (
<div className="toolcall-locations-list flex flex-col gap-1 max-w-full">
{locations.map((loc, idx) => (
<FileLink key={idx} path={loc.path} line={loc.line} showFullPath={true} />

View file

@ -6,7 +6,7 @@
* Shared copy utilities for toolcall components
*/
import type React from 'react';
import type { FC } from 'react';
import { useState, useCallback } from 'react';
import { usePlatform } from '../../../context/PlatformContext.js';
@ -46,7 +46,7 @@ interface CopyButtonProps {
* Uses PlatformContext for platform-specific clipboard access with fallback
* Note: Parent element should have 'group' class for hover effect
*/
export const CopyButton: React.FC<CopyButtonProps> = ({ text }) => {
export const CopyButton: FC<CopyButtonProps> = ({ text }) => {
const [showTooltip, setShowTooltip] = useState(false);
const platform = usePlatform();

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { ReactNode } from 'react';
import { forwardRef } from 'react';
/**
@ -28,7 +28,7 @@ export type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Button content */
children: React.ReactNode;
children: ReactNode;
/** Visual style variant */
variant?: ButtonVariant;
/** Button size */
@ -36,9 +36,9 @@ export interface ButtonProps
/** Loading state - shows spinner and disables button */
loading?: boolean;
/** Icon to display before children */
leftIcon?: React.ReactNode;
leftIcon?: ReactNode;
/** Icon to display after children */
rightIcon?: React.ReactNode;
rightIcon?: ReactNode;
/** Full width button */
fullWidth?: boolean;
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { ReactNode } from 'react';
import { forwardRef } from 'react';
/**
@ -28,9 +28,9 @@ export interface InputProps
/** Helper text below input */
helperText?: string;
/** Left icon/element */
leftElement?: React.ReactNode;
leftElement?: ReactNode;
/** Right icon/element */
rightElement?: React.ReactNode;
rightElement?: ReactNode;
/** Full width input */
fullWidth?: boolean;
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { FC } from 'react';
/**
* Tooltip component props
@ -22,7 +22,7 @@ export interface TooltipProps {
* Tooltip component using CSS group-hover for display
* Supports CSS variables for theming
*/
export const Tooltip: React.FC<TooltipProps> = ({
export const Tooltip: FC<TooltipProps> = ({
children,
content,
position = 'top',

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { createContext, useContext } from 'react';
import type { ReactNode } from 'react';
/**
* Platform types supported by the webui library
@ -88,7 +88,7 @@ export function usePlatform(): PlatformContextValue {
* Provider component props
*/
export interface PlatformProviderProps {
children: React.ReactNode;
children: ReactNode;
value: PlatformContextValue;
}

View file

@ -205,3 +205,6 @@ export type { CompletionItem, CompletionItemType } from './types/completion';
// Utils
export { groupSessionsByDate, getTimeAgo } from './utils/sessionGrouping';
export type { SessionGroup } from './utils/sessionGrouping';
// VSCode Webview utilities
export { default as WebviewContainer } from './components/WebviewContainer';

View file

@ -78,8 +78,8 @@
--app-radius-sm: 0.25rem;
--app-radius-md: 0.375rem;
--app-radius-lg: 0.5rem;
--corner-radius-small: 6px;
--corner-radius-medium: 8px;
--qwen-corner-radius-small: 6px;
--qwen-corner-radius-medium: 8px;
/* ===========================
Spacing

View file

@ -0,0 +1,254 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Isolated styles for VSCode webview environments to prevent conflicts
*/
/* Isolate all webui styles under a specific namespace to prevent global conflicts in webviews */
.qwen-webui-container {
/* Apply all CSS variables in the isolated namespace */
--app-primary: var(--app-primary, #3b82f6);
--app-primary-hover: var(--app-primary-hover, #2563eb);
--app-primary-foreground: var(--app-primary-foreground, #e4e4e7);
--app-secondary-foreground: var(--app-secondary-foreground, #a1a1aa);
--app-background: var(--app-background, #1e1e1e);
--app-primary-background: var(--app-primary-background, #1e1e1e);
--app-background-secondary: var(--app-background-secondary, #252526);
--app-secondary-background: var(--app-secondary-background, #252526);
--app-background-tertiary: var(--app-background-tertiary, #2d2d2d);
--app-foreground: var(--app-foreground, #e4e4e7);
--app-foreground-secondary: var(--app-foreground-secondary, #a1a1aa);
--app-foreground-muted: var(--app-foreground-muted, #71717a);
--app-border: var(--app-border, #3f3f46);
--app-border-focus: var(--app-border-focus, #3b82f6);
--app-primary-border-color: var(--app-primary-border-color, #3f3f46);
--app-success: var(--app-success, #10b981);
--app-warning: var(--app-warning, #f59e0b);
--app-error: var(--app-error, #ef4444);
--app-info: var(--app-info, #3b82f6);
--app-font-sans: var(--app-font-sans, system-ui, -apple-system, sans-serif);
--app-font-mono: var(--app-font-mono, ui-monospace, 'SF Mono', monospace);
--app-monospace-font-size: var(--app-monospace-font-size, 13px);
--app-link-foreground: var(--app-link-foreground, #007acc);
--app-link-active-foreground: var(--app-link-active-foreground, #005a9e);
--app-qwen-ivory: var(--app-qwen-ivory, #f5f5dc);
--app-radius-sm: var(--app-radius-sm, 0.25rem);
--app-radius-md: var(--app-radius-md, 0.375rem);
--app-radius-lg: var(--app-radius-lg, 0.5rem);
--qwen-corner-radius-small: var(--qwen-corner-radius-small, 6px);
--qwen-corner-radius-medium: var(--qwen-corner-radius-medium, 8px);
--app-spacing-xs: var(--app-spacing-xs, 0.25rem);
--app-spacing-sm: var(--app-spacing-sm, 0.5rem);
--app-spacing-md: var(--app-spacing-md, 1rem);
--app-spacing-medium: var(--app-spacing-medium, 8px);
--app-spacing-lg: var(--app-spacing-lg, 1.5rem);
--app-spacing-xl: var(--app-spacing-xl, 2rem);
--app-input-background: var(--app-input-background, #3c3c3c);
--app-input-secondary-background: var(--app-input-secondary-background, #2d2d2d);
--app-input-border: var(--app-input-border, #3f3f46);
--app-input-foreground: var(--app-input-foreground, #e4e4e7);
--app-input-placeholder-foreground: var(--app-input-placeholder-foreground, #71717a);
--app-ghost-button-hover-background: var(--app-ghost-button-hover-background, rgba(90, 93, 94, 0.31));
--app-button-background: var(--app-button-background, #3c3c3c);
--app-button-foreground: var(--app-button-foreground, #ffffff);
--app-transparent-inner-border: var(--app-transparent-inner-border, rgba(255, 255, 255, 0.1));
--app-header-background: var(--app-header-background, #252526);
--app-list-padding: var(--app-list-padding, 0px);
--app-list-item-padding: var(--app-list-item-padding, 4px 8px);
--app-list-border-color: var(--app-list-border-color, transparent);
--app-list-border-radius: var(--app-list-border-radius, 4px);
--app-list-hover-background: var(--app-list-hover-background, rgba(90, 93, 94, 0.31));
--app-list-active-background: var(--app-list-active-background, #094771);
--app-list-active-foreground: var(--app-list-active-foreground, #ffffff);
--app-list-gap: var(--app-list-gap, 2px);
--app-menu-background: var(--app-menu-background, #252526);
--app-menu-border: var(--app-menu-border, #454545);
--app-menu-foreground: var(--app-menu-foreground, #cccccc);
--app-menu-selection-background: var(--app-menu-selection-background, #094771);
--app-menu-selection-foreground: var(--app-menu-selection-foreground, #ffffff);
--app-tool-background: var(--app-tool-background, #1e1e1e);
--app-code-background: var(--app-code-background, #2d2d2d);
--app-warning-background: var(--app-warning-background, rgba(255, 204, 0, 0.1));
--app-warning-border: var(--app-warning-border, #ffcc00);
--app-warning-foreground: var(--app-warning-foreground, #ffcc00);
}
/* Reset potential conflicts with VSCode webview styles */
.qwen-webui-container *,
.qwen-webui-container *::before,
.qwen-webui-container *::after {
box-sizing: border-box;
}
/* Prevent styles from bleeding out of the container */
.qwen-webui-container {
all: inherit;
font: inherit;
line-height: inherit;
letter-spacing: inherit;
color: var(--app-foreground);
background: var(--app-background);
width: 100%;
height: 100%;
}
/* Isolated component styles */
.qwen-webui-container .code-block {
font-family: var(--app-font-mono);
font-size: var(--app-monospace-font-size, 13px);
background: var(--app-primary-background);
border: 1px solid var(--app-input-border);
border-radius: var(--app-radius-sm, 4px);
padding: var(--app-spacing-medium, 8px);
overflow-x: auto;
margin: 4px 0 0 0;
white-space: pre-wrap;
word-break: break-word;
max-height: 300px;
overflow-y: auto;
}
.qwen-webui-container .diff-display-container {
margin: 8px 0;
border: 1px solid var(--app-input-border);
border-radius: var(--app-radius-md, 6px);
overflow: hidden;
}
.qwen-webui-container .diff-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--app-input-secondary-background, var(--app-background-secondary));
border-bottom: 1px solid var(--app-input-border);
}
.qwen-webui-container .diff-file-path {
font-family: var(--app-font-mono);
font-size: 13px;
color: var(--app-primary-foreground);
}
.qwen-webui-container .open-diff-button {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: transparent;
border: 1px solid var(--app-input-border);
border-radius: var(--app-radius-sm, 4px);
color: var(--app-primary-foreground);
cursor: pointer;
font-size: 12px;
transition: background-color 0.15s;
}
.qwen-webui-container .open-diff-button:hover {
background: var(--app-ghost-button-hover-background);
}
.qwen-webui-container .diff-label {
padding: 8px 12px;
background: var(--app-primary-background);
border-bottom: 1px solid var(--app-input-border);
font-size: 11px;
font-weight: 600;
color: var(--app-secondary-foreground);
text-transform: uppercase;
}
/* Timeline styles in isolation */
.qwen-webui-container .toolcall-container {
position: relative;
padding-left: 30px;
padding-top: 8px;
padding-bottom: 8px;
}
.qwen-webui-container .toolcall-container::after {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--app-primary-border-color);
}
.qwen-webui-container .toolcall-container:first-child::after {
top: 24px;
}
.qwen-webui-container .toolcall-container:last-child::after {
height: calc(100% - 24px);
top: 0;
bottom: auto;
}
.qwen-webui-container .assistant-message-container {
position: relative;
padding-left: 30px;
padding-top: 8px;
padding-bottom: 8px;
}
.qwen-webui-container .assistant-message-container::after {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--app-primary-border-color);
}
.qwen-webui-container .assistant-message-container:first-child::after {
top: 24px;
}
.qwen-webui-container .assistant-message-container:last-child::after {
height: calc(100% - 24px);
top: 0;
bottom: auto;
}
.qwen-webui-container .qwen-message.message-item:not(.user-message-container)::after {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--app-primary-border-color);
z-index: 0;
}
.qwen-webui-container .message-item {
padding: 8px 0;
width: 100%;
align-items: flex-start;
padding-left: 30px;
user-select: text;
position: relative;
padding-top: 8px;
padding-bottom: 8px;
}

View file

@ -6,7 +6,7 @@
* Completion item types for autocomplete menus
*/
import type React from 'react';
import type { ReactNode } from 'react';
/**
* Completion item type categories
@ -30,7 +30,7 @@ export interface CompletionItem {
/** Optional description shown below label */
description?: string;
/** Optional icon to display */
icon?: React.ReactNode;
icon?: ReactNode;
/** Type of completion item */
type: CompletionItemType;
/** Value inserted into the input when selected (e.g., filename or command) */