diff --git a/.github/workflows/release-vscode-companion.yml b/.github/workflows/release-vscode-companion.yml index ea02b01fb9..1011975291 100644 --- a/.github/workflows/release-vscode-companion.yml +++ b/.github/workflows/release-vscode-companion.yml @@ -223,7 +223,7 @@ jobs: npm --workspace=qwen-code-vscode-ide-companion run prepackage - name: 'Package VSIX (platform-specific)' - if: '${{ matrix.target != '''' }}' + if: "${{ matrix.target != '' }}" working-directory: 'packages/vscode-ide-companion' run: |- if [[ "${{ needs.prepare.outputs.is_preview }}" == "true" ]]; then @@ -236,7 +236,7 @@ jobs: shell: 'bash' - name: 'Package VSIX (universal)' - if: '${{ matrix.target == '''' }}' + if: "${{ matrix.target == '' }}" working-directory: 'packages/vscode-ide-companion' run: |- if [[ "${{ needs.prepare.outputs.is_preview }}" == "true" ]]; then @@ -251,7 +251,7 @@ jobs: - name: 'Upload VSIX Artifact' uses: 'actions/upload-artifact@v4' with: - name: 'vsix-${{ matrix.target || ''universal'' }}' + name: "vsix-${{ matrix.target || 'universal' }}" path: 'qwen-code-vscode-companion-${{ needs.prepare.outputs.release_version }}-*.vsix' if-no-files-found: 'error' @@ -292,7 +292,7 @@ jobs: npm install -g ovsx - name: 'Publish to Microsoft Marketplace' - if: '${{ needs.prepare.outputs.is_dry_run == ''false'' && needs.prepare.outputs.is_preview != ''true'' }}' + if: "${{ needs.prepare.outputs.is_dry_run == 'false' && needs.prepare.outputs.is_preview != 'true' }}" env: VSCE_PAT: '${{ secrets.VSCE_PAT }}' run: |- @@ -303,7 +303,7 @@ jobs: done - name: 'Publish to OpenVSX' - if: '${{ needs.prepare.outputs.is_dry_run == ''false'' }}' + if: "${{ needs.prepare.outputs.is_dry_run == 'false' }}" env: OVSX_TOKEN: '${{ secrets.OVSX_TOKEN }}' run: |- @@ -318,7 +318,7 @@ jobs: done - name: 'Upload all VSIXes as release artifacts (dry run)' - if: '${{ needs.prepare.outputs.is_dry_run == ''true'' }}' + if: "${{ needs.prepare.outputs.is_dry_run == 'true' }}" uses: 'actions/upload-artifact@v4' with: name: 'all-vsix-packages-${{ needs.prepare.outputs.release_version }}' diff --git a/.gitignore b/.gitignore index 27e0ab904c..d7d239fe06 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ gha-creds-*.json # Log files patch_output.log +vscode-app-*.log # docs build docs-site/.next diff --git a/integration-tests/concurrent-runner/render-chat-temp.html b/integration-tests/concurrent-runner/render-chat-temp.html index 5f33eaf695..bc6d01b617 100644 --- a/integration-tests/concurrent-runner/render-chat-temp.html +++ b/integration-tests/concurrent-runner/render-chat-temp.html @@ -1,277 +1,291 @@ - + + + + + Qwen Code Chat Export + + + + + + + + + + + + + + - - - -
-
-
-

Qwen Code Export

