From 85e33beb9d5cfec20b7cecd541bf5e2f5b431e0e Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Mon, 13 Jul 2026 09:26:50 -0400 Subject: [PATCH 1/5] Add failing regression tests for #14762: getContext misses open-but-inactive notebooks --- .../mainThreadNotebookFeatures.vitest.ts | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts b/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts index e030672c77ca..04c4cc0bf385 100644 --- a/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts +++ b/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts @@ -9,14 +9,20 @@ import { stubInterface } from '../../../../../test/vitest/stubInterface.js'; import { createTestContainer } from '../../../../../test/vitest/positronTestContainer.js'; import { encodeBase64, VSBuffer } from '../../../../../base/common/buffer.js'; import { constObservable } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { isEqual } from '../../../../../base/common/resources.js'; import { IExtHostContext } from '../../../../services/extensions/common/extHostCustomers.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IRuntimeSessionService } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IVisibleEditorPane } from '../../../../common/editor.js'; +import { EditorInput } from '../../../../common/editor/editorInput.js'; import { IPositronNotebookService } from '../../../../contrib/positronNotebook/browser/positronNotebookService.js'; import { IPositronNotebookInstance } from '../../../../contrib/positronNotebook/browser/IPositronNotebookInstance.js'; import { IPositronNotebookCodeCell, NotebookCellOutputs } from '../../../../contrib/positronNotebook/browser/PositronNotebookCells/IPositronNotebookCell.js'; +import { SelectionState, SelectionStateMachine } from '../../../../contrib/positronNotebook/browser/selectionMachine.js'; +import { POSITRON_NOTEBOOK_EDITOR_INPUT_ID } from '../../../../contrib/positronNotebook/common/positronNotebookCommon.js'; import { MainThreadNotebookFeatures } from '../../../browser/positron/mainThreadNotebookFeatures.js'; const { mockRasterizeSvgToPng } = vi.hoisted(() => ({ mockRasterizeSvgToPng: vi.fn() })); @@ -110,3 +116,111 @@ describe('MainThreadNotebookFeatures $getCellOutputs SVG handling', () => { features.dispose(); }); }); + +describe('MainThreadNotebookFeatures $getActiveNotebookContext', () => { + createTestContainer().build(); + + const TEXT_FILE_EDITOR_PANE_ID = 'workbench.editors.files.textFileEditor'; + const TEXT_FILE_EDITOR_INPUT_ID = 'workbench.editors.files.fileEditorInput'; + + /** A notebook instance stub with no cells, no kernel, and no selection. */ + function createNotebookInstance(uriString: string): IPositronNotebookInstance { + return stubInterface({ + uri: URI.parse(uriString), + cells: constObservable([]), + kernel: constObservable(undefined), + selectionStateMachine: stubInterface({ + state: constObservable({ type: SelectionState.NoCells }), + }), + }); + } + + /** An editor input stub as it appears in the editor service's MRU list. */ + function createEditorInput(typeId: string, uriString: string): EditorInput { + return stubInterface({ + typeId, + resource: URI.parse(uriString), + }); + } + + /** + * Builds a MainThreadNotebookFeatures against a stubbed editor state: the + * given active pane, editors in most-recently-active order, and the open + * Positron notebook instances. + */ + function createContextFeatures(options: { + activeEditorPane: IVisibleEditorPane | undefined; + mruEditors: EditorInput[]; + instances: IPositronNotebookInstance[]; + }): MainThreadNotebookFeatures { + const editorService = stubInterface({ + activeEditorPane: options.activeEditorPane, + getEditors: () => options.mruEditors.map((editor, groupId) => ({ groupId, editor })), + }); + const notebookService = stubInterface({ + listInstances: (uri?: URI) => options.instances.filter(instance => !uri || isEqual(instance.uri, uri)), + }); + return new MainThreadNotebookFeatures( + stubInterface(), + editorService, + notebookService, + stubInterface(), + stubInterface(), + stubInterface({ getNotebookSessionForNotebookUri: () => undefined }), + ); + } + + it('resolves an open Positron notebook when it is not the active editor pane (#14762)', async () => { + const notebookUri = 'file:///test/notebook.ipynb'; + const notebook = createNotebookInstance(notebookUri); + // Focus is elsewhere: the user is typing in another editor (chat + // editor, split group, another file tab) while the notebook stays + // open in its own tab. + const features = createContextFeatures({ + activeEditorPane: stubInterface({ getId: () => TEXT_FILE_EDITOR_PANE_ID }), + mruEditors: [ + createEditorInput(TEXT_FILE_EDITOR_INPUT_ID, 'file:///test/script.py'), + createEditorInput(POSITRON_NOTEBOOK_EDITOR_INPUT_ID, notebookUri), + ], + instances: [notebook], + }); + + const context = await features.$getActiveNotebookContext(); + + expect(context?.uri).toBe(notebookUri); + features.dispose(); + }); + + it('resolves the most recently active notebook when multiple are open and none is active', async () => { + const olderUri = 'file:///test/older.ipynb'; + const recentUri = 'file:///test/recent.ipynb'; + // Registration order (older first) deliberately differs from MRU + // order (recent first): the fallback must follow the editor + // service's most-recently-active order, not instance registration. + const features = createContextFeatures({ + activeEditorPane: stubInterface({ getId: () => TEXT_FILE_EDITOR_PANE_ID }), + mruEditors: [ + createEditorInput(TEXT_FILE_EDITOR_INPUT_ID, 'file:///test/script.py'), + createEditorInput(POSITRON_NOTEBOOK_EDITOR_INPUT_ID, recentUri), + createEditorInput(POSITRON_NOTEBOOK_EDITOR_INPUT_ID, olderUri), + ], + instances: [createNotebookInstance(olderUri), createNotebookInstance(recentUri)], + }); + + const context = await features.$getActiveNotebookContext(); + + expect(context?.uri).toBe(recentUri); + features.dispose(); + }); + + it('resolves undefined when no Positron notebook is open anywhere', async () => { + const features = createContextFeatures({ + activeEditorPane: stubInterface({ getId: () => TEXT_FILE_EDITOR_PANE_ID }), + mruEditors: [createEditorInput(TEXT_FILE_EDITOR_INPUT_ID, 'file:///test/script.py')], + instances: [], + }); + + expect(await features.$getActiveNotebookContext()).toBeUndefined(); + features.dispose(); + }); +}); From d91ad69407f2746a867e221896e6c97ebfb81fa0 Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Mon, 13 Jul 2026 10:25:59 -0400 Subject: [PATCH 2/5] Resolve open-but-inactive Positron notebooks in getContext (#14762) positron.notebooks.getContext resolved the notebook only from the active editor pane, so assistant tools reported 'no notebook is open in the editor' whenever focus was in another editor (chat editor, split group, another tab) even though a Positron notebook was open. Fall back to the most recently active open Positron notebook, resolved through the editor service's MRU list against registered notebook instances. --- src/positron-dts/positron.d.ts | 15 +++++--- .../positron/mainThreadNotebookFeatures.ts | 38 +++++++++++++++++-- .../mainThreadNotebookFeatures.vitest.ts | 17 +++++++++ 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index 6f82b24aafe3..4be7140d885d 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -3785,13 +3785,18 @@ declare module 'positron' { /** * Get context about the active notebook. * + * When no Positron notebook is the active editor pane (e.g. focus is + * in another editor or view), falls back to the most recently active + * Positron notebook that is still open. + * * Resolves with `undefined` when no notebook is open. Rejects with an - * actionable error when a notebook is open but in an editor other than - * the Positron Notebook Editor (e.g. the built-in/Jupyter notebook - * editor), since the notebook API only operates on Positron Notebook - * Editor instances; the error message explains how to switch editors. + * actionable error when the active editor holds a notebook in an + * editor other than the Positron Notebook Editor (e.g. the + * built-in/Jupyter notebook editor), since the notebook API only + * operates on Positron Notebook Editor instances; the error message + * explains how to switch editors. * - * @returns The notebook context, or undefined if no notebook is active + * @returns The notebook context, or undefined if no notebook is open */ export function getContext(): Thenable; diff --git a/src/vs/workbench/api/browser/positron/mainThreadNotebookFeatures.ts b/src/vs/workbench/api/browser/positron/mainThreadNotebookFeatures.ts index b8bec4e7f8ae..346eac443a9a 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadNotebookFeatures.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadNotebookFeatures.ts @@ -23,6 +23,8 @@ import { rasterizeSvgToPng } from '../../../contrib/positronNotebook/browser/svg import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { POSITRON_NOTEBOOK_ASSISTANT_AUTO_FOLLOW_KEY } from '../../../contrib/positronNotebook/common/positronNotebookConfig.js'; import { IRuntimeSessionService } from '../../../services/runtimeSession/common/runtimeSessionService.js'; +import { EditorsOrder } from '../../../common/editor.js'; +import { POSITRON_NOTEBOOK_EDITOR_INPUT_ID } from '../../../contrib/positronNotebook/common/positronNotebookCommon.js'; /** * Main thread implementation of notebook features for extension host communication. @@ -90,12 +92,14 @@ export class MainThreadNotebookFeatures implements MainThreadNotebookFeaturesSha } /** - * Gets the context information for the currently active notebook. - * @returns The notebook context DTO, or undefined if no notebook is active. + * Gets the context information for the currently active notebook. When no + * Positron notebook is the active editor pane, falls back to the most + * recently active Positron notebook that is still open. + * @returns The notebook context DTO, or undefined if no notebook is open. */ async $getActiveNotebookContext(): Promise { // Use existing helper function instead of service method - const instance = getNotebookInstanceFromActiveEditorPane(this._editorService); + let instance = getNotebookInstanceFromActiveEditorPane(this._editorService); if (!instance) { // A notebook may be open, but in the built-in editor rather than the // Positron Notebook Editor that this API operates on. Surface an @@ -106,7 +110,14 @@ export class MainThreadNotebookFeatures implements MainThreadNotebookFeaturesSha if (unsupportedEditorMessage) { throw new Error(unsupportedEditorMessage); } - return undefined; + // A Positron notebook can be open without being the active editor + // pane (focus in a chat editor, a split group, or another tab). + // Fall back to the most recently active open notebook so assistant + // tools can still find it (#14762). + instance = this._getMostRecentlyActiveNotebookInstance(); + if (!instance) { + return undefined; + } } // Get current state from observables @@ -703,6 +714,25 @@ export class MainThreadNotebookFeatures implements MainThreadNotebookFeaturesSha return instance.textModel; } + /** + * Finds the open Positron notebook instance whose editor was most + * recently active, regardless of which editor pane currently has focus. + * @returns The notebook instance, or undefined if no Positron notebook + * editor is open. + */ + private _getMostRecentlyActiveNotebookInstance(): IPositronNotebookInstance | undefined { + for (const { editor } of this._editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + if (editor.typeId !== POSITRON_NOTEBOOK_EDITOR_INPUT_ID || !editor.resource) { + continue; + } + const instances = this._positronNotebookService.listInstances(editor.resource); + if (instances.length > 0) { + return instances[0]; + } + } + return undefined; + } + /** * Helper method to get a notebook instance by URI string. * @param uriString The notebook URI as a string. diff --git a/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts b/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts index 04c4cc0bf385..1859935fcb7d 100644 --- a/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts +++ b/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts @@ -22,7 +22,9 @@ import { IPositronNotebookService } from '../../../../contrib/positronNotebook/b import { IPositronNotebookInstance } from '../../../../contrib/positronNotebook/browser/IPositronNotebookInstance.js'; import { IPositronNotebookCodeCell, NotebookCellOutputs } from '../../../../contrib/positronNotebook/browser/PositronNotebookCells/IPositronNotebookCell.js'; import { SelectionState, SelectionStateMachine } from '../../../../contrib/positronNotebook/browser/selectionMachine.js'; +import { UNSUPPORTED_NOTEBOOK_EDITOR_MESSAGE } from '../../../../contrib/positronNotebook/browser/notebookUtils.js'; import { POSITRON_NOTEBOOK_EDITOR_INPUT_ID } from '../../../../contrib/positronNotebook/common/positronNotebookCommon.js'; +import { NOTEBOOK_EDITOR_ID } from '../../../../contrib/notebook/common/notebookCommon.js'; import { MainThreadNotebookFeatures } from '../../../browser/positron/mainThreadNotebookFeatures.js'; const { mockRasterizeSvgToPng } = vi.hoisted(() => ({ mockRasterizeSvgToPng: vi.fn() })); @@ -223,4 +225,19 @@ describe('MainThreadNotebookFeatures $getActiveNotebookContext', () => { expect(await features.$getActiveNotebookContext()).toBeUndefined(); features.dispose(); }); + + it('surfaces the unsupported-editor error when the built-in notebook editor is active, even if a Positron notebook is open elsewhere', async () => { + // The user is looking at a notebook in the built-in editor; falling + // back to a different open Positron notebook would make assistant + // tools operate on the wrong notebook. + const notebookUri = 'file:///test/notebook.ipynb'; + const features = createContextFeatures({ + activeEditorPane: stubInterface({ getId: () => NOTEBOOK_EDITOR_ID }), + mruEditors: [createEditorInput(POSITRON_NOTEBOOK_EDITOR_INPUT_ID, notebookUri)], + instances: [createNotebookInstance(notebookUri)], + }); + + await expect(features.$getActiveNotebookContext()).rejects.toThrow(UNSUPPORTED_NOTEBOOK_EDITOR_MESSAGE); + features.dispose(); + }); }); From 7799e7a0dd7894a464914adfaed2835c49bc4ab5 Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Mon, 13 Jul 2026 10:34:49 -0400 Subject: [PATCH 3/5] Pin getContext fallback to MRU editor order in tests (Gate 2 review) --- .../browser/positron/mainThreadNotebookFeatures.vitest.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts b/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts index 1859935fcb7d..0a627d5fefdd 100644 --- a/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts +++ b/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts @@ -16,7 +16,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { IRuntimeSessionService } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IVisibleEditorPane } from '../../../../common/editor.js'; +import { EditorsOrder, IVisibleEditorPane } from '../../../../common/editor.js'; import { EditorInput } from '../../../../common/editor/editorInput.js'; import { IPositronNotebookService } from '../../../../contrib/positronNotebook/browser/positronNotebookService.js'; import { IPositronNotebookInstance } from '../../../../contrib/positronNotebook/browser/IPositronNotebookInstance.js'; @@ -157,7 +157,11 @@ describe('MainThreadNotebookFeatures $getActiveNotebookContext', () => { }): MainThreadNotebookFeatures { const editorService = stubInterface({ activeEditorPane: options.activeEditorPane, - getEditors: () => options.mruEditors.map((editor, groupId) => ({ groupId, editor })), + // Order-sensitive: only the most-recently-active query sees the + // editors, so a regression to another EditorsOrder fails here. + getEditors: (order: EditorsOrder) => order === EditorsOrder.MOST_RECENTLY_ACTIVE + ? options.mruEditors.map((editor, groupId) => ({ groupId, editor })) + : [], }); const notebookService = stubInterface({ listInstances: (uri?: URI) => options.instances.filter(instance => !uri || isEqual(instance.uri, uri)), From e8f7eeaf027881e4e1aecf025f57e26018cd3fde Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Mon, 13 Jul 2026 11:59:13 -0400 Subject: [PATCH 4/5] Prefer the foreground session's notebook in getContext fallback The confirmed repro for #14762 (Data Explorer or plot editor takes the active pane while the notebook session stays foreground) shows assistant notebook mode is keyed on the foreground session. Resolve that notebook first so getContext agrees with the mode and the interpreter picker; keep the MRU editor fallback for cases where the foreground session was demoted (e.g. focus moved to a code file mid-turn). --- src/positron-dts/positron.d.ts | 5 +- .../positron/mainThreadNotebookFeatures.ts | 28 ++++++--- .../mainThreadNotebookFeatures.vitest.ts | 58 ++++++++++++++++++- 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index 4be7140d885d..5a260aae1cc6 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -3786,8 +3786,9 @@ declare module 'positron' { * Get context about the active notebook. * * When no Positron notebook is the active editor pane (e.g. focus is - * in another editor or view), falls back to the most recently active - * Positron notebook that is still open. + * in another editor or view), falls back to the open notebook attached + * to the foreground session, then to the most recently active Positron + * notebook that is still open. * * Resolves with `undefined` when no notebook is open. Rejects with an * actionable error when the active editor holds a notebook in an diff --git a/src/vs/workbench/api/browser/positron/mainThreadNotebookFeatures.ts b/src/vs/workbench/api/browser/positron/mainThreadNotebookFeatures.ts index 346eac443a9a..3c24fa477cd5 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadNotebookFeatures.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadNotebookFeatures.ts @@ -93,8 +93,9 @@ export class MainThreadNotebookFeatures implements MainThreadNotebookFeaturesSha /** * Gets the context information for the currently active notebook. When no - * Positron notebook is the active editor pane, falls back to the most - * recently active Positron notebook that is still open. + * Positron notebook is the active editor pane, falls back to the open + * notebook attached to the foreground session, then to the most recently + * active Positron notebook that is still open. * @returns The notebook context DTO, or undefined if no notebook is open. */ async $getActiveNotebookContext(): Promise { @@ -111,10 +112,10 @@ export class MainThreadNotebookFeatures implements MainThreadNotebookFeaturesSha throw new Error(unsupportedEditorMessage); } // A Positron notebook can be open without being the active editor - // pane (focus in a chat editor, a split group, or another tab). - // Fall back to the most recently active open notebook so assistant - // tools can still find it (#14762). - instance = this._getMostRecentlyActiveNotebookInstance(); + // pane (a Data Explorer or plot editor took the pane, focus moved + // to another tab mid-turn). Fall back to an open notebook so + // assistant tools can still find it (#14762). + instance = this._getFallbackNotebookInstance(); if (!instance) { return undefined; } @@ -715,12 +716,21 @@ export class MainThreadNotebookFeatures implements MainThreadNotebookFeaturesSha } /** - * Finds the open Positron notebook instance whose editor was most - * recently active, regardless of which editor pane currently has focus. + * Finds an open Positron notebook instance when none is the active editor + * pane. Prefers the notebook attached to the foreground session (what the + * interpreter picker shows, and what assistant notebook mode is keyed on), + * then falls back to the notebook whose editor was most recently active. * @returns The notebook instance, or undefined if no Positron notebook * editor is open. */ - private _getMostRecentlyActiveNotebookInstance(): IPositronNotebookInstance | undefined { + private _getFallbackNotebookInstance(): IPositronNotebookInstance | undefined { + const foregroundNotebookUri = this._runtimeSessionService.foregroundSession?.metadata.notebookUri; + if (foregroundNotebookUri) { + const instances = this._positronNotebookService.listInstances(foregroundNotebookUri); + if (instances.length > 0) { + return instances[0]; + } + } for (const { editor } of this._editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { if (editor.typeId !== POSITRON_NOTEBOOK_EDITOR_INPUT_ID || !editor.resource) { continue; diff --git a/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts b/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts index 0a627d5fefdd..c7fac2c7fb2e 100644 --- a/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts +++ b/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts @@ -13,7 +13,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { IExtHostContext } from '../../../../services/extensions/common/extHostCustomers.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { IRuntimeSessionService } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; +import { ILanguageRuntimeSession, IRuntimeSessionMetadata, IRuntimeSessionService } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { EditorsOrder, IVisibleEditorPane } from '../../../../common/editor.js'; @@ -150,10 +150,20 @@ describe('MainThreadNotebookFeatures $getActiveNotebookContext', () => { * given active pane, editors in most-recently-active order, and the open * Positron notebook instances. */ + /** A foreground session stub attached to the given notebook. */ + function createForegroundNotebookSession(uriString: string): ILanguageRuntimeSession { + return stubInterface({ + metadata: stubInterface({ + notebookUri: URI.parse(uriString), + }), + }); + } + function createContextFeatures(options: { activeEditorPane: IVisibleEditorPane | undefined; mruEditors: EditorInput[]; instances: IPositronNotebookInstance[]; + foregroundSession?: ILanguageRuntimeSession; }): MainThreadNotebookFeatures { const editorService = stubInterface({ activeEditorPane: options.activeEditorPane, @@ -172,7 +182,10 @@ describe('MainThreadNotebookFeatures $getActiveNotebookContext', () => { notebookService, stubInterface(), stubInterface(), - stubInterface({ getNotebookSessionForNotebookUri: () => undefined }), + stubInterface({ + getNotebookSessionForNotebookUri: () => undefined, + foregroundSession: options.foregroundSession, + }), ); } @@ -219,6 +232,47 @@ describe('MainThreadNotebookFeatures $getActiveNotebookContext', () => { features.dispose(); }); + it('prefers the foreground session\'s notebook over a more recently active notebook editor', async () => { + const attachedUri = 'file:///test/attached.ipynb'; + const recentUri = 'file:///test/recent.ipynb'; + // The user's session (what the interpreter picker shows, and what the + // assistant's notebook mode is keyed on) is attached to one notebook + // while another notebook's editor was touched more recently: the + // attached notebook wins. + const features = createContextFeatures({ + activeEditorPane: stubInterface({ getId: () => TEXT_FILE_EDITOR_PANE_ID }), + mruEditors: [ + createEditorInput(POSITRON_NOTEBOOK_EDITOR_INPUT_ID, recentUri), + createEditorInput(POSITRON_NOTEBOOK_EDITOR_INPUT_ID, attachedUri), + ], + instances: [createNotebookInstance(attachedUri), createNotebookInstance(recentUri)], + foregroundSession: createForegroundNotebookSession(attachedUri), + }); + + const context = await features.$getActiveNotebookContext(); + + expect(context?.uri).toBe(attachedUri); + features.dispose(); + }); + + it('falls back to the most recently active notebook editor when the foreground notebook is closed', async () => { + const closedUri = 'file:///test/closed.ipynb'; + const openUri = 'file:///test/open.ipynb'; + // The foreground session's notebook editor was closed (its session may + // still be running); resolution must not target a closed notebook. + const features = createContextFeatures({ + activeEditorPane: stubInterface({ getId: () => TEXT_FILE_EDITOR_PANE_ID }), + mruEditors: [createEditorInput(POSITRON_NOTEBOOK_EDITOR_INPUT_ID, openUri)], + instances: [createNotebookInstance(openUri)], + foregroundSession: createForegroundNotebookSession(closedUri), + }); + + const context = await features.$getActiveNotebookContext(); + + expect(context?.uri).toBe(openUri); + features.dispose(); + }); + it('resolves undefined when no Positron notebook is open anywhere', async () => { const features = createContextFeatures({ activeEditorPane: stubInterface({ getId: () => TEXT_FILE_EDITOR_PANE_ID }), From 14f47ff09b5b169ddb605c57c6faa3cd00b80c2f Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Mon, 13 Jul 2026 12:04:10 -0400 Subject: [PATCH 5/5] Restore createContextFeatures doc placement (delta review) --- .../positron/mainThreadNotebookFeatures.vitest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts b/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts index c7fac2c7fb2e..1458d4490c31 100644 --- a/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts +++ b/src/vs/workbench/api/test/browser/positron/mainThreadNotebookFeatures.vitest.ts @@ -145,11 +145,6 @@ describe('MainThreadNotebookFeatures $getActiveNotebookContext', () => { }); } - /** - * Builds a MainThreadNotebookFeatures against a stubbed editor state: the - * given active pane, editors in most-recently-active order, and the open - * Positron notebook instances. - */ /** A foreground session stub attached to the given notebook. */ function createForegroundNotebookSession(uriString: string): ILanguageRuntimeSession { return stubInterface({ @@ -159,6 +154,11 @@ describe('MainThreadNotebookFeatures $getActiveNotebookContext', () => { }); } + /** + * Builds a MainThreadNotebookFeatures against a stubbed editor state: the + * given active pane, editors in most-recently-active order, the open + * Positron notebook instances, and optionally the foreground session. + */ function createContextFeatures(options: { activeEditorPane: IVisibleEditorPane | undefined; mruEditors: EditorInput[];