Repository: posit-dev/positron
Pull request #7696: 7285 in new jupyter projects the starting notebook doesnt connect to the runtime automatically
Author: login: nstrayer
Created: 2025-05-14T17:06:08Z | Merged: 2025-05-16T17:24:26Z

@:new-project-wizard

This PR fixes addresses #7285 where newly created Jupyter notebook projects didn't automatically connect to their runtime. 

When starting a new notebook project, users previously had to manually connect it to the runtime, which was confusing and unexpected. Now, the notebook automatically connects once the runtime becomes available.

The implementation uses a few key approaches:
1. Listens for runtime initialization and reacts when it's ready
2. Finds and connects the notebook to the appropriate runtime
3. Selects the best kernel based on the project's runtime
4. Cleans up event listeners once the connection is established

  ### Release Notes

  #### Bug Fixes

  - Fixed issue where new Jupyter notebook projects didn't automatically connect to their runtime

  ### QA Notes

  To test this change:
  1. Create a new Jupyter notebook project
  2. Verify the notebook automatically connects to the runtime after it has started up (no manual connection required)
  3. Confirm you can execute notebook cells without having to manually select a kernel
  4. This change affects the notebook project creation workflow, but shouldn't impact other project types


Diff:
diff --git a/src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts b/src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts
index 140dfc6fd0d4..7d0aecc5978b 100644
--- a/src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts
+++ b/src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts
@@ -3,7 +3,7 @@
  *  Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
  *--------------------------------------------------------------------------------------------*/
 
-import { Disposable } from '../../../../base/common/lifecycle.js';
+import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
 import { ISettableObservable, observableValue } from '../../../../base/common/observable.js';
 import { ICommandService } from '../../../../platform/commands/common/commands.js';
 import { ILogService } from '../../../../platform/log/common/log.js';
@@ -13,7 +13,7 @@ import { IWorkspaceTrustManagementService } from '../../../../platform/workspace
 import { CreateEnvironmentResult, IPositronNewProjectService, NewProjectConfiguration, NewProjectStartupPhase, NewProjectTask, NewProjectType, POSITRON_NEW_PROJECT_CONFIG_STORAGE_KEY } from './positronNewProject.js';
 import { Event } from '../../../../base/common/event.js';
 import { Barrier } from '../../../../base/common/async.js';
-import { ILanguageRuntimeMetadata } from '../../languageRuntime/common/languageRuntimeService.js';
+import { ILanguageRuntimeMetadata, LanguageRuntimeSessionMode } from '../../languageRuntime/common/languageRuntimeService.js';
 import { IFileService } from '../../../../platform/files/common/files.js';
 import { VSBuffer } from '../../../../base/common/buffer.js';
 import { joinPath, relativePath } from '../../../../base/common/resources.js';
@@ -21,7 +21,10 @@ import { DOT_IGNORE_JUPYTER, DOT_IGNORE_PYTHON, DOT_IGNORE_R } from './positronN
 import { URI } from '../../../../base/common/uri.js';
 import { INotificationService } from '../../../../platform/notification/common/notification.js';
 import { localize } from '../../../../nls.js';
