Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
419 changes: 356 additions & 63 deletions DEEPNOTE_KERNEL_IMPLEMENTATION.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ This extension allows you to work with Deepnote notebooks in VS Code. Deepnote n
- **More block types** - Choose from SQL blocks, chart blocks, and more specialized data science blocks
- **Seamless language switching** - Switch between Python and SQL seamlessly within the same notebook
- **Database integrations** - Connect directly to Postgres, Snowflake, BigQuery and more data sources
- **Init notebooks** - Automatically runs initialization code (like dependency installation) before your notebooks execute
- **Project requirements** - Automatically creates `requirements.txt` from your project settings for easy dependency management

## Useful commands

Expand Down
2 changes: 2 additions & 0 deletions src/extension.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import { Common } from './platform/common/utils/localize';
import { IServiceContainer, IServiceManager } from './platform/ioc/types';
import { initializeLoggers as init, logger } from './platform/logging';
import { ILogger } from './platform/logging/types';
import { getJupyterOutputChannel } from './standalone/devTools/jupyterOutputChannel';
import { isUsingPylance } from './standalone/intellisense/notebookPythonPathService';
import { noop } from './platform/common/utils/misc';
Expand Down Expand Up @@ -127,6 +128,7 @@ export function initializeGlobals(
getJupyterOutputChannel(context.subscriptions),
JUPYTER_OUTPUT_CHANNEL
);
serviceManager.addSingletonInstance<ILogger>(ILogger, logger);

return [serviceManager, serviceContainer];
}
Expand Down
2 changes: 1 addition & 1 deletion src/kernels/deepnote/deepnoteToolkitInstaller.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller {
}
}

