From f2eb4e7c0106534fc8e45e9451d652814356da0d Mon Sep 17 00:00:00 2001 From: Robert Talamantez Date: Fri, 22 Nov 2024 23:17:08 +0000 Subject: [PATCH] Bug Fix: unexpected window behavior --- .devcontainer/devcontainer.json | 5 +-- CHANGELOG.md | 5 ++- Dockerfile | 10 ++++- package.json | 6 +-- src/editor-utils.ts | 71 +++++++++++++++++++++++++++++++++ src/extension.ts | 65 +++++++++++++++++------------- src/test-utils.ts | 66 ++++++++++++++++++++++++++++++ test/suite/extension.test.ts | 58 +++++++++++++++++---------- 8 files changed, 230 insertions(+), 56 deletions(-) create mode 100644 src/editor-utils.ts create mode 100644 src/test-utils.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9a44797..7898559 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -25,7 +25,6 @@ }, "ghcr.io/devcontainers/features/git:1": {} }, - "postCreateCommand": "npm install", - "remoteUser": "node", - "postStartCommand": "npm install -g @vscode/vsce" + "postCreateCommand": "sudo apt-get update && sudo apt-get install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libdrm2 libgtk-3-0 libasound2 libgbm1 xvfb && npm install -g @vscode/vsce && npm install", + "remoteUser": "node" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d127afc..39a9be3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.12] - 2024-11-22 +### Fixed + - Bug: Panel was closing, brought extension under docker vm, updated tests ## [1.1.11] - 2024-11-22 -### Fixed +### ~Fixed~ - Bug: Panel was closing, changed preview mode to false ## [1.1.10] - 2024-11-21 diff --git a/Dockerfile b/Dockerfile index f4ba915..b662033 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,18 @@ FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:18 -# Install additional OS tools +# Install additional OS tools and VS Code test dependencies RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends \ git \ openssh-client \ + libnss3 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libdrm2 \ + libgtk-3-0 \ + libasound2 \ + libgbm1 \ + xvfb \ && apt-get clean -y \ && rm -rf /var/lib/apt/lists/* diff --git a/package.json b/package.json index 37d89cc..00bc5e5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "claude-vscode-assistant", "displayName": "Claude AI Assistant", - "version": "1.1.11", + "version": "1.1.12", "description": "Claude AI assistant for Visual Studio Code", "publisher": "conscious-robot", "repository": { @@ -99,7 +99,7 @@ "publish": "pnpm run verify && vsce publish", "verify": "pnpm run build && pnpm run test", "pretest": "pnpm run build && pnpm run compile-tests", - "test": "node --force-node-api-uncaught-exceptions-policy=true ./out/test/runTest.js", + "test": "xvfb-run --auto-servernum --server-args='-screen 0 1024x768x24' node --force-node-api-uncaught-exceptions-policy=true ./out/test/runTest.js", "compile-tests": "tsc -p ./tsconfig.test.json", "watch-tests": "tsc -p ./tsconfig.test.json --watch" }, @@ -121,4 +121,4 @@ "ts-node": "^10.9.2", "typescript": "^5.6.3" } -} +} \ No newline at end of file diff --git a/src/editor-utils.ts b/src/editor-utils.ts new file mode 100644 index 0000000..cb9b039 --- /dev/null +++ b/src/editor-utils.ts @@ -0,0 +1,71 @@ +// src/editor-utils.ts +import * as vscode from 'vscode'; + +export interface EditorRetryOptions { + maxAttempts?: number; + delayMs?: number; + timeout?: number; +} + +const DEFAULT_OPTIONS: EditorRetryOptions = { + maxAttempts: 3, + delayMs: 100, + timeout: 5000 +}; + +export class EditorTimeoutError extends Error { + constructor(message: string) { + super(message); + this.name = 'EditorTimeoutError'; + } +} + +/** + * Waits for the active editor with retries + */ +export async function waitForActiveEditor(options: EditorRetryOptions = {}): Promise { + const opts = { ...DEFAULT_OPTIONS, ...options }; + const startTime = Date.now(); + + for (let attempt = 1; attempt <= opts.maxAttempts!; attempt++) { + const editor = vscode.window.activeTextEditor; + if (editor) { + return editor; + } + + if (Date.now() - startTime > opts.timeout!) { + throw new EditorTimeoutError('Timed out waiting for active editor'); + } + + // Log retry attempts in debug mode + console.log(`Waiting for active editor... Attempt ${attempt}/${opts.maxAttempts}`); + + await new Promise(resolve => setTimeout(resolve, opts.delayMs)); + } + + throw new Error('No active editor found after retries'); +} + +/** + * Ensures response panel remains visible and active + */ +export async function ensureResponsePanelActive(panel: vscode.TextEditor, options: EditorRetryOptions = {}): Promise { + const opts = { ...DEFAULT_OPTIONS, ...options }; + const startTime = Date.now(); + + while (Date.now() - startTime < opts.timeout!) { + if (!panel.document.isClosed && vscode.window.visibleTextEditors.includes(panel)) { + return; + } + + try { + await vscode.window.showTextDocument(panel.document, panel.viewColumn); + return; + } catch (error) { + console.log('Retrying to ensure response panel visibility...', error); + await new Promise(resolve => setTimeout(resolve, opts.delayMs)); + } + } + + throw new EditorTimeoutError('Failed to keep response panel active'); +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 26d13d0..3bd8414 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode'; import { ClaudeApiService, DefaultClaudeApiService } from './services/claude-api'; import { ClaudeResponse } from './api'; import { Timeouts } from './config'; -import { waitForExtensionReady, ensureAllEditorsClosed, unregisterCommands } from './utils'; +import { waitForActiveEditor, ensureResponsePanelActive, EditorTimeoutError } from './editor-utils'; // Global state management let registeredCommands: vscode.Disposable[] = []; @@ -71,23 +71,6 @@ export async function createResponsePanel(content: string): Promise { - // Link the progress cancellation to our token source progressToken.onCancellationRequested(() => { tokenSource.cancel(); }); - + + const prompt = mode === 'document' + ? `Please document this code:\n\n${text}` + : text; + return await apiService.askClaude(prompt, tokenSource.token); }); const formattedResponse = formatResponse(text, response, mode); - await createResponsePanel(formattedResponse); + const responseEditor = await createResponsePanel(formattedResponse); + + if (responseEditor) { + // Ensure the response panel stays active + await ensureResponsePanelActive(responseEditor, { + maxAttempts: 3, + delayMs: 100 + }); + } + } catch (error) { if (error instanceof vscode.CancellationError) { vscode.window.showInformationMessage('Request cancelled'); return; } + if (error instanceof EditorTimeoutError) { + vscode.window.showErrorMessage('Editor window management issue. Please try again.'); + return; + } const errorMessage = error instanceof Error ? error.message : 'Unknown error'; vscode.window.showErrorMessage(`Error: ${errorMessage}`); console.error('Error handling Claude request:', error); } finally { statusBarItem.dispose(); - tokenSource.dispose(); } } @@ -168,7 +177,7 @@ export async function activate(context: vscode.ExtensionContext, service?: Claud // Ensure previous commands are disposed and add safety timeout await deactivate(); await new Promise(resolve => setTimeout(resolve, Timeouts.ACTIVATION)); // Wait for cleanup - + // Initialize API service apiService = service || new DefaultClaudeApiService(); @@ -180,11 +189,11 @@ export async function activate(context: vscode.ExtensionContext, service?: Claud }), // Main commands - vscode.commands.registerCommand('claude-vscode.askClaude', () => + vscode.commands.registerCommand('claude-vscode.askClaude', () => handleClaudeRequest('general') ), - vscode.commands.registerCommand('claude-vscode.documentCode', () => + vscode.commands.registerCommand('claude-vscode.documentCode', () => handleClaudeRequest('document') ) ]; diff --git a/src/test-utils.ts b/src/test-utils.ts new file mode 100644 index 0000000..eafab41 --- /dev/null +++ b/src/test-utils.ts @@ -0,0 +1,66 @@ +// src/test-utils.ts +import * as vscode from 'vscode'; +import { waitForExtensionReady } from './utils'; + +export interface CleanupOptions { + timeout?: number; + retryDelay?: number; + maxRetries?: number; +} + +const DEFAULT_CLEANUP_OPTIONS: CleanupOptions = { + timeout: 1000, + retryDelay: 100, + maxRetries: 3 +}; + +/** + * Thorough cleanup of VS Code resources + */ +export async function thoroughCleanup(options: CleanupOptions = {}): Promise { + const opts = { ...DEFAULT_CLEANUP_OPTIONS, ...options }; + const startTime = Date.now(); + + // First attempt normal cleanup + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await waitForExtensionReady(opts.retryDelay); + + for (let attempt = 0; attempt < opts.maxRetries!; attempt++) { + if (Date.now() - startTime > opts.timeout!) { + console.warn('Cleanup timeout reached'); + break; + } + + // Force close any remaining editors + if (vscode.window.visibleTextEditors.length > 0) { + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await waitForExtensionReady(opts.retryDelay); + } + + // Explicit disposal of tab resources + vscode.window.tabGroups.all.forEach(group => { + group.tabs.forEach(tab => { + try { + if (tab.input && typeof tab.input === 'object' && 'dispose' in tab.input) { + (tab.input as { dispose: () => void }).dispose(); + } + } catch (error) { + console.warn('Tab disposal error:', error); + } + }); + }); + + // Force garbage collection if available + if (global.gc) { + global.gc(); + } + + // Break if everything is cleaned up + if (vscode.window.visibleTextEditors.length === 0 && + vscode.window.tabGroups.all.every(group => group.tabs.length === 0)) { + break; + } + + await waitForExtensionReady(opts.retryDelay); + } +} \ No newline at end of file diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 753dae4..0a78664 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -5,6 +5,7 @@ import * as extension from '../../src/extension'; import { cleanupPanelsAndEditors, createResponsePanel } from '../../src/extension'; import { waitForExtensionReady, ensureAllEditorsClosed } from '../../src/utils'; import { ClaudeResponse } from '../../src/api'; +import { thoroughCleanup } from '../../src/test-utils'; interface ClaudeApiService { askClaude(text: string, token?: vscode.CancellationToken): Promise; @@ -58,10 +59,17 @@ suite('Claude Extension Test Suite', () => { test('Multiple Panel Resource Management', async function () { this.timeout(45000); const panelCount = 3; + + // Initial cleanup and GC + await thoroughCleanup(); + if (global.gc) global.gc(); + await waitForExtensionReady(500); + const initialMemory = process.memoryUsage(); try { for (let i = 0; i < panelCount; i++) { + // Create document and show const doc = await vscode.workspace.openTextDocument({ content: `Test content ${i + 1}`, language: 'markdown' @@ -73,25 +81,35 @@ suite('Claude Extension Test Suite', () => { assert.ok(editor, `Panel ${i + 1} should be visible`); await vscode.commands.executeCommand('workbench.action.moveEditorToNextGroup'); + + // Cleanup after each panel + await thoroughCleanup({ timeout: 500, retryDelay: 50 }); + if (global.gc) global.gc(); await waitForExtensionReady(100); } - const editorCount = vscode.window.visibleTextEditors.filter( - editor => editor.document.languageId === 'markdown' - ).length; - assert.strictEqual(editorCount, panelCount); - - await cleanupPanelsAndEditors(); + // Final cleanup + await thoroughCleanup({ timeout: 1000, retryDelay: 100, maxRetries: 5 }); if (global.gc) global.gc(); + await waitForExtensionReady(500); const finalMemory = process.memoryUsage(); const memoryDiff = finalMemory.heapUsed - initialMemory.heapUsed; - assert.ok(memoryDiff < 5 * 1024 * 1024, 'Memory usage should not increase significantly'); + // More detailed memory logging for debugging + console.log('Memory usage:', { + initial: initialMemory.heapUsed / 1024 / 1024, + final: finalMemory.heapUsed / 1024 / 1024, + diff: memoryDiff / 1024 / 1024 + }); + + assert.ok(memoryDiff < 5 * 1024 * 1024, 'Memory usage should not increase significantly'); assert.strictEqual(vscode.window.visibleTextEditors.length, 0, 'All editors should be closed'); + } catch (error) { console.error('Test failed:', error); - await ensureAllEditorsClosed(5, 1000); + // One final cleanup attempt on failure + await thoroughCleanup({ timeout: 2000, retryDelay: 200, maxRetries: 5 }); throw error; } }); @@ -167,31 +185,31 @@ suite('Claude Extension Test Suite', () => { // Create a mock environment variable collection class MockEnvironmentVariableCollection implements vscode.EnvironmentVariableCollection { private variables = new Map(); - + [Symbol.iterator](): Iterator<[string, vscode.EnvironmentVariableMutator]> { return this.variables[Symbol.iterator](); } - + public get size(): number { return this.variables.size; } - + public clear(): void { this.variables.clear(); } - + public delete(variable: string): boolean { return this.variables.delete(variable); } - + public forEach(callback: (variable: string, mutator: vscode.EnvironmentVariableMutator, collection: vscode.EnvironmentVariableCollection) => void): void { this.variables.forEach((mutator, variable) => callback(variable, mutator, this)); } - + public get(variable: string): vscode.EnvironmentVariableMutator | undefined { return this.variables.get(variable); } - + public replace(variable: string, value: string): void { this.variables.set(variable, { value, @@ -199,7 +217,7 @@ suite('Claude Extension Test Suite', () => { options: { applyAtProcessCreation: true } }); } - + public append(variable: string, value: string): void { this.variables.set(variable, { value, @@ -207,7 +225,7 @@ suite('Claude Extension Test Suite', () => { options: { applyAtProcessCreation: true } }); } - + public prepend(variable: string, value: string): void { this.variables.set(variable, { value, @@ -215,15 +233,15 @@ suite('Claude Extension Test Suite', () => { options: { applyAtProcessCreation: true } }); } - + public get persistent(): boolean { return false; } - + public getScoped(scope: vscode.EnvironmentVariableScope): vscode.EnvironmentVariableCollection { return this; } - + public description: string | vscode.MarkdownString | undefined; }