-import { IRuntimeSessionService } from '../../runtimeSession/common/runtimeSessionService.js';
+import { IRuntimeSessionService, RuntimeStartMode } from '../../runtimeSession/common/runtimeSessionService.js';
+import { INotebookEditorService } from '../../../contrib/notebook/browser/services/notebookEditorService.js';
+import { INotebookKernel, INotebookKernelService } from '../../../contrib/notebook/common/notebookKernelService.js';
+import { INotebookTextModel } from '../../../contrib/notebook/common/notebookCommon.js';
 
 /**
  * PositronNewProjectService class.
@@ -53,6 +56,9 @@ export class PositronNewProjectService extends Disposable implements IPositronNe
 	// Runtime metadata for the new project
 	private _runtimeMetadata: ILanguageRuntimeMetadata | undefined;
 
+	// Add a single log prefix for notebook-startup related messages
+	private readonly _nbLogPrefix = '[New project notebook]';
+
 	// Create the Positron New Project service instance.
 	constructor(
 		@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
@@ -63,6 +69,8 @@ export class PositronNewProjectService extends Disposable implements IPositronNe
 		@IRuntimeSessionService private readonly _runtimeSessionService: IRuntimeSessionService,
 		@IStorageService private readonly _storageService: IStorageService,
 		@IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService,
+		@INotebookEditorService private readonly _notebookEditorService: INotebookEditorService,
+		@INotebookKernelService private readonly _notebookKernelService: INotebookKernelService,
 	) {
 		super();
 
@@ -236,9 +244,12 @@ export class PositronNewProjectService extends Disposable implements IPositronNe
 			case NewProjectType.RProject:
 				await this._commandService.executeCommand('r.createNewFile');
 				break;
-			case NewProjectType.JupyterNotebook:
-				await this._commandService.executeCommand('ipynb.newUntitledIpynb');
+			case NewProjectType.JupyterNotebook: {
+				// Use the languageId from the runtime metadata if available, otherwise use 'python' as a default because that's the most common language for Jupyter Notebooks.
+				const languageId = this._newProjectConfig?.runtimeMetadata?.languageId ?? 'python';
+				await this._commandService.executeCommand('ipynb.newUntitledIpynb', languageId);
 				break;
+			}
 			default:
 				this._logService.error(
 					'Cannot determine new file command for unknown project type',
@@ -473,8 +484,8 @@ export class PositronNewProjectService extends Disposable implements IPositronNe
 
 				// Check if the newly created runtime metadata was returned
 				if (!result.metadata) {
-					// Warn the user, but don't exit early. We'll still try to continue since the
-					// environment creation was successful.
+					// Warn the user, but don't exit early. We'll still try to
+					// continue since the environment creation was successful.
 					const message = this._failedPythonEnvMessage(`Could not determine interpreter metadata returned from ${createEnvCommand} command. The interpreter may need to be selected manually.`);
 					this._logService.error(message);
 					this._notificationService.warn(message);
@@ -533,6 +544,206 @@ export class PositronNewProjectService extends Disposable implements IPositronNe
 
 	//#endregion Extension Tasks
 
+
+	/**
+	 * Centralised validation helper for notebook connection.
+	 * Prefers the active notebook editor if present, otherwise falls back to the only open notebook editor.
+	 * If no notebook editors are open, or no active notebook editor is present when multiple are open, logs and notifies the user.
+	 *
+	 * @returns The validated context required to establish a runtime session, or `undefined` if validation failed.
+	 */
+	private _getNotebookContext(): { model: INotebookTextModel; runtimeId: string } | undefined {
+		const notebookEditors = this._notebookEditorService.listNotebookEditors();
+
+		// Prefer the active notebook editor if available
+		const activeEditor = (this._notebookEditorService as any).activeNotebookEditor as { textModel?: INotebookTextModel } | undefined;
+		const editor = activeEditor?.textModel
+			? activeEditor
+			: (notebookEditors.length === 1 ? notebookEditors[0] : undefined);
+
+		if (!editor || !editor.textModel) {
+			if (notebookEditors.length === 0) {
+				this._logService.debug(`${this._nbLogPrefix} No notebook editor found for connection.`);
+				this._notificationService.warn('No notebook editor is open. Please open a notebook to connect it to the new environment.');
+			} else {
+				this._logService.error(`${this._nbLogPrefix} No active notebook editor found among multiple open editors.`);
+				this._notificationService.error('Multiple notebook editors are open, but none is active. Please focus the notebook you want to connect.');
+			}
+			return undefined;
+		}
+
+		const textModel = editor.textModel;
+		if (!textModel.uri) {
+			this._logService.debug(`${this._nbLogPrefix} Notebook text model has no URI.`);
+			return undefined;
+		}
+
+		const runtimeId = this._runtimeMetadata?.runtimeId;
+		if (!runtimeId) {
+			this._logService.error(`${this._nbLogPrefix} No runtime ID available for connection.`);
+			return undefined;
+		}
+
+		return { model: textModel, runtimeId };
+	}
+
+	/**
+	 * Provides automatic notebook-to-runtime connection as the final step in notebook project creation.
+	 * This addresses issue #7285 where newly created notebooks weren't automatically connecting.
+	 * Without this connection, users would face confusing manual setup steps that contradict
+	 * the streamlined project creation experience we're aiming for.
+	 *
+	 * @param sessionListener The event listener for runtime start, used to dispose itself upon completion or error.
+	 */
+	private async _connectNotebookToRuntime(sessionListener: IDisposable): Promise<void> {
+		try {
+			// Validate context in a single place
+			const context = this._getNotebookContext();
+			if (!context) {
+				return; // All logging handled by helper
+			}
+
+			const { model, runtimeId } = context;
+			const sessionName = this._newProjectConfig?.projectName ?? 'New Project Notebook';
+
+			// Start / attach runtime session
+			try {
+				await this._runtimeSessionService.startNewRuntimeSession(
+					runtimeId,
+					sessionName,
+					LanguageRuntimeSessionMode.Notebook,
+					model.uri,
+					'New Project Notebook Creation',
+					RuntimeStartMode.Starting,
+					true
+				);
+				this._logService.debug(`${this._nbLogPrefix} Connected notebook ${model.uri.toString()} to runtime ${runtimeId}`);
+			} catch (error) {
+				this._logService.error(`${this._nbLogPrefix} Failed to connect notebook to runtime: ${String(error)}`);
+				// Show a user-facing error toast, but do not break the flow
+				this._notificationService.error('Failed to start the runtime environment for your new notebook. You may need to select or start an environment manually.');
+				return;
+			}
+
+			// Select kernel
+			await this._selectKernelForNotebook(model);
+		} catch (error) {
+			this._logService.error(`${this._nbLogPrefix} Error during post-init notebook setup: ${String(error)}`);
+			this._notificationService.error(localize('positronNewProjectService.postInitNotebookError', 'Error setting up notebook for new project: {0}', String(error)));
+		} finally {
+			sessionListener.dispose();
+		}
+	}
+
+	/**
+	 * Waits for a kernel matching the given runtimePath to be registered for the notebook.
+	 * Listens to notebookKernelService.onDidAddKernel to avoid polling. Falls back after 10 s.
+	 *
+	 * @param notebookTextModel The notebook text model to select a kernel for
+	 * @param runtimePath The interpreter path to match.
+	 */
+	private async _waitForKernelRegistration(
+		notebookTextModel: INotebookTextModel,
+		runtimePath: string,
+	): Promise<INotebookKernel | undefined> {
+		// Helper to find a matching kernel among currently known ones
+		const findMatch = (): INotebookKernel | undefined => {
+			const matching = this._notebookKernelService.getMatchingKernel(notebookTextModel);
+			return matching.all.find(k => k.description === runtimePath);
+		};
+
+		// Return immediately if a kernel is already registered
+		const existing = findMatch();
+		if (existing) {
+			return existing;
+		}
+
+		return new Promise<INotebookKernel | undefined>((resolve) => {
+			// Listener for newly-added kernels
+			const disposable = this._notebookKernelService.onDidAddKernel((kernel) => {
+				if (kernel.description === runtimePath) {
+					disposable.dispose();
+					resolve(kernel);
+				}
+			});
+
+			// Fallback timeout (10 s)
+			setTimeout(() => {
+				disposable.dispose();
+				resolve(undefined);
+			}, 10000);
+		});
+	}
+
+	/**
+	 * Ensures the notebook has a properly selected kernel that matches the project's runtime.
+	 * This eliminates the need for manual kernel selection when creating a new notebook project.
+	 *
+	 * If no suitable kernel is found, a user-facing error notification is shown, but the flow continues.
+	 *
+	 * @param notebookTextModel The notebook text model to select a kernel for
+	 */
+	private async _selectKernelForNotebook(notebookTextModel: INotebookTextModel) {
+		const runtimePath: string | undefined = this._runtimeMetadata?.runtimePath;
+		const languageId = this._runtimeMetadata?.languageId;
+		this._logService.debug('[New project startup] Kernel selection: runtimePath=', runtimePath);
+
+		const matchingInitial = this._notebookKernelService.getMatchingKernel(notebookTextModel);
+		// If a kernel is already selected and it doesn't match our runtime, decide if we should override.
+		if (runtimePath && matchingInitial.selected && matchingInitial.selected.description !== runtimePath) {
+			const hasExecuted = notebookTextModel.cells.some(cell => typeof cell.internalMetadata?.executionOrder === 'number');
+			if (!hasExecuted) {
+				this._logService.debug('[New project startup] Overriding pre-selected kernel because notebook has no execution history.');
+				// We simply clear the selection; the subsequent logic will select the correct kernel.
+				this._notebookKernelService.selectKernelForNotebook(undefined as any, notebookTextModel);
+			}
+		}
+
+		let kernelToSelect: INotebookKernel | undefined;
+
+		if (runtimePath) {
+			// Wait for the kernel to be registered if needed
+			kernelToSelect = await this._waitForKernelRegistration(notebookTextModel, runtimePath);
+			if (kernelToSelect) {
+				this._logService.debug(`[New project startup] Kernel '${kernelToSelect.id}' selected: exact match for runtimePath after polling.`);
+			}
+		}
+
+		if (!kernelToSelect) {
+			// Fallback to previous logic if polling failed
+			const matchingKernels = this._notebookKernelService.getMatchingKernel(notebookTextModel);
+			if (!runtimePath) {
+				for (const kernel of matchingKernels.all) {
+					if (kernel.extension.value.includes('positron') && languageId && kernel.supportedLanguages.includes(languageId)) {
+						this._logService.debug(`[New project startup] Kernel '${kernel.id}' selected: Positron-specific for language (no venv).`);
+						kernelToSelect = kernel;
+						break;
+					}
+				}
+			} else {
+				// If we have a runtimePath but polling failed, warn the user
+				this._logService.warn('[New project startup] No kernel registered for the new environment after waiting. Falling back to best available kernel.');
+				for (const kernel of matchingKernels.all) {
+					if (kernel.description && kernel.description.includes('.venv')) {
+						kernelToSelect = kernel;
+						break;
+					}
+				}
+			}
+		}
+
+		if (!kernelToSelect) {
+			// Show a user-facing error toast, but do not break the flow
+			this._notificationService.error('No matching Jupyter kernel was found for the new environment. You will need to select a kernel manually in the notebook.');
+			this._logService.debug(`[New project startup] No suitable kernel found for notebook`);
+			return;
+		}
+
+		this._notebookKernelService.selectKernelForNotebook(kernelToSelect, notebookTextModel);
+		this._logService.debug(`[New project startup] Selected kernel ${kernelToSelect.id} for notebook`);
+		return;
+	}
+
 	/**
 	 * Removes a pending init task.
 	 */