-
-
-
- Session Id - - + + /* Scrollbar styling */ + ::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + ::-webkit-scrollbar-track { + background: var(--bg-primary); + } + + ::-webkit-scrollbar-thumb { + background: var(--bg-secondary); + border-radius: 5px; + border: 2px solid var(--bg-primary); + } + + ::-webkit-scrollbar-thumb:hover { + background: #52525b; + } + + /* Responsive adjustments */ + @media (max-width: 768px) { + .chat-container { + max-width: 100%; + padding: 20px 16px; + } + + .header { + padding: 12px 16px; + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .header-left { + width: 100%; + justify-content: space-between; + } + + .meta { + width: 100%; + flex-direction: column; + gap: 6px; + } + } + + @media (max-width: 480px) { + .chat-container { + padding: 16px 12px; + } + } + + + + +
+
+
+

Qwen Code Export

-
- Export Time - - +
+
+ Session Id + - +
+
+ Export Time + - +
+ +
-
-
- - - - + + - + const sessionDateElement = document.getElementById('session-date'); + if (sessionDateElement && chatData.startTime) { + try { + const date = new Date(chatData.startTime); + sessionDateElement.textContent = date.toLocaleString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } catch (e) { + sessionDateElement.textContent = chatData.startTime; + } + } + // Get the ChatViewer and Platform components from the global object + const { ChatViewer, PlatformProvider } = QwenCodeWebUI; + + // Define a minimal platform context for web usage + const platformContext = { + platform: 'web', + postMessage: (message) => { + console.log('Posted message:', message); + }, + onMessage: (handler) => { + window.addEventListener('message', handler); + return () => window.removeEventListener('message', handler); + }, + openFile: (path) => { + console.log('Opening file:', path); + }, + getResourceUrl: (resource) => { + return null; + }, + features: { + canOpenFile: false, + canCopy: true, + }, + }; + + // Render the ChatViewer component without Babel + const rootElementNoBabel = document.getElementById('chat-root-no-babel'); + + // Create the ChatViewer element wrapped with PlatformProvider using React.createElement (no JSX) + const ChatAppNoBabel = React.createElement( + PlatformProvider, + { value: platformContext }, + React.createElement(ChatViewer, { + messages, + autoScroll: false, + theme: 'dark', + }), + ); + + ReactDOM.render(ChatAppNoBabel, rootElementNoBabel); + + diff --git a/package-lock.json b/package-lock.json index 5df32acc01..6312685e4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14284,7 +14284,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -20877,39 +20876,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "packages/sdk-typescript/node_modules/@vitest/browser": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.6.1.tgz", - "integrity": "sha512-9ZYW6KQ30hJ+rIfJoGH4wAub/KAb4YrFzX0kVLASvTm7nJWVC5EAv5SlzlXVl3h3DaUq5aqHlZl77nmOPnALUQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vitest/utils": "1.6.1", - "magic-string": "^0.30.5", - "sirv": "^2.0.4" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "playwright": "*", - "vitest": "1.6.1", - "webdriverio": "*" - }, - "peerDependenciesMeta": { - "playwright": { - "optional": true - }, - "safaridriver": { - "optional": true - }, - "webdriverio": { - "optional": true - } - } - }, "packages/sdk-typescript/node_modules/@vitest/coverage-v8": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz", @@ -21717,23 +21683,6 @@ "url": "https://opencollective.com/express" } }, - "packages/sdk-typescript/node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "packages/sdk-typescript/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/packages/vscode-ide-companion/assets/icon.svg b/packages/vscode-ide-companion/assets/icon.svg new file mode 100644 index 0000000000..a4bb382a63 --- /dev/null +++ b/packages/vscode-ide-companion/assets/icon.svg @@ -0,0 +1 @@ +Qwen \ No newline at end of file diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index f83d3cd864..756aa0592a 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -31,6 +31,24 @@ "onStartupFinished" ], "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "qwen-code-sidebar", + "title": "Qwen Code", + "icon": "assets/icon.svg" + } + ] + }, + "views": { + "qwen-code-sidebar": [ + { + "id": "qwen-code-chat", + "name": "Chat", + "type": "webview" + } + ] + }, "jsonValidation": [ { "fileMatch": "**/.qwen/settings.json", diff --git a/packages/vscode-ide-companion/src/extension.test.ts b/packages/vscode-ide-companion/src/extension.test.ts index ef0d5ad46a..d6128f91ef 100644 --- a/packages/vscode-ide-companion/src/extension.test.ts +++ b/packages/vscode-ide-companion/src/extension.test.ts @@ -43,6 +43,9 @@ vi.mock('vscode', () => ({ registerWebviewPanelSerializer: vi.fn(() => ({ dispose: vi.fn(), })), + registerWebviewViewProvider: vi.fn(() => ({ + dispose: vi.fn(), + })), }, workspace: { workspaceFolders: [], diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index 14ff4bcaeb..f8f284e5ed 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -15,6 +15,7 @@ import { type IdeInfo, } from '@qwen-code/qwen-code-core/src/ide/detect-ide.js'; import { WebViewProvider } from './webview/WebViewProvider.js'; +import { SidebarWebviewProvider } from './webview/SidebarWebviewProvider.js'; import { registerNewCommands } from './commands/index.js'; import { ReadonlyFileSystemProvider } from './services/readonlyFileSystemProvider.js'; import { isWindows } from './utils/platform.js'; @@ -150,6 +151,24 @@ export async function activate(context: vscode.ExtensionContext) { return provider; }; + // Register sidebar webview provider + const sidebarProvider = new SidebarWebviewProvider( + context, + context.extensionUri, + ); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider( + 'qwen-code-chat', + sidebarProvider, + { + webviewOptions: { + retainContextWhenHidden: true, + }, + }, + ), + ); + log('Sidebar webview provider registered'); + // Register WebView panel serializer for persistence across reloads context.subscriptions.push( vscode.window.registerWebviewPanelSerializer('qwenCode.chat', { diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index 0a5aec02c0..6302576e0e 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -429,6 +429,15 @@ export class AcpConnection { this.sessionManager.reset(); } + /** + * Reset session manager state to allow creating a new session + * This clears the current session ID so that newSession() will create a fresh session + */ + resetSessionState(): void { + console.log('[ACP] Resetting session state'); + this.sessionManager.reset(); + } + /** * Check if connected */ diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 0944ee5b7d..af1899b6d9 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -1243,6 +1243,18 @@ export class QwenAgentManager { await this.connection.switchSession(sessionId); } + /** + * Clear current session to force creation of a new one + * This resets the session manager state so that the next createNewSession call + * will create a fresh session instead of reusing the existing one + */ + clearCurrentSession(): void { + console.log('[QwenAgentManager] Clearing current session'); + // Reset the connection's session manager state + this.connection.resetSessionState(); + console.log('[QwenAgentManager] Session state cleared successfully'); + } + /** * Cancel current prompt */ diff --git a/packages/vscode-ide-companion/src/webview/PanelManager.ts b/packages/vscode-ide-companion/src/webview/PanelManager.ts index 44f1a6ecc6..26ea9ea96e 100644 --- a/packages/vscode-ide-companion/src/webview/PanelManager.ts +++ b/packages/vscode-ide-companion/src/webview/PanelManager.ts @@ -224,8 +224,16 @@ export class PanelManager { return; } + const scheduledPanel = this.panel; + // Defer slightly so the tab model is updated after create/reveal setTimeout(() => { + // The panel may have been disposed/replaced before this callback runs. + if (!this.panel || this.panel !== scheduledPanel) { + return; + } + + const panelTitle = this.panel.title; const allTabs = vscode.window.tabGroups.all.flatMap((g) => g.tabs); const match = allTabs.find((t) => { // Type guard for webview tab input @@ -234,7 +242,7 @@ export class PanelManager { !!inp && typeof inp === 'object' && 'viewType' in inp; const isWebview = isWebviewInput(input); const sameViewType = isWebview && input.viewType === 'qwenCode.chat'; - const sameLabel = t.label === this.panel!.title; + const sameLabel = t.label === panelTitle; return !!(sameViewType || sameLabel); }); this.panelTab = match ?? null; diff --git a/packages/vscode-ide-companion/src/webview/SidebarWebviewProvider.ts b/packages/vscode-ide-companion/src/webview/SidebarWebviewProvider.ts new file mode 100644 index 0000000000..cce9b7a307 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/SidebarWebviewProvider.ts @@ -0,0 +1,490 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode'; +import { QwenAgentManager } from '../services/qwenAgentManager.js'; +import { ConversationStore } from '../services/conversationStore.js'; +import { MessageHandler } from './MessageHandler.js'; +import type { AcpPermissionRequest } from '../types/acpTypes.js'; +import type { AvailableCommand } from '../types/acpTypes.js'; +import type { ModelInfo } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; +import { isAuthenticationRequiredError } from '../utils/authErrors.js'; + +/** + * WebviewView provider for displaying Qwen Code chat in the sidebar + */ +export class SidebarWebviewProvider implements vscode.WebviewViewProvider { + private view?: vscode.WebviewView; + private agentManager: QwenAgentManager; + private conversationStore: ConversationStore; + private messageHandler: MessageHandler; + private disposables: vscode.Disposable[] = []; + private agentInitialized = false; + private pendingPermissionRequest: AcpPermissionRequest | null = null; + private pendingPermissionResolve: ((optionId: string) => void) | null = null; + private currentModeId: ApprovalModeValue | null = null; + private authState: boolean | null = null; + private cachedAvailableModels: ModelInfo[] | null = null; + private cachedAvailableCommands: AvailableCommand[] | null = null; + + constructor( + private readonly context: vscode.ExtensionContext, + private readonly extensionUri: vscode.Uri, + ) { + this.agentManager = new QwenAgentManager(); + this.conversationStore = new ConversationStore(context); + this.messageHandler = new MessageHandler( + this.agentManager, + this.conversationStore, + null, + (message) => this.sendMessageToWebView(message), + ); + + // Set login handler + this.messageHandler.setLoginHandler(async () => { + await this.forceReLogin(); + }); + + // Setup agent callbacks + this.setupAgentCallbacks(); + } + + private setupAgentCallbacks(): void { + this.agentManager.onMessage((message) => { + this.sendMessageToWebView({ + type: 'message', + data: message, + }); + }); + + this.agentManager.onStreamChunk((chunk: string) => { + this.messageHandler.appendStreamContent(chunk); + this.sendMessageToWebView({ + type: 'streamChunk', + data: { chunk }, + }); + }); + + this.agentManager.onThoughtChunk((chunk: string) => { + this.messageHandler.appendStreamContent(chunk); + this.sendMessageToWebView({ + type: 'thoughtChunk', + data: { chunk }, + }); + }); + + this.agentManager.onModeInfo((info) => { + try { + const current = (info?.currentModeId || + null) as ApprovalModeValue | null; + this.currentModeId = current; + this.sendMessageToWebView({ + type: 'modeInfo', + data: info, + }); + } catch (error) { + console.error( + '[SidebarWebviewProvider] Error handling mode info:', + error, + ); + } + }); + + this.agentManager.onAvailableModels((models: ModelInfo[]) => { + this.cachedAvailableModels = models; + this.sendMessageToWebView({ + type: 'availableModels', + data: { models }, + }); + }); + + this.agentManager.onAvailableCommands((commands: AvailableCommand[]) => { + this.cachedAvailableCommands = commands; + this.sendMessageToWebView({ + type: 'availableCommands', + data: { commands }, + }); + }); + + this.agentManager.onPermissionRequest( + async (request: AcpPermissionRequest) => + new Promise((resolve) => { + this.pendingPermissionRequest = request; + this.pendingPermissionResolve = resolve; + this.sendMessageToWebView({ + type: 'permissionRequest', + data: request, + }); + }), + ); + } + + async resolveWebviewView( + webviewView: vscode.WebviewView, + _context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ): Promise { + console.log('[SidebarWebviewProvider] resolveWebviewView called'); + this.view = webviewView; + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this.extensionUri], + }; + + // Generate HTML for webview view (similar to panel but for view) + const scriptUri = webviewView.webview.asWebviewUri( + vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview.js'), + ); + const extensionUriForWebview = webviewView.webview.asWebviewUri( + this.extensionUri, + ); + + console.log('[SidebarWebviewProvider] Setting webview HTML'); + webviewView.webview.html = ` + + + + + + Qwen Code + + +
+ + + `; + + console.log('[SidebarWebviewProvider] Setting up message handlers'); + // Handle messages from webview + this.disposables.push( + webviewView.webview.onDidReceiveMessage(async (message) => { + console.log('[SidebarWebviewProvider] Received message:', message); + await this.handleWebviewMessage(message); + }), + ); + + // Handle webview visibility changes + this.disposables.push( + webviewView.onDidChangeVisibility(() => { + console.log( + '[SidebarWebviewProvider] Visibility changed:', + webviewView.visible, + ); + if (webviewView.visible && !this.agentInitialized) { + this.initializeAgentConnection(); + } + }), + ); + + console.log( + '[SidebarWebviewProvider] Webview setup complete, visible:', + webviewView.visible, + ); + // Don't initialize here - wait for webviewReady message + } + + private async handleWebviewMessage(message: unknown): Promise { + if (!message || typeof message !== 'object') { + return; + } + + const msg = message as { type: string; [key: string]: unknown }; + + switch (msg.type) { + case 'webviewReady': + this.handleWebviewReady(); + break; + case 'permissionResponse': + this.handlePermissionResponse(msg); + break; + case 'newQwenSession': + await this.handleNewSession(); + break; + case 'openNewChatTab': + // In sidebar, create new session instead of opening new tab + await this.handleNewSession(); + break; + default: + // Use route method instead of handleMessage + await this.messageHandler.route( + msg as { type: string; data?: unknown }, + ); + break; + } + } + + private handleWebviewReady(): void { + console.log('[SidebarWebviewProvider] Webview ready event received'); + console.log('[SidebarWebviewProvider] Auth state:', this.authState); + console.log( + '[SidebarWebviewProvider] Agent initialized:', + this.agentInitialized, + ); + + if (this.authState !== null) { + console.log('[SidebarWebviewProvider] Sending auth state to webview'); + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: this.authState }, + }); + } + + if (this.cachedAvailableModels) { + console.log( + '[SidebarWebviewProvider] Sending available models to webview', + ); + this.sendMessageToWebView({ + type: 'availableModels', + data: { models: this.cachedAvailableModels }, + }); + } + + if (this.cachedAvailableCommands) { + console.log( + '[SidebarWebviewProvider] Sending available commands to webview', + ); + this.sendMessageToWebView({ + type: 'availableCommands', + data: { commands: this.cachedAvailableCommands }, + }); + } + + if (!this.agentInitialized) { + console.log('[SidebarWebviewProvider] Initializing agent connection'); + this.initializeAgentConnection(); + } else { + console.log('[SidebarWebviewProvider] Agent already initialized'); + } + } + + private handlePermissionResponse(msg: { + type: string; + [key: string]: unknown; + }): void { + const optionId = msg.optionId as string | undefined; + if (this.pendingPermissionResolve && optionId) { + this.pendingPermissionResolve(optionId); + this.pendingPermissionRequest = null; + this.pendingPermissionResolve = null; + } + } + + private async initializeAgentConnection(): Promise { + if (this.agentInitialized) { + return; + } + + await this.doInitializeAgentConnection({ autoAuthenticate: false }); + } + + private async doInitializeAgentConnection(options?: { + autoAuthenticate?: boolean; + }): Promise { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + + const bundledCliEntry = vscode.Uri.joinPath( + this.extensionUri, + 'dist', + 'qwen-cli', + 'cli.js', + ).fsPath; + + console.log('[SidebarWebviewProvider] Connecting to agent...'); + const connectResult = await this.agentManager.connect( + workingDir, + bundledCliEntry, + { autoAuthenticate: options?.autoAuthenticate ?? false }, + ); + + this.agentInitialized = true; + + if (connectResult.requiresAuth) { + console.log('[SidebarWebviewProvider] Authentication required'); + this.authState = false; + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: false }, + }); + return false; + } + + this.authState = true; + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: true }, + }); + + await this.loadCurrentSessionMessages(); + return true; + } catch (error) { + console.error( + '[SidebarWebviewProvider] Failed to initialize agent:', + error, + ); + if (isAuthenticationRequiredError(error)) { + this.authState = false; + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: false }, + }); + return false; + } else { + // Initialize empty conversation on other errors + await this.initializeEmptyConversation(); + return false; + } + } + } + + private async initializeEmptyConversation(): Promise { + try { + console.log('[SidebarWebviewProvider] Initializing empty conversation'); + this.sendMessageToWebView({ + type: 'loadMessages', + data: { messages: [], conversationId: null }, + }); + } catch (error) { + console.error( + '[SidebarWebviewProvider] Failed to initialize empty conversation:', + error, + ); + } + } + + private async forceReLogin(): Promise { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + }, + async (progress) => { + progress.report({ message: 'Preparing sign-in...' }); + + if (this.agentInitialized) { + try { + this.agentManager.disconnect(); + } catch (error) { + console.warn( + '[SidebarWebviewProvider] Failed to disconnect before re-login:', + error, + ); + } + this.agentInitialized = false; + } + + this.authState = null; + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: null }, + }); + + await new Promise((resolve) => setTimeout(resolve, 300)); + + progress.report({ + message: 'Connecting to CLI and starting sign-in...', + }); + + const authenticated = await this.doInitializeAgentConnection({ + autoAuthenticate: true, + }); + + if (!authenticated) { + throw new Error('Authentication was not completed.'); + } + + this.sendMessageToWebView({ + type: 'loginSuccess', + data: { message: 'Successfully logged in!' }, + }); + }, + ); + } + + private async loadCurrentSessionMessages(): Promise { + try { + const conversationId = this.conversationStore.getCurrentConversationId(); + if (conversationId) { + // ConversationStore doesn't have getMessages, load differently + this.sendMessageToWebView({ + type: 'loadMessages', + data: { messages: [], conversationId }, + }); + } + } catch (error) { + console.error('[SidebarWebviewProvider] Failed to load messages:', error); + } + } + + private async handleNewSession(): Promise { + try { + console.log('[SidebarWebviewProvider] Creating new session'); + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + + // Clear the current conversation ID first + this.messageHandler.setCurrentConversationId(null); + + // Clear the current session in the agent manager to force creation of a new one + this.agentManager.clearCurrentSession(); + + // Create new session via agent manager + // Now it will create a fresh session instead of reusing the existing one + const newSessionId = await this.agentManager.createNewSession(workingDir); + + // Update message handler with new session ID + this.messageHandler.setCurrentConversationId(newSessionId); + + // Clear current conversation UI (same as WebViewProvider) + this.sendMessageToWebView({ + type: 'conversationCleared', + data: {}, + }); + + console.log( + '[SidebarWebviewProvider] New session created:', + newSessionId, + ); + } catch (error) { + console.error( + '[SidebarWebviewProvider] Failed to create new session:', + error, + ); + // Show error to user + this.sendMessageToWebView({ + type: 'error', + data: { message: 'Failed to create new session' }, + }); + } + } + + private sendMessageToWebView(message: unknown): void { + if (this.view?.webview) { + this.view.webview.postMessage(message); + } + } + + hasPendingPermission(): boolean { + return this.pendingPermissionRequest !== null; + } + + getCurrentModeId(): ApprovalModeValue | null { + return this.currentModeId; + } + + shouldSuppressDiff(): boolean { + const mode = this.currentModeId; + return mode === 'auto-edit' || mode === 'plan'; + } + + dispose(): void { + this.disposables.forEach((d) => d.dispose()); + // QwenAgentManager doesn't have dispose method + } +} diff --git a/packages/vscode-ide-companion/src/webview/styles/App.css b/packages/vscode-ide-companion/src/webview/styles/App.css index 6216d2b87d..f3dc303dba 100644 --- a/packages/vscode-ide-companion/src/webview/styles/App.css +++ b/packages/vscode-ide-companion/src/webview/styles/App.css @@ -95,7 +95,10 @@ /* Buttons - VSCode tokens */ --app-ghost-button-hover-background: var(--vscode-toolbar-hoverBackground); - --app-button-foreground: var(--vscode-button-foreground, var(--app-qwen-ivory)); + --app-button-foreground: var( + --vscode-button-foreground, + var(--app-qwen-ivory) + ); --app-button-background: var( --vscode-button-background, var(--app-qwen-clay-button-orange) diff --git a/packages/webui/examples/cdn-usage-demo.html b/packages/webui/examples/cdn-usage-demo.html index c013e90783..2919614af5 100644 --- a/packages/webui/examples/cdn-usage-demo.html +++ b/packages/webui/examples/cdn-usage-demo.html @@ -1,99 +1,117 @@ - + + + + + @qwen-code/webui CDN Usage Example + + + + + + - - - - - - - - - - - - - + .container { + max-width: 1200px; + margin: 0 auto; + background: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + overflow: hidden; + } - -
-

@qwen-code/webui CDN Usage Example

-

ChatViewer Component Demo

-
-
+ h1 { + text-align: center; + color: #333; + margin-bottom: 30px; + } - - + theme: 'light', + }), + ); + ReactDOM.render(ChatAppNoBabel, rootElementNoBabel); + + diff --git a/packages/webui/examples/complex-chat-demo.html b/packages/webui/examples/complex-chat-demo.html index c05be1e1b8..22adbc3dd2 100644 --- a/packages/webui/examples/complex-chat-demo.html +++ b/packages/webui/examples/complex-chat-demo.html @@ -1,99 +1,112 @@ - + + + + + @qwen-code/webui Complex Chat Demo + + + + + + + + + + + + + + + + - - - -
-

@qwen-code/webui Complex Chat Demo

-

Real conversation example with tool calls

-
- -

Alternative: With Full Tailwind Support

-

For full Tailwind utility class support (like gap-1.5, button classes, etc.), also include:

-
<script src="https://cdn.tailwindcss.com"></script>
-
- - - + // Get the ChatViewer and Platform components from the global object + const { ChatViewer, PlatformProvider } = QwenCodeWebUI; + // Define a minimal platform context for web usage + const platformContext = { + platform: 'web', + postMessage: (message) => { + // In a web context, you might want to handle messages differently + console.log('Posted message:', message); + }, + onMessage: (handler) => { + // In a web context, you might listen for custom events + window.addEventListener('message', handler); + return () => window.removeEventListener('message', handler); + }, + openFile: (path) => { + console.log('Opening file:', path); + }, + getResourceUrl: (resource) => { + // Return URLs for platform-specific resources + return null; // Use default resources + }, + features: { + canOpenFile: false, + canCopy: true, + }, + }; + + // Render the ChatViewer component + const rootElement = document.getElementById('complex-chat-root'); + + // Create the ChatViewer element wrapped with PlatformProvider with complex data + const ChatApp = React.createElement( + PlatformProvider, + { value: platformContext }, + React.createElement(ChatViewer, { + messages: combinedMessages, + autoScroll: true, + theme: 'light', + emptyMessage: 'Loading conversation...', + }), + ); + + ReactDOM.render(ChatApp, rootElement); + + diff --git a/packages/webui/src/components/ChatViewer/ChatViewer.css b/packages/webui/src/components/ChatViewer/ChatViewer.css index 3d8144caf8..94b8dca78f 100644 --- a/packages/webui/src/components/ChatViewer/ChatViewer.css +++ b/packages/webui/src/components/ChatViewer/ChatViewer.css @@ -15,9 +15,22 @@ flex-direction: column; width: 100%; height: 100%; - background-color: var(--app-background, var(--app-primary-background, #1e1e1e)); + background-color: var( + --app-background, + var(--app-primary-background, #1e1e1e) + ); color: var(--app-primary-foreground, #cccccc); - font-family: var(--vscode-chat-font-family, var(--vscode-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif)); + font-family: var( + --vscode-chat-font-family, + var( + --vscode-font-family, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + sans-serif + ) + ); font-size: var(--vscode-chat-font-size, 13px); overflow: hidden; } @@ -58,21 +71,25 @@ /* Light theme scrollbar styling */ @media (prefers-color-scheme: light) { - .chat-viewer-container.auto-theme .chat-viewer-messages::-webkit-scrollbar-thumb { + .chat-viewer-container.auto-theme + .chat-viewer-messages::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); } - - .chat-viewer-container.auto-theme .chat-viewer-messages::-webkit-scrollbar-thumb:hover { + + .chat-viewer-container.auto-theme + .chat-viewer-messages::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.3); } } /* Force light theme scrollbar */ -.chat-viewer-container.light-theme .chat-viewer-messages::-webkit-scrollbar-thumb { +.chat-viewer-container.light-theme + .chat-viewer-messages::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); } -.chat-viewer-container.light-theme .chat-viewer-messages::-webkit-scrollbar-thumb:hover { +.chat-viewer-container.light-theme + .chat-viewer-messages::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.3); } diff --git a/packages/webui/src/components/messages/Assistant/AssistantMessage.css b/packages/webui/src/components/messages/Assistant/AssistantMessage.css index a9a1369fde..24ebbe26fc 100644 --- a/packages/webui/src/components/messages/Assistant/AssistantMessage.css +++ b/packages/webui/src/components/messages/Assistant/AssistantMessage.css @@ -63,8 +63,13 @@ } @keyframes assistantPulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } } /* Timeline connector line - full height by default */ diff --git a/packages/webui/src/styles/components.css b/packages/webui/src/styles/components.css index e873e7d9eb..7ef3cd237a 100644 --- a/packages/webui/src/styles/components.css +++ b/packages/webui/src/styles/components.css @@ -180,7 +180,10 @@ align-items: center; justify-content: space-between; padding: 8px 12px; - background: var(--app-input-secondary-background, var(--app-background-secondary)); + background: var( + --app-input-secondary-background, + var(--app-background-secondary) + ); border-bottom: 1px solid var(--app-input-border); } @@ -393,7 +396,10 @@ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); transition: border-color 0.2s; z-index: 1; - background: var(--app-input-secondary-background, var(--app-background-secondary)); + background: var( + --app-input-secondary-background, + var(--app-background-secondary) + ); color: var(--app-input-foreground); } @@ -424,7 +430,7 @@ } .composer-input:empty::before, -.composer-input[data-empty="true"]::before { +.composer-input[data-empty='true']::before { content: attr(data-placeholder); color: var(--app-input-placeholder-foreground); pointer-events: none; @@ -440,7 +446,7 @@ } .composer-input:disabled, -.composer-input[contenteditable="false"] { +.composer-input[contenteditable='false'] { color: #999; cursor: not-allowed; } diff --git a/packages/webui/src/styles/timeline.css b/packages/webui/src/styles/timeline.css index b69aeb8159..d437938df7 100644 --- a/packages/webui/src/styles/timeline.css +++ b/packages/webui/src/styles/timeline.css @@ -46,7 +46,9 @@ top: var(--timeline-center-offset, 13px); } -.qwen-message.message-item:not(.user-message-container):has(+ .user-message-container)::after, +.qwen-message.message-item:not(.user-message-container):has( + + .user-message-container + )::after, .qwen-message.message-item:not(.user-message-container):has( + :not(.qwen-message.message-item) )::after, diff --git a/scripts/prepare-package.js b/scripts/prepare-package.js index 3ae9d3e089..ec6dba3a85 100644 --- a/scripts/prepare-package.js +++ b/scripts/prepare-package.js @@ -179,4 +179,9 @@ fs.writeFileSync( console.log('\n✅ Package prepared for publishing at dist/'); console.log('\nPackage structure:'); -execSync('ls -lh dist/', { stdio: 'inherit', cwd: rootDir }); +try { + const listCmd = process.platform === 'win32' ? 'dir dist' : 'ls -lh dist/'; + execSync(listCmd, { stdio: 'inherit', cwd: rootDir, shell: true }); +} catch (_error) { + console.log('(Could not list directory structure)'); +}