diff --git a/src/kernels/deepnote/deepnoteServerStarter.node.ts b/src/kernels/deepnote/deepnoteServerStarter.node.ts index bee7c4f131..53c892cb92 100644 --- a/src/kernels/deepnote/deepnoteServerStarter.node.ts +++ b/src/kernels/deepnote/deepnoteServerStarter.node.ts @@ -18,6 +18,7 @@ import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from '../../platform/vscode-path/path'; import { generateUuid } from '../../platform/common/uuid'; +import { DeepnoteServerStartupError, DeepnoteServerTimeoutError } from '../../platform/errors/deepnoteKernelErrors'; /** * Lock file data structure for tracking server ownership @@ -42,6 +43,8 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension private readonly sessionId: string = generateUuid(); // Directory for lock files private readonly lockFileDir: string = path.join(os.tmpdir(), 'vscode-deepnote-locks'); + // Track server output for error reporting + private readonly serverOutputByFile: Map = new Map(); constructor( @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, @@ -118,12 +121,9 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension Cancellation.throwIfCanceled(token); - // Ensure toolkit is installed + // Ensure toolkit is installed (will throw typed errors on failure) logger.info(`Ensuring deepnote-toolkit is installed for ${fileKey}...`); - const installed = await this.toolkitInstaller.ensureInstalled(interpreter, deepnoteFileUri, token); - if (!installed) { - throw new Error('Failed to install deepnote-toolkit. Please check the output for details.'); - } + await this.toolkitInstaller.ensureInstalled(interpreter, deepnoteFileUri, token); Cancellation.throwIfCanceled(token); @@ -197,15 +197,27 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension const disposables: IDisposable[] = []; this.disposablesByFile.set(fileKey, disposables); + // Initialize output tracking for error reporting + this.serverOutputByFile.set(fileKey, { stdout: '', stderr: '' }); + // Monitor server output serverProcess.out.onDidChange( (output) => { + const outputTracking = this.serverOutputByFile.get(fileKey); if (output.source === 'stdout') { logger.trace(`Deepnote server (${fileKey}): ${output.out}`); this.outputChannel.appendLine(output.out); + if (outputTracking) { + // Keep last 5000 characters of output for error reporting + outputTracking.stdout = (outputTracking.stdout + output.out).slice(-5000); + } } else if (output.source === 'stderr') { logger.warn(`Deepnote server stderr (${fileKey}): ${output.out}`); this.outputChannel.appendLine(output.out); + if (outputTracking) { + // Keep last 5000 characters of error output for error reporting + outputTracking.stderr = (outputTracking.stderr + output.out).slice(-5000); + } } }, this, @@ -228,13 +240,35 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension try { const serverReady = await this.waitForServer(serverInfo, 120000, token); if (!serverReady) { + const output = this.serverOutputByFile.get(fileKey); await this.stopServerImpl(deepnoteFileUri); - throw new Error('Deepnote server failed to start within timeout period'); + + throw new DeepnoteServerTimeoutError(serverInfo.url, 120000, output?.stderr || undefined); } } catch (error) { - // Clean up leaked server before rethrowing + // If this is already a DeepnoteKernelError, clean up and rethrow it + if (error instanceof DeepnoteServerTimeoutError || error instanceof DeepnoteServerStartupError) { + await this.stopServerImpl(deepnoteFileUri); + throw error; + } + + // Capture output BEFORE cleaning up (stopServerImpl deletes it) + const output = this.serverOutputByFile.get(fileKey); + const capturedStdout = output?.stdout || ''; + const capturedStderr = output?.stderr || ''; + + // Clean up leaked server after capturing output await this.stopServerImpl(deepnoteFileUri); - throw error; + + // Wrap in a generic server startup error with captured output + throw new DeepnoteServerStartupError( + interpreter.uri.fsPath, + port, + 'unknown', + capturedStdout, + capturedStderr, + error instanceof Error ? error : undefined + ); } logger.info(`Deepnote server started successfully at ${url} for ${fileKey}`); @@ -283,6 +317,7 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension serverProcess.proc?.kill(); this.serverProcesses.delete(fileKey); this.serverInfos.delete(fileKey); + this.serverOutputByFile.delete(fileKey); this.outputChannel.appendLine(`Deepnote server stopped for ${fileKey}`); // Clean up lock file after stopping the server @@ -403,6 +438,7 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension this.serverInfos.clear(); this.disposablesByFile.clear(); this.pendingOperations.clear(); + this.serverOutputByFile.clear(); logger.info('DeepnoteServerStarter disposed successfully'); } diff --git a/src/kernels/deepnote/deepnoteToolkitInstaller.node.ts b/src/kernels/deepnote/deepnoteToolkitInstaller.node.ts index 86efba92ac..403235623b 100644 --- a/src/kernels/deepnote/deepnoteToolkitInstaller.node.ts +++ b/src/kernels/deepnote/deepnoteToolkitInstaller.node.ts @@ -11,6 +11,7 @@ import { IOutputChannel, IExtensionContext } from '../../platform/common/types'; import { STANDARD_OUTPUT_CHANNEL } from '../../platform/common/constants'; import { IFileSystem } from '../../platform/common/platform/types'; import { Cancellation } from '../../platform/common/cancellation'; +import { DeepnoteVenvCreationError, DeepnoteToolkitInstallError } from '../../platform/errors/deepnoteKernelErrors'; /** * Handles installation of the deepnote-toolkit Python package. @@ -19,7 +20,7 @@ import { Cancellation } from '../../platform/common/cancellation'; export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller { private readonly venvPythonPaths: Map = new Map(); // Track in-flight installations per venv path to prevent concurrent installs - private readonly pendingInstallations: Map> = new Map(); + private readonly pendingInstallations: Map> = new Map(); constructor( @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, @@ -62,7 +63,7 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller { baseInterpreter: PythonEnvironment, deepnoteFileUri: Uri, token?: CancellationToken - ): Promise { + ): Promise { const venvPath = this.getVenvPath(deepnoteFileUri); const venvKey = venvPath.fsPath; @@ -119,7 +120,7 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller { deepnoteFileUri: Uri, venvPath: Uri, token?: CancellationToken - ): Promise { + ): Promise { try { Cancellation.throwIfCanceled(token); @@ -158,7 +159,12 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller { logger.error(`venv stderr: ${venvResult.stderr}`); } this.outputChannel.appendLine('Error: Failed to create virtual environment'); - return undefined; + + throw new DeepnoteVenvCreationError( + baseInterpreter.uri.fsPath, + venvPath.fsPath, + venvResult.stderr || 'Virtual environment was created but Python interpreter not found' + ); } // Use undefined as resource to get full system environment (including git in PATH) @@ -250,12 +256,33 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller { } else { logger.error('deepnote-toolkit installation failed'); this.outputChannel.appendLine('✗ deepnote-toolkit installation failed'); - return undefined; + + throw new DeepnoteToolkitInstallError( + venvInterpreter.uri.fsPath, + venvPath.fsPath, + DEEPNOTE_TOOLKIT_WHEEL_URL, + installResult.stdout || '', + installResult.stderr || 'Package installation completed but verification failed' + ); } } catch (ex) { + // If this is already a DeepnoteKernelError, rethrow it without wrapping + if (ex instanceof DeepnoteVenvCreationError || ex instanceof DeepnoteToolkitInstallError) { + throw ex; + } + + // Otherwise, log full details and wrap in a generic toolkit install error logger.error(`Failed to set up deepnote-toolkit: ${ex}`); - this.outputChannel.appendLine(`Error setting up deepnote-toolkit: ${ex}`); - return undefined; + this.outputChannel.appendLine('Failed to set up deepnote-toolkit; see logs for details'); + + throw new DeepnoteToolkitInstallError( + baseInterpreter.uri.fsPath, + venvPath.fsPath, + DEEPNOTE_TOOLKIT_WHEEL_URL, + '', + ex instanceof Error ? ex.message : String(ex), + ex instanceof Error ? ex : undefined + ); } } diff --git a/src/kernels/deepnote/types.ts b/src/kernels/deepnote/types.ts index ed76eb919e..de16f84a68 100644 --- a/src/kernels/deepnote/types.ts +++ b/src/kernels/deepnote/types.ts @@ -72,13 +72,15 @@ export interface IDeepnoteToolkitInstaller { * @param baseInterpreter The base Python interpreter to use for creating the venv * @param deepnoteFileUri The URI of the .deepnote file (used to create a unique venv per file) * @param token Cancellation token to cancel the operation - * @returns The Python interpreter from the venv if installed successfully, undefined otherwise + * @returns The Python interpreter from the venv + * @throws {DeepnoteVenvCreationError} If venv creation fails + * @throws {DeepnoteToolkitInstallError} If toolkit installation fails */ ensureInstalled( baseInterpreter: PythonEnvironment, deepnoteFileUri: vscode.Uri, token?: vscode.CancellationToken - ): Promise; + ): Promise; /** * Gets the venv Python interpreter if toolkit is installed, undefined otherwise. diff --git a/src/notebooks/deepnote/deepnoteKernelAutoSelector.node.ts b/src/notebooks/deepnote/deepnoteKernelAutoSelector.node.ts index 9c506affdf..a74f79713f 100644 --- a/src/notebooks/deepnote/deepnoteKernelAutoSelector.node.ts +++ b/src/notebooks/deepnote/deepnoteKernelAutoSelector.node.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { inject, injectable, optional } from 'inversify'; +import { inject, injectable, optional, named } from 'inversify'; import { CancellationToken, NotebookDocument, @@ -13,7 +13,8 @@ import { NotebookController, CancellationTokenSource, Disposable, - l10n + l10n, + env } from 'vscode'; import { IExtensionSyncActivationService } from '../../platform/activation/types'; import { IDisposableRegistry } from '../../platform/common/types'; @@ -42,6 +43,9 @@ import { IDeepnoteNotebookManager } from '../types'; import { IDeepnoteRequirementsHelper } from './deepnoteRequirementsHelper.node'; import { DeepnoteProject } from './deepnoteTypes'; import { IKernelProvider, IKernel } from '../../kernels/types'; +import { DeepnoteKernelError } from '../../platform/errors/deepnoteKernelErrors'; +import { STANDARD_OUTPUT_CHANNEL } from '../../platform/common/constants'; +import { IOutputChannel } from '../../platform/common/types'; /** * Automatically selects and starts Deepnote kernel for .deepnote notebooks @@ -78,7 +82,8 @@ export class DeepnoteKernelAutoSelector implements IDeepnoteKernelAutoSelector, @inject(IDeepnoteInitNotebookRunner) private readonly initNotebookRunner: IDeepnoteInitNotebookRunner, @inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager, @inject(IKernelProvider) private readonly kernelProvider: IKernelProvider, - @inject(IDeepnoteRequirementsHelper) private readonly requirementsHelper: IDeepnoteRequirementsHelper + @inject(IDeepnoteRequirementsHelper) private readonly requirementsHelper: IDeepnoteRequirementsHelper, + @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel ) {} public activate() { @@ -129,9 +134,7 @@ export class DeepnoteKernelAutoSelector implements IDeepnoteKernelAutoSelector, // Don't await - let it happen in background so notebook opens quickly void this.ensureKernelSelected(notebook).catch((error) => { logger.error(`Failed to auto-select Deepnote kernel for ${getDisplayPath(notebook.uri)}`, error); - void window.showErrorMessage( - l10n.t('Failed to load Deepnote kernel. Please check the output for details.') - ); + void this.handleKernelSelectionError(error); }); } @@ -342,16 +345,13 @@ export class DeepnoteKernelAutoSelector implements IDeepnoteKernelAutoSelector, logger.info(`Using base interpreter: ${getDisplayPath(interpreter.uri)}`); // Ensure deepnote-toolkit is installed in a venv and get the venv interpreter + // Will throw typed errors on failure (DeepnoteVenvCreationError or DeepnoteToolkitInstallError) progress.report({ message: l10n.t('Installing Deepnote toolkit...') }); const venvInterpreter = await this.toolkitInstaller.ensureInstalled( interpreter, baseFileUri, progressToken ); - if (!venvInterpreter) { - logger.error('Failed to set up Deepnote toolkit environment'); - return; // Exit gracefully - } logger.info(`Deepnote toolkit venv ready at: ${getDisplayPath(venvInterpreter.uri)}`); @@ -553,4 +553,61 @@ export class DeepnoteKernelAutoSelector implements IDeepnoteKernelAutoSelector, this.loadingControllers.set(notebookKey, loadingController); logger.info(`Created loading controller for ${notebookKey}`); } + + /** + * Handle kernel selection errors with user-friendly messages and actions + */ + private async handleKernelSelectionError(error: unknown): Promise { + // Handle DeepnoteKernelError types with specific guidance + if (error instanceof DeepnoteKernelError) { + // Log the technical details + logger.error(error.getErrorReport()); + + // Show user-friendly error with actions + const showOutputAction = l10n.t('Show Output'); + const copyErrorAction = l10n.t('Copy Error Details'); + const actions: string[] = [showOutputAction, copyErrorAction]; + + const troubleshootingHeader = l10n.t('Troubleshooting:'); + const troubleshootingSteps = error.troubleshootingSteps + .slice(0, 3) + .map((step, i) => `${i + 1}. ${step}`) + .join('\n'); + + const selectedAction = await window.showErrorMessage( + `${error.userMessage}\n\n${troubleshootingHeader}\n${troubleshootingSteps}`, + { modal: false }, + ...actions + ); + + if (selectedAction === showOutputAction) { + this.outputChannel.show(); + } else if (selectedAction === copyErrorAction) { + try { + await env.clipboard.writeText(error.getErrorReport()); + void window.showInformationMessage(l10n.t('Error details copied to clipboard')); + } catch (clipboardError) { + logger.error('Failed to copy error details to clipboard', clipboardError); + void window.showErrorMessage(l10n.t('Failed to copy error details to clipboard')); + } + } + + return; + } + + // Handle generic errors + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Deepnote kernel error: ${errorMessage}`); + + const showOutputAction = l10n.t('Show Output'); + const selectedAction = await window.showErrorMessage( + l10n.t('Failed to load Deepnote kernel: {0}', errorMessage), + { modal: false }, + showOutputAction + ); + + if (selectedAction === showOutputAction) { + this.outputChannel.show(); + } + } } diff --git a/src/platform/errors/deepnoteKernelErrors.ts b/src/platform/errors/deepnoteKernelErrors.ts new file mode 100644 index 0000000000..9a21be4e44 --- /dev/null +++ b/src/platform/errors/deepnoteKernelErrors.ts @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Base class for Deepnote kernel-related errors with troubleshooting guidance + */ +export abstract class DeepnoteKernelError extends Error { + /** + * User-friendly error message + */ + public abstract readonly userMessage: string; + + /** + * Detailed technical information about the error + */ + public abstract readonly technicalDetails: string; + + /** + * Actionable troubleshooting steps for the user + */ + public abstract readonly troubleshootingSteps: string[]; + + /** + * Get a formatted error report for copying/sharing + */ + public getErrorReport(): string { + const lines = [ + '=== Deepnote Kernel Error Report ===', + '', + 'Error Type: ' + this.constructor.name, + 'Message: ' + this.userMessage, + '', + 'Technical Details:', + this.technicalDetails, + '', + 'Troubleshooting Steps:' + ]; + + this.troubleshootingSteps.forEach((step, index) => { + lines.push(`${index + 1}. ${step}`); + }); + + return lines.join('\n'); + } +} + +/** + * Error thrown when virtual environment creation fails + */ +export class DeepnoteVenvCreationError extends DeepnoteKernelError { + public readonly userMessage: string; + public readonly technicalDetails: string; + public readonly troubleshootingSteps: string[]; + + constructor( + public readonly pythonPath: string, + public readonly venvPath: string, + public readonly stderr: string, + cause?: Error + ) { + super(`Failed to create virtual environment for Deepnote toolkit`); + this.name = 'DeepnoteVenvCreationError'; + + this.userMessage = 'Failed to create virtual environment for Deepnote toolkit'; + + this.technicalDetails = [ + `Python interpreter: ${pythonPath}`, + `Target venv path: ${venvPath}`, + stderr ? `Error output:\n${stderr}` : 'No error output available', + cause ? `Underlying error: ${cause.message}` : '' + ] + .filter(Boolean) + .join('\n'); + + this.troubleshootingSteps = [ + 'Ensure Python is correctly installed and accessible', + 'Check that the Python interpreter has the "venv" module (try: python -m venv --help)', + 'Verify you have write permissions to the extension storage directory', + 'Check available disk space', + 'Try selecting a different Python interpreter in VS Code', + 'If using a system Python, consider installing a dedicated Python distribution (e.g., from python.org)' + ]; + + // Preserve the original error's stack trace if available + if (cause && cause.stack) { + this.stack = cause.stack; + } + } +} + +/** + * Error thrown when deepnote-toolkit package installation fails + */ +export class DeepnoteToolkitInstallError extends DeepnoteKernelError { + public readonly userMessage: string; + public readonly technicalDetails: string; + public readonly troubleshootingSteps: string[]; + + constructor( + public readonly pythonPath: string, + public readonly venvPath: string, + public readonly packageUrl: string, + public readonly stdout: string, + public readonly stderr: string, + cause?: Error + ) { + super(`Failed to install deepnote-toolkit package`); + this.name = 'DeepnoteToolkitInstallError'; + + this.userMessage = 'Failed to install deepnote-toolkit package'; + + this.technicalDetails = [ + `Python interpreter: ${pythonPath}`, + `Virtual environment: ${venvPath}`, + `Package URL: ${packageUrl}`, + stdout ? `Installation output:\n${stdout}` : '', + stderr ? `Error output:\n${stderr}` : 'No error output available', + cause ? `Underlying error: ${cause.message}` : '' + ] + .filter(Boolean) + .join('\n'); + + // Detect common error patterns and provide specific guidance + const hasNetworkError = + stderr.toLowerCase().includes('could not find a version') || + stderr.toLowerCase().includes('connection') || + stderr.toLowerCase().includes('timeout') || + stderr.toLowerCase().includes('ssl') || + stderr.toLowerCase().includes('certificate'); + + const hasPermissionError = + stderr.toLowerCase().includes('permission denied') || stderr.toLowerCase().includes('access is denied'); + + const hasDependencyError = + stderr.toLowerCase().includes('no matching distribution') || + stderr.toLowerCase().includes('could not find a version that satisfies'); + + this.troubleshootingSteps = [ + ...(hasNetworkError + ? [ + 'Check your internet connection', + 'If behind a corporate firewall/proxy, configure pip proxy settings', + 'Try disabling VPN temporarily' + ] + : []), + ...(hasPermissionError + ? [ + 'Check file permissions in the extension storage directory', + 'Try running VS Code with appropriate permissions' + ] + : []), + ...(hasDependencyError + ? [ + 'Verify your Python version is compatible (Python 3.8+ required)', + 'Try upgrading pip: python -m pip install --upgrade pip' + ] + : []), + 'Ensure pip is working correctly: python -m pip --version', + 'Check that you can access the package URL in a browser', + 'Try manually installing: pip install deepnote-toolkit', + 'Check the Output panel for detailed installation logs', + 'If the issue persists, report it with the error details' + ]; + + if (cause && cause.stack) { + this.stack = cause.stack; + } + } +} + +/** + * Error thrown when the Deepnote server fails to start + */ +export class DeepnoteServerStartupError extends DeepnoteKernelError { + public readonly userMessage: string; + public readonly technicalDetails: string; + public readonly troubleshootingSteps: string[]; + + constructor( + public readonly pythonPath: string, + public readonly port: number, + public readonly reason: 'process_failed' | 'health_check_failed' | 'unknown', + public readonly stdout: string, + public readonly stderr: string, + cause?: Error + ) { + super(`Deepnote server failed to start`); + this.name = 'DeepnoteServerStartupError'; + + this.userMessage = 'Deepnote server failed to start'; + + this.technicalDetails = [ + `Python interpreter: ${pythonPath}`, + `Port: ${port}`, + `Failure reason: ${reason}`, + stdout ? `Server output:\n${stdout}` : '', + stderr ? `Server errors:\n${stderr}` : 'No error output available', + cause ? `Underlying error: ${cause.message}` : '' + ] + .filter(Boolean) + .join('\n'); + + // Detect common error patterns + const hasPortConflict = + stderr.toLowerCase().includes('address already in use') || + (stderr.toLowerCase().includes('port') && stderr.toLowerCase().includes('in use')); + + const hasModuleError = + stderr.toLowerCase().includes('no module named') || + stderr.toLowerCase().includes('modulenotfounderror') || + stderr.toLowerCase().includes('importerror'); + + const hasPermissionError = stderr.toLowerCase().includes('permission denied'); + + this.troubleshootingSteps = [ + ...(hasPortConflict + ? [ + `Port ${port} is already in use by another application`, + 'Close other Jupyter servers or applications using that port', + 'Restart VS Code to clean up orphaned server processes' + ] + : []), + ...(hasModuleError + ? [ + 'The deepnote-toolkit package may not be correctly installed', + 'Try reloading the VS Code window to trigger reinstallation', + 'Check the Output panel for package installation errors' + ] + : []), + ...(hasPermissionError + ? [ + 'Check that the server has permission to bind to the port', + 'Verify firewall settings are not blocking local connections' + ] + : []), + 'Check the Output panel for detailed server logs', + 'Ensure no antivirus software is blocking Python', + 'Try closing and reopening the notebook file', + 'Reload the VS Code window (Cmd/Ctrl+Shift+P → "Reload Window")', + 'If the issue persists, report it with the error details' + ]; + + if (cause && cause.stack) { + this.stack = cause.stack; + } + } +} + +/** + * Error thrown when the Deepnote server fails to become ready within timeout + */ +export class DeepnoteServerTimeoutError extends DeepnoteKernelError { + public readonly userMessage: string; + public readonly technicalDetails: string; + public readonly troubleshootingSteps: string[]; + + constructor( + public readonly serverUrl: string, + public readonly timeoutMs: number, + public readonly lastError?: string + ) { + super(`Deepnote server failed to start within ${timeoutMs / 1000} seconds`); + this.name = 'DeepnoteServerTimeoutError'; + + this.userMessage = `Deepnote server failed to start within ${timeoutMs / 1000} seconds`; + + this.technicalDetails = [ + `Server URL: ${serverUrl}`, + `Timeout: ${timeoutMs}ms`, + lastError ? `Last connection error: ${lastError}` : 'Server process started but health check failed', + 'The server process may still be starting or may have crashed silently' + ] + .filter(Boolean) + .join('\n'); + + this.troubleshootingSteps = [ + 'The server may be slow to start - try waiting a bit longer and reloading', + 'Check the Output panel for server startup logs', + 'Ensure no firewall is blocking localhost connections', + 'Check that port is not being blocked by security software', + 'Verify Python and deepnote-toolkit are correctly installed', + 'Try closing other resource-intensive applications', + 'Reload the VS Code window (Cmd/Ctrl+Shift+P → "Reload Window")', + 'If the issue persists, report it with the error details' + ]; + } +} + +/** + * Error thrown when Python interpreter is not found or invalid + */ +export class DeepnotePythonNotFoundError extends DeepnoteKernelError { + public readonly userMessage: string; + public readonly technicalDetails: string; + public readonly troubleshootingSteps: string[]; + + constructor(public readonly attemptedPath?: string) { + super('Python interpreter not found'); + this.name = 'DeepnotePythonNotFoundError'; + + this.userMessage = 'No Python interpreter found for Deepnote kernel'; + + this.technicalDetails = attemptedPath + ? `Attempted to use: ${attemptedPath}` + : 'No Python interpreter is selected in VS Code'; + + this.troubleshootingSteps = [ + 'Install Python from python.org (Python 3.8 or later required)', + 'Install the Python extension for VS Code if not already installed', + 'Select a Python interpreter: Cmd/Ctrl+Shift+P → "Python: Select Interpreter"', + 'Ensure the selected Python interpreter is accessible from VS Code', + 'Reload the VS Code window after installing Python', + 'Check the Output panel for more details' + ]; + } +}