private getVenvHash(deepnoteFileUri: Uri): string {
public getVenvHash(deepnoteFileUri: Uri): string {
// Create a short hash from the file path for kernel naming and venv directory
// This provides better uniqueness and prevents directory structure leakage
const path = deepnoteFileUri.fsPath;
Expand Down
7 changes: 7 additions & 0 deletions src/kernels/deepnote/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ export interface IDeepnoteToolkitInstaller {
* @param deepnoteFileUri The URI of the .deepnote file
*/
getVenvInterpreter(deepnoteFileUri: vscode.Uri): Promise<PythonEnvironment | undefined>;

/**
* Gets the hash for the venv directory/kernel spec name based on file path.
* @param deepnoteFileUri The URI of the .deepnote file
* @returns The hash string used for venv directory and kernel spec naming
*/
getVenvHash(deepnoteFileUri: vscode.Uri): string;
}

export const IDeepnoteServerStarter = Symbol('IDeepnoteServerStarter');
Expand Down
286 changes: 286 additions & 0 deletions src/notebooks/deepnote/deepnoteInitNotebookRunner.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import { inject, injectable } from 'inversify';
import { NotebookDocument, ProgressLocation, window, CancellationTokenSource, CancellationToken } from 'vscode';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Localize user‑facing progress strings

Wrap titles with l10n.t() per guidelines.

As per coding guidelines

-import { NotebookDocument, ProgressLocation, window, CancellationTokenSource, CancellationToken } from 'vscode';
+import { NotebookDocument, ProgressLocation, window, CancellationTokenSource, CancellationToken, l10n } from 'vscode';
@@
-                    title: `🚀 Initializing project environment`,
+                    title: l10n.t('🚀 Initializing project environment'),
@@
-                            title: `Init: "${initNotebook.name}"`,
+                            title: l10n.t('Init: "{0}"', initNotebook.name),

Also applies to: 136-147

🤖 Prompt for AI Agents
In src/notebooks/deepnote/deepnoteInitNotebookRunner.node.ts around line 2 (and
also apply the same change to lines 136-147), user-facing progress titles are
not localized; update all ProgressLocation/window progress title strings to use
l10n.t(...)—import l10n if not present—and replace raw literal titles with
l10n.t('your.key', { default: 'Original Title' }) or appropriate keys, ensuring
each progress message uses a unique key and preserves existing
interpolation/formatting.

import { logger } from '../../platform/logging';
import { IDeepnoteNotebookManager } from '../types';
import { DeepnoteProject, DeepnoteNotebook } from './deepnoteTypes';
import { IKernelProvider } from '../../kernels/types';
import { getDisplayPath } from '../../platform/common/platform/fs-paths';

/**
* Service responsible for running init notebooks before the main notebook starts.
* Init notebooks typically contain setup code like pip installs.
*/
@injectable()
export class DeepnoteInitNotebookRunner {
constructor(
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager,
@inject(IKernelProvider) private readonly kernelProvider: IKernelProvider
) {}

/**
* Runs the init notebook if it exists and hasn't been run yet for this project.
* This should be called after the kernel is started but before user code executes.
* @param notebook The notebook document
* @param projectId The Deepnote project ID
* @param token Optional cancellation token to stop execution if notebook is closed
*/
async runInitNotebookIfNeeded(
projectId: string,
notebook: NotebookDocument,
token?: CancellationToken
): Promise<void> {
try {
// Check for cancellation before starting
if (token?.isCancellationRequested) {
logger.info(`Init notebook cancelled before start for project ${projectId}`);
return;
}

// Check if init notebook has already run for this project
if (this.notebookManager.hasInitNotebookBeenRun(projectId)) {
logger.info(`Init notebook already ran for project ${projectId}, skipping`);
return;
}

if (token?.isCancellationRequested) {
logger.info(`Init notebook cancelled for project ${projectId}`);
return;
}

// Get the project data
const project = this.notebookManager.getOriginalProject(projectId) as DeepnoteProject | undefined;
if (!project) {
logger.warn(`Project ${projectId} not found, cannot run init notebook`);
return;
}

// Check if project has an init notebook ID
const initNotebookId = (project.project as { initNotebookId?: string }).initNotebookId;
if (!initNotebookId) {
logger.info(`No init notebook configured for project ${projectId}`);
// Mark as run so we don't check again
this.notebookManager.markInitNotebookAsRun(projectId);
return;
}

// Find the init notebook
const initNotebook = project.project.notebooks.find((nb) => nb.id === initNotebookId);
if (!initNotebook) {
logger.warn(
`Init notebook ${initNotebookId} not found in project ${projectId}, skipping initialization`
);
this.notebookManager.markInitNotebookAsRun(projectId);
return;
}

if (token?.isCancellationRequested) {
logger.info(`Init notebook cancelled before execution for project ${projectId}`);
return;
}

logger.info(`Running init notebook "${initNotebook.name}" (${initNotebookId}) for project ${projectId}`);

// Execute the init notebook with progress
const success = await this.executeInitNotebook(notebook, initNotebook, token);

if (success) {
// Mark as run so we don't run it again
this.notebookManager.markInitNotebookAsRun(projectId);
logger.info(`Init notebook completed successfully for project ${projectId}`);
} else {
logger.warn(`Init notebook did not execute for project ${projectId} - kernel not available`);
}
} catch (error) {
// Check if this is a cancellation error
if (error instanceof Error && error.message === 'Cancelled') {
logger.info(`Init notebook cancelled for project ${projectId}`);
return;
}
// Log error but don't throw - we want to let user continue anyway
logger.error(`Error running init notebook for project ${projectId}:`, error);
// Still mark as run to avoid retrying on every notebook open
this.notebookManager.markInitNotebookAsRun(projectId);
}
}

/**
* Executes the init notebook's code blocks in the kernel.
* @param notebook The notebook document (for kernel context)
* @param initNotebook The init notebook to execute
* @param token Optional cancellation token from parent operation
* @returns True if execution completed, false if kernel was not available
*/
private async executeInitNotebook(
notebook: NotebookDocument,
initNotebook: DeepnoteNotebook,
token?: CancellationToken
): Promise<boolean> {
// Check for cancellation before starting
if (token?.isCancellationRequested) {
logger.info(`Init notebook execution cancelled before start`);
return false;
}

// Show progress in both notification AND window for maximum visibility
const cancellationTokenSource = new CancellationTokenSource();

// Link parent token to our local token if provided
const tokenDisposable = token?.onCancellationRequested(() => {
cancellationTokenSource.cancel();
});

// Create a wrapper that reports to both progress locations
const executeWithDualProgress = async () => {
return window.withProgress(
{
location: ProgressLocation.Notification,
title: `🚀 Initializing project environment`,
cancellable: false
},
async (notificationProgress) => {
return window.withProgress(
{
location: ProgressLocation.Window,
title: `Init: "${initNotebook.name}"`,
cancellable: false
},
async (windowProgress) => {
// Helper to report to both progress bars
const reportProgress = (message: string, increment: number) => {
notificationProgress.report({ message, increment });
windowProgress.report({ message, increment });
};

return this.executeInitNotebookImpl(
notebook,
initNotebook,
reportProgress,
cancellationTokenSource.token
);
}
);
}
);
};

try {
return await executeWithDualProgress();
} finally {
tokenDisposable?.dispose();
cancellationTokenSource.dispose();
}
}

private async executeInitNotebookImpl(
notebook: NotebookDocument,
initNotebook: DeepnoteNotebook,
progress: (message: string, increment: number) => void,
token: CancellationToken
): Promise<boolean> {
try {
// Check for cancellation
if (token.isCancellationRequested) {
logger.info(`Init notebook execution cancelled`);
return false;
}

progress(`Running init notebook "${initNotebook.name}"...`, 0);

// Get the kernel for this notebook
// Note: This should always exist because onKernelStarted already fired
const kernel = this.kernelProvider.get(notebook);
if (!kernel) {
logger.error(
`No kernel found for ${getDisplayPath(
notebook.uri
)} even after onDidStartKernel fired - this should not happen`
);
return false;
}

logger.info(`Kernel found for ${getDisplayPath(notebook.uri)}, starting init notebook execution`);

// Filter out non-code blocks
const codeBlocks = initNotebook.blocks.filter((block) => block.type === 'code');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have other types of executable blocks here soon that we will want to support. Can you expand it to include SQL, input blocks, and chart types?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since SQL blocks are not yet implemented in the extension, we agreed that it can be added to the init notebooks once implemented.


if (codeBlocks.length === 0) {
logger.info(`Init notebook has no code blocks, skipping execution`);
return true; // Not an error - just nothing to execute
}

logger.info(`Executing ${codeBlocks.length} code blocks from init notebook`);
progress(
`Preparing to execute ${codeBlocks.length} initialization ${
codeBlocks.length === 1 ? 'block' : 'blocks'
}...`,
5
);

// Check for cancellation
if (token.isCancellationRequested) {
logger.info(`Init notebook execution cancelled before starting blocks`);
return false;
}

// Get kernel execution
const kernelExecution = this.kernelProvider.getKernelExecution(kernel);

// Execute each code block sequentially
for (let i = 0; i < codeBlocks.length; i++) {
// Check for cancellation between blocks
if (token.isCancellationRequested) {
logger.info(`Init notebook execution cancelled after block ${i}`);
return false;
}

const block = codeBlocks[i];
const percentComplete = Math.floor((i / codeBlocks.length) * 100);

// Show more detailed progress with percentage
progress(
`[${percentComplete}%] Executing block ${i + 1} of ${codeBlocks.length}...`,
90 / codeBlocks.length // Reserve 5% for start, 5% for finish
);

logger.info(`Executing init notebook block ${i + 1}/${codeBlocks.length}`);

try {
// Execute the code silently in the background
const outputs = await kernelExecution.executeHidden(block.content ?? '');

// Log outputs for debugging
if (outputs && outputs.length > 0) {
logger.info(`Init notebook block ${i + 1} produced ${outputs.length} outputs`);

// Check for errors in outputs
const errors = outputs.filter(
(output: { output_type?: string }) => output.output_type === 'error'
);
if (errors.length > 0) {
logger.warn(`Init notebook block ${i + 1} produced errors:`, errors);
}
}
} catch (blockError) {
// Log error but continue with next block
logger.error(`Error executing init notebook block ${i + 1}:`, blockError);
}
}

logger.info(`Completed executing all init notebook blocks`);
progress(`✓ Initialization complete! Environment ready.`, 5);

// Give user a moment to see the completion message
await new Promise((resolve) => setTimeout(resolve, 1000));

return true;
} catch (error) {
logger.error(`Error in executeInitNotebook:`, error);
throw error;
}
}
}

export const IDeepnoteInitNotebookRunner = Symbol('IDeepnoteInitNotebookRunner');
export interface IDeepnoteInitNotebookRunner {
runInitNotebookIfNeeded(projectId: string, notebook: NotebookDocument, token?: CancellationToken): Promise<void>;
}
Loading