@@ -605,13 +816,25 @@ export class PositronNewProjectService extends Disposable implements IPositronNe
 			return new Set();
 		}
 
-		// listen for runtime to be created so that post-init tasks can be run
-		const sessionListener = this._runtimeSessionService.onDidStartRuntime(async (runtimeId) => {
-			this._logService.debug('[New project startup] Runtime created. Running post-init tasks.');
+		// Set up the runtime session listener
+		const sessionListener = this._runtimeSessionService.onDidStartRuntime(async (runtimeSession) => {
+			this._logService.debug(`[New project startup] Runtime ${runtimeSession.sessionId} created. Running post-init tasks.`);
 			this._startupPhase.set(NewProjectStartupPhase.PostInitialization, undefined);
-			sessionListener.dispose();
+
+			// If we've created a Jupyter Notebook project we need to connect the notebook to the runtime.
+			if (this._newProjectConfig?.projectType === NewProjectType.JupyterNotebook) {
+				// Pass the listener itself to the connection method to allow self-disposal
+				await this._connectNotebookToRuntime(sessionListener);
+			} else {
+				// For non-notebook projects, dispose the listener immediately
+				sessionListener.dispose();
+			}
 		});
 
+		// Register the listener with the service's disposables to ensure cleanup
+		// if the service is disposed before the listener self-disposes
+		this._register(sessionListener);
+
 		const tasks = new Set<NewProjectTask>();
 		if (this._newProjectConfig.useRenv) {
 			tasks.add(NewProjectTask.REnvironment);
diff --git a/test/e2e/tests/new-project-wizard/new-project-python.test.ts b/test/e2e/tests/new-project-wizard/new-project-python.test.ts
index 848271f68aee..3f91ef796cc5 100644
--- a/test/e2e/tests/new-project-wizard/new-project-python.test.ts
+++ b/test/e2e/tests/new-project-wizard/new-project-python.test.ts
@@ -72,6 +72,38 @@ test.describe('Python - New Project Wizard', { tag: [tags.MODAL, tags.NEW_PROJEC
 		await verifyProjectCreation(app, projectTitle);
 		await verifyVenEnvStarts(app);
 	});
+
+	test('Python Notebook Project - connects notebook to runtime on startup', async function ({ app }) {
+		const projectTitle = `python-notebook-runtime-${Math.floor(Math.random() * 1000000)}`;
+
+		// Create a new Python notebook project
+		await app.workbench.newProjectWizard.createNewProject({
+			type: ProjectType.JUPYTER_NOTEBOOK,
+			title: projectTitle,
+			status: 'new',
+			pythonEnv: 'venv', // or 'conda' if that's your default
+		});
+
+		// Wait for the notebook editor to be visible
+		const notebookEditorTab = app.code.driver.page.locator('[id="workbench.parts.editor"]').getByText('Untitled-1.ipynb', { exact: true });
+		await expect(notebookEditorTab).toBeVisible();
+
+		// Get the Python version from the session selector button
+		const sessionSelectorButton = app.code.driver.page.getByRole('button', { name: 'Session Selector' });
+		const sessionSelectorText = await sessionSelectorButton.textContent();
+		// Extract the version number (e.g., '3.10.12') from the button text
+		const versionMatch = sessionSelectorText && sessionSelectorText.match(/Python ([0-9]+\.[0-9]+\.[0-9]+)/);
+		const pythonVersion = versionMatch ? versionMatch[1] : undefined;
+		// Fail the test if we can't extract the version
+		expect(pythonVersion, 'Python version should be present in session selector').toBeTruthy();
+
+		// After the runtime starts up the kernel status should be replaced with the kernel name.
+		// The kernel name should contain the Python version from the session selector
+		// Only look within an 'a' tag with class 'kernel-label' to avoid false positives
+		const kernelLabel = app.code.driver.page.locator('a.kernel-label');
+		await expect(kernelLabel).toContainText(`Python ${pythonVersion}`);
+		await expect(kernelLabel).toContainText('.venv');
+	});
 });
 
 // Helper functions


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR addresses issue #7285 by automatically connecting new Jupyter notebook projects to the runtime upon startup. Key changes include:
- Passing a language ID as a parameter when creating new untitled notebooks.
- Adding helper functions to select the appropriate kernel and establish notebook-runtime sessions.
- Introducing a runtime session listener that triggers the post-initialization connection for notebooks.


<details>
<summary>Comments suppressed due to low confidence (3)</summary>

**src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts:658**
* [nitpick] When multiple notebook editors are found, the code logs an error and notifies the user, but this scenario might occasionally be valid. Consider handling this case more gracefully or adding documentation to explain why multiple editors are not supported.
```
if (notebookEditors.length > 1) {
```
**src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts:247**
* The addition of the languageId parameter to the 'ipynb.newUntitledIpynb' command is a useful enhancement; please verify that the corresponding command handler accepts this parameter to maintain backward compatibility.
```
await this._commandService.executeCommand('ipynb.newUntitledIpynb', languageId);
```
**src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts:792**
* [nitpick] By passing the sessionListener to the connection method and disposing it within, ensure that this pattern does not inadvertently dispose of valid listeners in scenarios where multiple runtime sessions might be expected.
```
await this._connectNotebookToRuntime(sessionListener);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T17:06:49Z
user: login: sharon-wang
body: A kernel now connects automatically for my new Jupyter Notebook project 👌 

For some reason though, the new notebook always connects to a 3.10.4 Python interpreter instead of my Venv:

<img width="1681" alt="image" src="https://github.com/user-attachments/assets/79a2b0d8-214a-47a9-bdcb-2b1692342cf6" />

I tried creating a Jupyter notebook project a few times with different Python interpreters, but the notebook always chose the 3.10.4 Python instead of the created Venv. I checked my interpreter-related settings and don't see something that would cause the 3.10.4 interpreter to be selected by default.

I'm also finding that sometimes the base interpreter (the one chosen in the Wizard to create the Venv from) gets started instead of the new Venv in the console:

<img width="1063" alt="image" src="https://github.com/user-attachments/assets/3b35f069-8ecc-496a-a3e6-bd9d5d987de5" />
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T17:38:37Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR fixes issue #7285 by ensuring that newly created Jupyter notebook projects automatically connect to the runtime once it becomes available. Key changes include:
- Adding an end-to-end test to verify that a new notebook project connects to the runtime on startup.
- Updating the new project service to listen for runtime initialization and automatically connect the notebook.
- Introducing helper functions for notebook context validation, runtime connection, kernel registration polling, and kernel selection.

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

| File | Description |
| ---- | ----------- |
| test/e2e/tests/new-project-wizard/new-project-python.test.ts | Adds a test for verifying the automatic connection of a new Python notebook project to the runtime. |
| src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts | Updates the service to support automatic connection, introduces helpers for notebook connection and kernel selection, and refines runtime session handling. |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T12:50:12Z
user: login: sharon-wang
body: Working well for me on Mac Desktop!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T17:21:17Z
user: login: sharon-wang
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T17:22:50Z

Review comments:
diff_hunk: @@ -533,6 +541,175 @@ export class PositronNewProjectService extends Disposable implements IPositronNe
 
 	//#endregion Extension Tasks
 
+	/**
+	 * Finds the most suitable kernel for a given notebook based on runtime information.
+	 *
+	 * @param matchingKernels The result object containing potential kernel matches for the notebook.
+	 * @param runtimeId The ID of the runtime associated with the new project.
+	 * @param languageId The language ID associated with the runtime, if available.
+	 * @returns The kernel to select, or undefined if no suitable kernel is found.
+	 */
+	private _findKernelForNotebook(
+		matchingKernels: INotebookKernelMatchResult,
+		runtimeId: string,
+		languageId: string | undefined
+	): INotebookKernel | undefined {
+		// Prefer kernels that directly reference the runtime ID or are Positron-specific for the language
+		for (const kernel of matchingKernels.all) {
+			// Kernels may have different ways of identifying runtimes, so we check a few common patterns
+			if (kernel.id.includes(runtimeId) ||
path: src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts
user: login: Copilot
body: [nitpick] Using a simple substring check on kernel.id to match the runtimeId might be too naive and could lead to unintended matches. Consider implementing a more robust matching mechanism to avoid potential conflicts.
```suggestion
			if ((new RegExp(`\\b${runtimeId}\\b`).test(kernel.id)) ||
```
created_at: 2025-05-14T17:06:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -533,6 +544,179 @@ export class PositronNewProjectService extends Disposable implements IPositronNe
 
 	//#endregion Extension Tasks
 
+
+	/**
+	 * Centralised validation helper for notebook connection.
+	 * Prefers the active notebook editor if present, otherwise falls back to the only open notebook editor.
+	 * If no notebook editors are open, or no active notebook editor is present when multiple are open, logs and notifies the user.
+	 *
+	 * @returns The validated context required to establish a runtime session, or `undefined` if validation failed.
+	 */
+	private _getNotebookContext(): { model: INotebookTextModel; runtimeId: string } | undefined {
+		const notebookEditors = this._notebookEditorService.listNotebookEditors();
+
+		// Prefer the active notebook editor if available
+		const activeEditor = (this._notebookEditorService as any).activeNotebookEditor as { textModel?: INotebookTextModel } | undefined;
path: src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts
user: login: Copilot
body: [nitpick] Avoid using 'any' when accessing activeNotebookEditor. Consider updating its type definition to improve type safety and maintainability.
created_at: 2025-05-16T12:50:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,6 +72,38 @@ test.describe('Python - New Project Wizard', { tag: [tags.MODAL, tags.NEW_PROJEC
 		await verifyProjectCreation(app, projectTitle);
 		await verifyVenEnvStarts(app);
 	});
+
+	test('Python Notebook Project - connects notebook to runtime on startup', async function ({ app }) {
+		const projectTitle = `python-notebook-runtime-${Math.floor(Math.random() * 1000000)}`;
+
+		// Create a new Python notebook project
+		await app.workbench.newProjectWizard.createNewProject({
+			type: ProjectType.JUPYTER_NOTEBOOK,
+			title: projectTitle,
+			status: 'new',
+			pythonEnv: 'venv', // or 'conda' if that's your default
+		});
path: test/e2e/tests/new-project-wizard/new-project-python.test.ts
user: login: sharon-wang
body: Suggestion for consistency with the other tests:

```suggestion
		const projectTitle = addRandomNumSuffix('python-notebook-runtime');

		await createNewProject(app, {
			type: ProjectType.JUPYTER_NOTEBOOK,
			title: projectTitle,
			status: 'new',
			pythonEnv: 'venv',
		});

		await verifyProjectCreation(app, projectTitle);
		await verifyVenEnvStarts(app);
```
created_at: 2025-05-16T16:54:55Z
author_association: MEMBER
diff_hunk: @@ -72,6 +72,38 @@ test.describe('Python - New Project Wizard', { tag: [tags.MODAL, tags.NEW_PROJEC
 		await verifyProjectCreation(app, projectTitle);
 		await verifyVenEnvStarts(app);
 	});
+
+	test('Python Notebook Project - connects notebook to runtime on startup', async function ({ app }) {
+		const projectTitle = `python-notebook-runtime-${Math.floor(Math.random() * 1000000)}`;
+
+		// Create a new Python notebook project
+		await app.workbench.newProjectWizard.createNewProject({
+			type: ProjectType.JUPYTER_NOTEBOOK,
+			title: projectTitle,
+			status: 'new',
+			pythonEnv: 'venv', // or 'conda' if that's your default
+		});
+
+		// Wait for the notebook editor to be visible
+		const notebookEditorTab = app.code.driver.page.locator('[id="workbench.parts.editor"]').getByText('Untitled-1.ipynb', { exact: true });
+		await expect(notebookEditorTab).toBeVisible();
+
+		// Get the Python version from the session selector button
+		const sessionSelectorButton = app.code.driver.page.getByRole('button', { name: 'Session Selector' });
+		const sessionSelectorText = await sessionSelectorButton.textContent();
+		// Extract the version number (e.g., '3.10.12') from the button text
+		const versionMatch = sessionSelectorText && sessionSelectorText.match(/Python ([0-9]+\.[0-9]+\.[0-9]+)/);
+		const pythonVersion = versionMatch ? versionMatch[1] : undefined;
+		// Fail the test if we can't extract the version
+		expect(pythonVersion, 'Python version should be present in session selector').toBeTruthy();
+
+		// After the runtime starts up the kernel status should be replaced with the kernel name.
+		// The kernel name should contain the Python version from the session selector
+		// Only look within an 'a' tag with class 'kernel-label' to avoid false positives
+		const kernelLabel = app.code.driver.page.locator('a.kernel-label');
+		await expect(kernelLabel).toContainText(`Python ${pythonVersion}`);
+		await expect(kernelLabel).toContainText('.venv');
+	});
path: test/e2e/tests/new-project-wizard/new-project-python.test.ts
user: login: sharon-wang
body: Suggestion to create a separate function, maybe something like `verifyNotebookRuntimeConnects` to match the other tests?
created_at: 2025-05-16T16:56:05Z
author_association: MEMBER
diff_hunk: @@ -72,6 +72,38 @@ test.describe('Python - New Project Wizard', { tag: [tags.MODAL, tags.NEW_PROJEC
 		await verifyProjectCreation(app, projectTitle);
 		await verifyVenEnvStarts(app);
 	});
+
+	test('Python Notebook Project - connects notebook to runtime on startup', async function ({ app }) {
path: test/e2e/tests/new-project-wizard/new-project-python.test.ts
user: login: sharon-wang
body: Should we remove the Jupyter Notebook test from `new-project-r-jupyter.test.ts` in favour of this one, since our Jupyter Notebook support in the wizard is Python only?
created_at: 2025-05-16T16:58:10Z
author_association: MEMBER
diff_hunk: @@ -13,15 +13,18 @@ import { IWorkspaceTrustManagementService } from '../../../../platform/workspace
 import { CreateEnvironmentResult, IPositronNewProjectService, NewProjectConfiguration, NewProjectStartupPhase, NewProjectTask, NewProjectType, POSITRON_NEW_PROJECT_CONFIG_STORAGE_KEY } from './positronNewProject.js';
 import { Event } from '../../../../base/common/event.js';
 import { Barrier } from '../../../../base/common/async.js';
-import { ILanguageRuntimeMetadata } from '../../languageRuntime/common/languageRuntimeService.js';
+import { ILanguageRuntimeMetadata, LanguageRuntimeSessionMode } from '../../languageRuntime/common/languageRuntimeService.js';
 import { IFileService } from '../../../../platform/files/common/files.js';
 import { VSBuffer } from '../../../../base/common/buffer.js';
 import { joinPath, relativePath } from '../../../../base/common/resources.js';
 import { DOT_IGNORE_JUPYTER, DOT_IGNORE_PYTHON, DOT_IGNORE_R } from './positronNewProjectTemplates.js';
 import { URI } from '../../../../base/common/uri.js';
 import { INotificationService } from '../../../../platform/notification/common/notification.js';
 import { localize } from '../../../../nls.js';
-import { IRuntimeSessionService } from '../../runtimeSession/common/runtimeSessionService.js';
+import { IRuntimeSessionService, RuntimeStartMode } from '../../runtimeSession/common/runtimeSessionService.js';
+import { INotebookEditorService } from '../../../contrib/notebook/browser/services/notebookEditorService.js';
+import { INotebookKernel, INotebookKernelService } from '../../../contrib/notebook/common/notebookKernelService.js';
+import { INotebookTextModel } from '../../../contrib/notebook/common/notebookCommon.js';
path: src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts
user: login: sharon-wang
body: I'm seeing a lint error on these imports. I'm not sure how best to address this though, as all the notebook services are under `workbench/contrib` instead of `editor/contrib`.

```
Bad layering. You are not allowed to access browser from here, allowed layers are: [common]eslint[local/code-layering](https://github.com/microsoft/vscode/wiki/Source-Code-Organization)
Imports violates 'vs/base/common/** or vs/base/parts/*/common/** or vs/platform/*/common/** or vs/editor/common/** or vs/editor/contrib/*/common/** or vs/workbench/common/** or vs/workbench/services/*/common/** or tas-client-umd or vscode-textmate or @vscode/vscode-languagedetection or @vscode/tree-sitter-wasm or vs/nls.js or vs/amdX.js' restrictions. See https://github.com/microsoft/vscode/wiki/Source-Code-Organizationeslint[local/code-import-patterns](https://github.com/microsoft/vscode/wiki/Source-Code-Organization)
```

Maybe the notebook services should be in `vs/workbench/services/*/common/**`?
created_at: 2025-05-16T17:10:07Z
author_association: MEMBER
diff_hunk: @@ -533,6 +544,206 @@ export class PositronNewProjectService extends Disposable implements IPositronNe
 
 	//#endregion Extension Tasks
 
+
+	/**
+	 * Centralised validation helper for notebook connection.
+	 * Prefers the active notebook editor if present, otherwise falls back to the only open notebook editor.
+	 * If no notebook editors are open, or no active notebook editor is present when multiple are open, logs and notifies the user.
+	 *
+	 * @returns The validated context required to establish a runtime session, or `undefined` if validation failed.
+	 */
+	private _getNotebookContext(): { model: INotebookTextModel; runtimeId: string } | undefined {
+		const notebookEditors = this._notebookEditorService.listNotebookEditors();
+
+		// Prefer the active notebook editor if available
+		const activeEditor = (this._notebookEditorService as any).activeNotebookEditor as { textModel?: INotebookTextModel } | undefined;
path: src/vs/workbench/services/positronNewProject/common/positronNewProjectService.ts
user: login: sharon-wang
body: Should the `INotebookEditorService` interface have `activeNotebookEditor` as a property, so that we don't need to do these type assertions?
created_at: 2025-05-16T17:17:33Z
author_association: MEMBER
diff_hunk: @@ -72,6 +72,38 @@ test.describe('Python - New Project Wizard', { tag: [tags.MODAL, tags.NEW_PROJEC
 		await verifyProjectCreation(app, projectTitle);
 		await verifyVenEnvStarts(app);
 	});
+
+	test('Python Notebook Project - connects notebook to runtime on startup', async function ({ app }) {
+		const projectTitle = `python-notebook-runtime-${Math.floor(Math.random() * 1000000)}`;
+
+		// Create a new Python notebook project
+		await app.workbench.newProjectWizard.createNewProject({
+			type: ProjectType.JUPYTER_NOTEBOOK,
+			title: projectTitle,
+			status: 'new',
+			pythonEnv: 'venv', // or 'conda' if that's your default
+		});
path: test/e2e/tests/new-project-wizard/new-project-python.test.ts
user: login: sharon-wang
body: I noticed that `setProjectConfiguration` doesn't handle `ProjectType.JUPYTER_NOTEBOOK`, so I don't think the `status` and `pythonEnv` is currently being used in this test

https://github.com/posit-dev/positron/blob/3627b1bf96039c610d7a9bc25a995ac6c407dff1/test/e2e/pages/newProjectWizard.ts#L70-L101
created_at: 2025-05-16T17:22:50Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-14T17:06:26Z
body: <!-- PR Tags -->
<!-- 
🚨 RED ALERT! ✋ Rule breaker detected! Tags don’t go here, they belong above ^ in the PR description using this proper format: `@:tag`. Changing them here won't do anything (trust us, we’ve tried). Confused? Check out the README hyperlink.
-->

**E2E Tests** 🚀
This PR will run tests tagged with: `@:critical` `@:new-project-wizard`

<sup>[readme](https://github.com/posit-dev/positron/blob/main/test/e2e/README.md#pull-requests-and-test-tags)</sup>&nbsp;&nbsp;<sup>[valid tags](https://github.com/posit-dev/positron/blob/main/test/e2e/infra/test-runner/test-tags.ts)</sup>
author_association: NONE
user: login: nstrayer
created_at: 2025-05-15T19:23:55Z
body: Oof, good catch! That's what I get for not adding an e2e test.
author_association: CONTRIBUTOR
user: login: nstrayer
created_at: 2025-05-16T01:11:33Z
body: Added a playwright tests. On my local machine the test fails but when I manually do the test actions it works. Pushing here to see if I just havent setup my playwright environment properly on my local machine.

author_association: CONTRIBUTOR
user: login: nstrayer
created_at: 2025-05-16T12:50:25Z
body: I fixed the selection bug on my manual runs but can't seem to get my local e2e tests running properly. I am hoping it's a setup issue on my end and CI will be able to handle it.

author_association: CONTRIBUTOR
user: login: sharon-wang
created_at: 2025-06-10T16:27:28Z
body: resolving the test comments above as they are being addressed in https://github.com/posit-dev/positron/pull/8044!
author_association: MEMBER