From 0577fe6f36eebf2f5a436b8cd3a04bf7aa85ed5e Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Wed, 19 Nov 2025 00:34:45 +0800 Subject: [PATCH] =?UTF-8?q?refactor(vscode-ide-companion):=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84=20WebViewProvider=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽离初始化代理连接逻辑到单独的方法中 - 优化面板恢复时的代理连接流程 - 移除 EmptyState 组件中的信息横幅 - 在 App 组件中添加可关闭的信息横幅 - 调整输入表单样式,移除冗余样式 --- .../src/WebViewProvider.ts | 166 +++++++++++------- .../vscode-ide-companion/src/webview/App.css | 94 ++++++++-- .../vscode-ide-companion/src/webview/App.tsx | 50 ++++++ .../src/webview/components/EmptyState.css | 71 -------- .../src/webview/components/EmptyState.tsx | 55 +----- .../src/webview/utils/resourceUrl.ts | 74 ++++++++ 6 files changed, 309 insertions(+), 201 deletions(-) create mode 100644 packages/vscode-ide-companion/src/webview/utils/resourceUrl.ts diff --git a/packages/vscode-ide-companion/src/WebViewProvider.ts b/packages/vscode-ide-companion/src/WebViewProvider.ts index ecb1471e..66b9278f 100644 --- a/packages/vscode-ide-companion/src/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/WebViewProvider.ts @@ -196,69 +196,7 @@ export class WebViewProvider { // Initialize agent connection only once if (!this.agentInitialized) { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - - console.log( - '[WebViewProvider] Starting initialization, workingDir:', - workingDir, - ); - - const config = vscode.workspace.getConfiguration('qwenCode'); - const qwenEnabled = config.get('qwen.enabled', true); - - if (qwenEnabled) { - // Check if CLI is installed before attempting to connect - const cliDetection = await CliDetector.detectQwenCli(); - - if (!cliDetection.isInstalled) { - console.log( - '[WebViewProvider] Qwen CLI not detected, skipping agent connection', - ); - console.log( - '[WebViewProvider] CLI detection error:', - cliDetection.error, - ); - - // Show VSCode notification with installation option - await this.promptCliInstallation(); - - // Initialize empty conversation (can still browse history) - await this.initializeEmptyConversation(); - } else { - console.log( - '[WebViewProvider] Qwen CLI detected, attempting connection...', - ); - console.log('[WebViewProvider] CLI path:', cliDetection.cliPath); - console.log('[WebViewProvider] CLI version:', cliDetection.version); - - try { - console.log('[WebViewProvider] Connecting to agent...'); - const authInfo = await this.authStateManager.getAuthInfo(); - console.log('[WebViewProvider] Auth cache status:', authInfo); - - await this.agentManager.connect(workingDir, this.authStateManager); - console.log('[WebViewProvider] Agent connected successfully'); - this.agentInitialized = true; - - // Load messages from the current Qwen session - await this.loadCurrentSessionMessages(); - } catch (error) { - console.error('[WebViewProvider] Agent connection error:', error); - // Clear auth cache on error (might be auth issue) - await this.authStateManager.clearAuthState(); - vscode.window.showWarningMessage( - `Failed to connect to Qwen CLI: ${error}\nYou can still use the chat UI, but messages won't be sent to AI.`, - ); - // Fallback to empty conversation - await this.initializeEmptyConversation(); - } - } - } else { - console.log('[WebViewProvider] Qwen agent is disabled in settings'); - // Fallback to ConversationStore - await this.initializeEmptyConversation(); - } + await this.initializeAgentConnection(); } else { console.log( '[WebViewProvider] Agent already initialized, reusing existing connection', @@ -268,6 +206,76 @@ export class WebViewProvider { } } + /** + * Initialize agent connection and session + * Can be called from show() or restorePanel() + */ + private async initializeAgentConnection(): Promise { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + + console.log( + '[WebViewProvider] Starting initialization, workingDir:', + workingDir, + ); + + const config = vscode.workspace.getConfiguration('qwenCode'); + const qwenEnabled = config.get('qwen.enabled', true); + + if (qwenEnabled) { + // Check if CLI is installed before attempting to connect + const cliDetection = await CliDetector.detectQwenCli(); + + if (!cliDetection.isInstalled) { + console.log( + '[WebViewProvider] Qwen CLI not detected, skipping agent connection', + ); + console.log( + '[WebViewProvider] CLI detection error:', + cliDetection.error, + ); + + // Show VSCode notification with installation option + await this.promptCliInstallation(); + + // Initialize empty conversation (can still browse history) + await this.initializeEmptyConversation(); + } else { + console.log( + '[WebViewProvider] Qwen CLI detected, attempting connection...', + ); + console.log('[WebViewProvider] CLI path:', cliDetection.cliPath); + console.log('[WebViewProvider] CLI version:', cliDetection.version); + + try { + console.log('[WebViewProvider] Connecting to agent...'); + const authInfo = await this.authStateManager.getAuthInfo(); + console.log('[WebViewProvider] Auth cache status:', authInfo); + + await this.agentManager.connect(workingDir, this.authStateManager); + console.log('[WebViewProvider] Agent connected successfully'); + this.agentInitialized = true; + + // Load messages from the current Qwen session + await this.loadCurrentSessionMessages(); + } catch (error) { + console.error('[WebViewProvider] Agent connection error:', error); + // Clear auth cache on error (might be auth issue) + await this.authStateManager.clearAuthState(); + vscode.window.showWarningMessage( + `Failed to connect to Qwen CLI: ${error}\nYou can still use the chat UI, but messages won't be sent to AI.`, + ); + // Fallback to empty conversation + await this.initializeEmptyConversation(); + } + } + } else { + console.log('[WebViewProvider] Qwen agent is disabled in settings'); + // Fallback to ConversationStore + await this.initializeEmptyConversation(); + } + } + private async checkCliInstallation(): Promise { try { const result = await CliDetector.detectQwenCli(); @@ -829,9 +837,8 @@ export class WebViewProvider { vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview.js'), ); - const iconUri = this.panel!.webview.asWebviewUri( - vscode.Uri.joinPath(this.extensionUri, 'assets', 'icon.png'), - ); + // Convert extension URI for webview access - this allows frontend to construct resource paths + const extensionUri = this.panel!.webview.asWebviewUri(this.extensionUri); return ` @@ -841,9 +848,8 @@ export class WebViewProvider { Qwen Code Chat - +
- `; @@ -951,6 +957,30 @@ export class WebViewProvider { this.capturePanelTab(); console.log('[WebViewProvider] Panel restored successfully'); + + // Initialize agent connection if not already done + if (!this.agentInitialized) { + console.log( + '[WebViewProvider] Initializing agent connection after restore...', + ); + this.initializeAgentConnection().catch((error) => { + console.error( + '[WebViewProvider] Failed to initialize agent after restore:', + error, + ); + }); + } else { + console.log( + '[WebViewProvider] Agent already initialized, loading current session...', + ); + // Reload current session messages + this.loadCurrentSessionMessages().catch((error) => { + console.error( + '[WebViewProvider] Failed to load session messages after restore:', + error, + ); + }); + } } /** diff --git a/packages/vscode-ide-companion/src/webview/App.css b/packages/vscode-ide-companion/src/webview/App.css index d96906ee..789f8f24 100644 --- a/packages/vscode-ide-companion/src/webview/App.css +++ b/packages/vscode-ide-companion/src/webview/App.css @@ -594,13 +594,83 @@ button { flex: 1; } +/* =========================== + Info Banner (at bottom) + =========================== */ +.info-banner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 16px; + background-color: var(--app-input-secondary-background); + border-top: 1px solid var(--app-primary-border-color); +} + +.banner-content { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + min-width: 0; +} + +.banner-icon { + flex-shrink: 0; + width: 16px; + height: 16px; + fill: var(--app-primary-foreground); +} + +.banner-content label { + font-size: 13px; + color: var(--app-primary-foreground); + margin: 0; + line-height: 1.4; +} + +.banner-link { + color: var(--app-claude-orange); + text-decoration: none; + cursor: pointer; +} + +.banner-link:hover { + text-decoration: underline; +} + +.banner-close { + flex-shrink: 0; + width: 20px; + height: 20px; + padding: 0; + background: transparent; + border: none; + border-radius: var(--corner-radius-small); + color: var(--app-primary-foreground); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s; +} + +.banner-close:hover { + background-color: var(--app-ghost-button-hover-background); +} + +.banner-close svg { + width: 10px; + height: 10px; +} + /* =========================== Claude Code Style Input Form (.Me > .u) =========================== */ /* Outer container (.Me) */ .input-form-container { - border-top: 1px solid var(--app-primary-border-color); background-color: var(--app-primary-background); + padding: 16px; } /* Inner wrapper */ @@ -608,13 +678,18 @@ button { display: block; } -/* Form (.u) */ +/* Form (.u) - The actual input form with border and shadow */ .input-form { + background: var(--app-input-background); + border: 1px solid var(--app-input-border); + border-radius: var(--corner-radius-large); + color: var(--app-input-foreground); display: flex; flex-direction: column; - gap: 0; - padding: 0; + max-width: 680px; + margin: 0 auto; position: relative; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } /* Banner/Warning area (.Wr) */ @@ -624,7 +699,7 @@ button { /* Input wrapper (.fo) */ .input-wrapper { - padding: 16px; + /* padding: 16px; */ padding-bottom: 0; } @@ -634,10 +709,10 @@ button { min-height: 40px; max-height: 200px; padding: 10px 12px; - background-color: var(--app-input-background); + background-color: transparent; color: var(--app-input-foreground); - border: 1px solid var(--app-input-border); - border-radius: var(--corner-radius-medium); + border: none; + border-radius: 0; font-size: var(--vscode-chat-font-size, 13px); font-family: var(--vscode-chat-font-family); outline: none; @@ -646,11 +721,10 @@ button { overflow-x: hidden; word-wrap: break-word; white-space: pre-wrap; - transition: border-color 0.2s; } .input-field-editable:focus { - border-color: var(--app-input-active-border); + /* No border change needed since we don't have a border */ } .input-field-editable:empty:before { diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index cb43eb80..b6feaee7 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -66,6 +66,7 @@ export const App: React.FC = () => { ); const messagesEndRef = useRef(null); const inputFieldRef = useRef(null); + const [showBanner, setShowBanner] = useState(true); const handlePermissionRequest = React.useCallback( (request: { @@ -464,6 +465,55 @@ export const App: React.FC = () => { )} + {/* Info Banner */} + {showBanner && ( +
+
+ + + + + + +
+ +
+ )} +
diff --git a/packages/vscode-ide-companion/src/webview/components/EmptyState.css b/packages/vscode-ide-companion/src/webview/components/EmptyState.css index 43e5ba86..ce580e3f 100644 --- a/packages/vscode-ide-companion/src/webview/components/EmptyState.css +++ b/packages/vscode-ide-companion/src/webview/components/EmptyState.css @@ -46,74 +46,3 @@ font-weight: 400; max-width: 400px; } - -/* Banner Styles */ -.empty-state-banner { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 12px 16px; - background-color: var(--app-input-secondary-background); - border: 1px solid var(--app-primary-border-color); - border-radius: var(--corner-radius-medium); - width: 100%; - max-width: 500px; -} - -.banner-content { - display: flex; - align-items: center; - gap: 12px; - flex: 1; - min-width: 0; -} - -.banner-icon { - flex-shrink: 0; - width: 16px; - height: 16px; - fill: var(--app-primary-foreground); -} - -.banner-content label { - font-size: 13px; - color: var(--app-primary-foreground); - margin: 0; - line-height: 1.4; -} - -.banner-link { - color: var(--app-claude-orange); - text-decoration: none; - cursor: pointer; -} - -.banner-link:hover { - text-decoration: underline; -} - -.banner-close { - flex-shrink: 0; - width: 20px; - height: 20px; - padding: 0; - background: transparent; - border: none; - border-radius: var(--corner-radius-small); - color: var(--app-primary-foreground); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background-color 0.2s; -} - -.banner-close:hover { - background-color: var(--app-ghost-button-hover-background); -} - -.banner-close svg { - width: 10px; - height: 10px; -} diff --git a/packages/vscode-ide-companion/src/webview/components/EmptyState.tsx b/packages/vscode-ide-companion/src/webview/components/EmptyState.tsx index fda83aca..de8856f7 100644 --- a/packages/vscode-ide-companion/src/webview/components/EmptyState.tsx +++ b/packages/vscode-ide-companion/src/webview/components/EmptyState.tsx @@ -6,17 +6,11 @@ import type React from 'react'; import './EmptyState.css'; - -// Extend Window interface to include ICON_URI -declare global { - interface Window { - ICON_URI?: string; - } -} +import { generateIconUrl } from '../utils/resourceUrl.js'; export const EmptyState: React.FC = () => { - // Get icon URI from window, fallback to empty string if not available - const iconUri = window.ICON_URI || ''; + // Generate icon URL using the utility function + const iconUri = generateIconUrl('icon.png'); return (
@@ -37,49 +31,6 @@ export const EmptyState: React.FC = () => {
- - {/* Info Banner */} -
-
- - - - - - -
- -
); diff --git a/packages/vscode-ide-companion/src/webview/utils/resourceUrl.ts b/packages/vscode-ide-companion/src/webview/utils/resourceUrl.ts new file mode 100644 index 00000000..71570003 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/utils/resourceUrl.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +// Extend Window interface to include __EXTENSION_URI__ +declare global { + interface Window { + __EXTENSION_URI__?: string; + } +} + +/** + * Get the extension URI from the body data attribute or window global + * @returns Extension URI or undefined if not found + */ +function getExtensionUri(): string | undefined { + // First try to get from window (for backwards compatibility) + if (window.__EXTENSION_URI__) { + return window.__EXTENSION_URI__; + } + + // Then try to get from body data attribute (CSP-compliant method) + const bodyUri = document.body?.getAttribute('data-extension-uri'); + if (bodyUri) { + // Cache it in window for future use + window.__EXTENSION_URI__ = bodyUri; + return bodyUri; + } + + return undefined; +} + +/** + * Generate a resource URL for webview access + * Similar to the pattern used in other VSCode extensions + * + * @param relativePath - Relative path from extension root (e.g., 'assets/icon.png') + * @returns Full webview-accessible URL + * + * @example + * ```tsx + * + * ``` + */ +export function generateResourceUrl(relativePath: string): string { + const extensionUri = getExtensionUri(); + + if (!extensionUri) { + console.warn('[resourceUrl] Extension URI not found in window or body'); + return ''; + } + + // Remove leading slash if present + const cleanPath = relativePath.startsWith('/') + ? relativePath.slice(1) + : relativePath; + + // Ensure extension URI has trailing slash + const baseUri = extensionUri.endsWith('/') + ? extensionUri + : `${extensionUri}/`; + + return `${baseUri}${cleanPath}`; +} + +/** + * Shorthand for generating icon URLs + * @param iconPath - Path relative to assets directory + */ +export function generateIconUrl(iconPath: string): string { + return generateResourceUrl(`assets/${iconPath}`); +}