Skip to content

Commit de66a3f

Browse files
authored
Positron Notebooks: Inline Chat (#9147)
Addresses #8733. ### Release Notes #### New Features - N/A #### Bug Fixes - N/A ### QA Notes @:notebooks @:assistant
1 parent 492e399 commit de66a3f

File tree

10 files changed

+179
-56
lines changed

10 files changed

+179
-56
lines changed

src/vs/workbench/api/browser/mainThreadNotebookEditors.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ExtHostContext, ExtHostNotebookEditorsShape, INotebookDocumentShowOptio
1919
// --- Start Positron ---
2020
import { IPositronNotebookService } from '../../services/positronNotebook/browser/positronNotebookService.js';
2121
import { PositronNotebookEditorInput } from '../../contrib/positronNotebook/browser/PositronNotebookEditorInput.js';
22+
import { isEqual } from '../../../base/common/resources.js';
2223
// --- End Positron ---
2324

2425
class MainThreadNotebook {
@@ -107,16 +108,17 @@ export class MainThreadNotebookEditors implements MainThreadNotebookEditorsShape
107108
// --- Start Positron ---
108109
// Check if a Positron notebook is already open for this resource
109110
const uri = URI.revive(resource);
110-
const positronInstance = this._positronNotebookService.getInstance(uri);
111-
if (positronInstance && positronInstance.connectedToEditor) {
112-
// Find the editor pane containing this Positron notebook
113-
for (const editorPane of this._editorService.visibleEditorPanes) {
114-
const input = editorPane.input;
115-
if (input instanceof PositronNotebookEditorInput && input.resource.toString() === uri.toString()) {
116-
// Positron notebook is already open, just return a synthetic ID
117-
// We can't return the actual notebook editor ID because Positron notebooks
118-
// don't implement INotebookEditor interface
119-
return `positron-notebook-${uri.toString()}`;
111+
for (const positronInstance of this._positronNotebookService.listInstances(uri)) {
112+
if (positronInstance.connectedToEditor) {
113+
// Find the editor pane containing this Positron notebook
114+
for (const editorPane of this._editorService.visibleEditorPanes) {
115+
const input = editorPane.input;
116+
if (input instanceof PositronNotebookEditorInput && isEqual(input.resource, uri)) {
117+
// Positron notebook is already open, just return a synthetic ID
118+
// We can't return the actual notebook editor ID because Positron notebooks
119+
// don't implement INotebookEditor interface
120+
return `positron-notebook-${uri.toString()}`;
121+
}
120122
}
121123
}
122124
}

src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import { CellUri } from '../../notebook/common/notebookCommon.js';
1515
import { IEditorService } from '../../../services/editor/common/editorService.js';
1616
import { NotebookTextDiffEditor } from '../../notebook/browser/diff/notebookDiffEditor.js';
1717
import { NotebookMultiTextDiffEditor } from '../../notebook/browser/diff/notebookMultiDiffEditor.js';
18+
// --- Start Positron ---
19+
// Imports to support inline chat in Positron notebooks.
20+
import { IPositronNotebookService } from '../../../services/positronNotebook/browser/positronNotebookService.js';
21+
// --- End Positron ---
1822

1923
export class InlineChatNotebookContribution {
2024

@@ -24,6 +28,10 @@ export class InlineChatNotebookContribution {
2428
@IInlineChatSessionService sessionService: IInlineChatSessionService,
2529
@IEditorService editorService: IEditorService,
2630
@INotebookEditorService notebookEditorService: INotebookEditorService,
31+
// --- Start Positron ---
32+
// Imports to support inline chat in Positron notebooks.
33+
@IPositronNotebookService positronNotebookService: IPositronNotebookService,
34+
// --- End Positron ---
2735
) {
2836

2937
this._store.add(sessionService.registerSessionKeyComputer(Schemas.vscodeNotebookCell, {
@@ -57,6 +65,19 @@ export class InlineChatNotebookContribution {
5765
// }
5866
}
5967
}
68+
// --- Start Positron ---
69+
// To support inline chat in Positron notebooks:
70+
// construct a session comparison key from the corresponding notebook
71+
for (const positronInstance of positronNotebookService.listInstances(data.notebook)) {
72+
const candidate = `<positron-notebook>${positronInstance.id}#${uri}`;
73+
if (!fallback) {
74+
fallback = candidate;
75+
}
76+
if (positronInstance.hasCodeEditor(editor)) {
77+
return candidate;
78+
}
79+
}
80+
// --- End Positron ---
6081

6182
if (fallback) {
6283
return fallback;
@@ -96,6 +117,20 @@ export class InlineChatNotebookContribution {
96117
}
97118
}
98119
}
120+
// --- Start Positron ---
121+
// To support inline chat in Positron notebooks:
122+
// cancel existing chat sessions when a new one is started.
123+
for (const positronInstance of positronNotebookService.listInstances(candidate.notebook)) {
124+
if (positronInstance.hasCodeEditor(newSessionEditor)) {
125+
for (const { editor } of positronInstance.cells.get()) {
126+
if (editor && editor !== newSessionEditor) {
127+
InlineChatController.get(editor)?.acceptSession();
128+
}
129+
}
130+
break;
131+
}
132+
}
133+
// --- End Positron ---
99134
}));
100135
}
101136

src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ILanguageSelection, ILanguageService } from '../../../../editor/common/
1616
import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js';
1717
import * as nls from '../../../../nls.js';
1818
// --- Start Positron ---
19-
// eslint-disable-next-line no-duplicate-imports
19+
/* eslint-disable no-duplicate-imports */
2020
import { ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js';
2121
// --- End Positron ---
2222
import { Extensions, IConfigurationPropertySchema, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
@@ -807,11 +807,9 @@ class NotebookEditorManager implements IWorkbenchContribution {
807807
if (model.isDirty() && !this._editorService.isOpened({ resource: model.resource, typeId: NotebookEditorInput.ID, editorId: model.viewType }) && extname(model.resource) !== '.interactive') {
808808
// --- Start Positron ---
809809
// Make sure that we dont try and open the same editor twice if we're using positron
810-
// notebooks. This is a separate if-statement so we don't have to put the diff
811-
// inside the inline conditional.
812-
const positronNotebookInstance = this._positronNotebookService.getInstance(model.resource);
813-
// Check to see if the instance is connected to the view.
814-
if (positronNotebookInstance && positronNotebookInstance.connectedToEditor) {
810+
// notebooks.
811+
const positronInstances = this._positronNotebookService.listInstances(model.resource);
812+
if (positronInstances.some(instance => instance.connectedToEditor)) {
815813
continue;
816814
}
817815
// --- End Positron ---

src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookCells/PositronNotebookCell.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEdito
1414
import { CellSelectionType } from '../../../../services/positronNotebook/browser/selectionMachine.js';
1515
import { PositronNotebookInstance } from '../PositronNotebookInstance.js';
1616
import { ISettableObservable, observableValue } from '../../../../../base/common/observable.js';
17+
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
1718

1819
export abstract class PositronNotebookCellGeneral extends Disposable implements IPositronNotebookCell {
1920
kind!: CellKind;
@@ -30,6 +31,10 @@ export abstract class PositronNotebookCellGeneral extends Disposable implements
3031
super();
3132
}
3233

34+
get editor(): ICodeEditor | undefined {
35+
return this._editor;
36+
}
37+
3338
get uri(): URI {
3439
return this.cellModel.uri;
3540
}

src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { PixelRatio } from '../../../../base/browser/pixelRatio.js';
1212
import { ISize, PositronReactRenderer } from '../../../../base/browser/positronReactRenderer.js';
1313
import { CancellationToken } from '../../../../base/common/cancellation.js';
1414
import { Emitter } from '../../../../base/common/event.js';
15-
import { DisposableStore } from '../../../../base/common/lifecycle.js';
15+
import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
1616
import { FontMeasurements } from '../../../../editor/browser/config/fontMeasurements.js';
1717
import { IEditorOptions } from '../../../../editor/common/config/editorOptions.js';
1818
import { BareFontInfo, FontInfo } from '../../../../editor/common/config/fontInfo.js';
@@ -49,6 +49,7 @@ import { PositronNotebookEditorInput } from './PositronNotebookEditorInput.js';
4949
import { ILogService } from '../../../../platform/log/common/log.js';
5050
import { NotebookVisibilityProvider } from './NotebookVisibilityContext.js';
5151
import { observableValue } from '../../../../base/common/observable.js';
52+
import { PositronNotebookEditorControl } from './PositronNotebookEditorControl.js';
5253

5354

5455
/*
@@ -94,6 +95,10 @@ export class PositronNotebookEditor extends EditorPane {
9495
*/
9596
private readonly _editorMemento: IEditorMemento<INotebookEditorViewState>;
9697

98+
/**
99+
* The editor control, used by other features to access the code editor widget of the selected cell.
100+
*/
101+
private readonly _control = this._register(new MutableDisposable<PositronNotebookEditorControl>());
97102

98103
private _scopedContextKeyService?: IContextKeyService;
99104
private _scopedInstantiationService?: IInstantiationService;
@@ -271,11 +276,22 @@ export class PositronNotebookEditor extends EditorPane {
271276
);
272277
}
273278

279+
if (input.notebookInstance === undefined) {
280+
throw new Error(
281+
'Notebook instance is undefined. This should have been created in the constructor.'
282+
);
283+
}
274284

275285
// We're setting the options on the input here so that the input can resolve the model
276286
// without having to pass the options to the resolve method.
277287
input.editorOptions = options;
278288

289+
// Update the editor control given the notebook instance.
290+
// This has to be done before we `await super.setInput` since that fires events
291+
// with listeners that call `this.getControl()` expecting an up-to-date control
292+
// i.e. with `activeCodeEditor` being the editor of the selected cell in the notebook.
293+
this._control.value = new PositronNotebookEditorControl(input.notebookInstance);
294+
279295
await super.setInput(input, options, context, token);
280296

281297
const model = await input.resolve(options);
@@ -299,12 +315,6 @@ export class PositronNotebookEditor extends EditorPane {
299315
)
300316
);
301317

302-
if (input.notebookInstance === undefined) {
303-
throw new Error(
304-
'Notebook instance is undefined. This should have been created in the constructor.'
305-
);
306-
}
307-
308318
this._renderReact();
309319

310320
input.notebookInstance.attachView(this._parentDiv);
@@ -326,6 +336,9 @@ export class PositronNotebookEditor extends EditorPane {
326336
// Clear the input observable.
327337
this._input = undefined;
328338

339+
// Clear the editor control.
340+
this._control.clear();
341+
329342
this._disposeReactRenderer();
330343

331344
// Call the base class's method.
@@ -382,6 +395,10 @@ export class PositronNotebookEditor extends EditorPane {
382395

383396
}
384397

398+
override getControl() {
399+
return this._control.value;
400+
}
401+
385402
private _fontInfo: FontInfo | undefined;
386403
private _dimension?: DOM.Dimension;
387404
/**
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
3+
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import { Event } from '../../../../base/common/event.js';
6+
import { Disposable } from '../../../../base/common/lifecycle.js';
7+
import { ICompositeCodeEditor, IEditor } from '../../../../editor/common/editorCommon.js';
8+
import { SelectionState } from '../../../services/positronNotebook/browser/selectionMachine.js';
9+
import { PositronNotebookInstance } from './PositronNotebookInstance.js';
10+
11+
/**
12+
* The PositronNotebookEditorControl is used by features like inline chat, debugging, and outlines
13+
* to access the code editor widget of the selected cell in a Positron notebook.
14+
*
15+
* TODO: Some notebook functionality (possibly debugging and outlines) require that the editor control
16+
* also have a `notebookEditor: INotebookEditor` property. We'll need to investigate what that unlocks,
17+
* whether to implement INotebookEditor, or find a different solution.
18+
*/
19+
export class PositronNotebookEditorControl extends Disposable implements ICompositeCodeEditor {
20+
/**
21+
* Event that fires when the active cell, and therefore the active code editor, changes.
22+
*/
23+
public readonly onDidChangeActiveEditor = Event.None;
24+
25+
/**
26+
* The active cell's code editor.
27+
*/
28+
private _activeCodeEditor: IEditor | undefined;
29+
30+
constructor(
31+
private readonly _notebookInstance: PositronNotebookInstance,
32+
) {
33+
super();
34+
35+
// Update the active code editor when the notebook selection state changes.
36+
this._register(this._notebookInstance.selectionStateMachine.onNewState((state) => {
37+
if (state.type === SelectionState.EditingSelection) {
38+
this._activeCodeEditor = state.selectedCell.editor;
39+
} else if (state.type === SelectionState.NoSelection) {
40+
this._activeCodeEditor = undefined;
41+
} else {
42+
this._activeCodeEditor = state.selected[0]?.editor;
43+
}
44+
}));
45+
}
46+
47+
/**
48+
* Gets the active cell's code editor.
49+
*/
50+
public get activeCodeEditor(): IEditor | undefined {
51+
return this._activeCodeEditor;
52+
}
53+
}

src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import { ILanguageRuntimeSession, IRuntimeSessionService } from '../../../servic
3535
import { isEqual } from '../../../../base/common/resources.js';
3636
import { IPositronWebviewPreloadService } from '../../../services/positronWebviewPreloads/browser/positronWebviewPreloadService.js';
3737
import { ISettableObservable, observableValue } from '../../../../base/common/observable.js';
38+
import { ResourceMap } from '../../../../base/common/map.js';
39+
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
3840

3941
interface IPositronNotebookInstanceRequiredTextModel extends IPositronNotebookInstance {
4042
textModel: NotebookTextModel;
@@ -56,7 +58,7 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot
5658
// ===== Statics =====
5759
// #region Statics
5860
/** Map of all active notebook instances, keyed by notebook URI */
59-
static _instanceMap: Map<string, PositronNotebookInstance> = new Map();
61+
static _instanceMap = new ResourceMap<PositronNotebookInstance>();
6062

6163
/**
6264
* Either makes or retrieves an instance of a Positron Notebook based on the resource. This
@@ -72,8 +74,7 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot
7274
instantiationService: IInstantiationService,
7375
): PositronNotebookInstance {
7476

75-
const pathOfNotebook = input.resource.toString();
76-
const existingInstance = PositronNotebookInstance._instanceMap.get(pathOfNotebook);
77+
const existingInstance = PositronNotebookInstance._instanceMap.get(input.resource);
7778
if (existingInstance) {
7879
// Update input
7980
existingInstance._input = input;
@@ -84,7 +85,7 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot
8485
}
8586

8687
const instance = instantiationService.createInstance(PositronNotebookInstance, input, creationOptions);
87-
PositronNotebookInstance._instanceMap.set(pathOfNotebook, instance);
88+
PositronNotebookInstance._instanceMap.set(input.resource, instance);
8889
return instance;
8990
}
9091

@@ -95,20 +96,17 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot
9596
* @param newUri The new URI of the notebook
9697
*/
9798
static updateInstanceUri(oldUri: URI, newUri: URI): void {
98-
const oldKey = oldUri.toString();
99-
const newKey = newUri.toString();
100-
101-
if (oldKey === newKey) {
99+
if (isEqual(oldUri, newUri)) {
102100
return; // No change needed
103101
}
104102

105-
const instance = PositronNotebookInstance._instanceMap.get(oldKey);
103+
const instance = PositronNotebookInstance._instanceMap.get(oldUri);
106104
if (instance) {
107105
// Remove from old key
108-
PositronNotebookInstance._instanceMap.delete(oldKey);
106+
PositronNotebookInstance._instanceMap.delete(oldUri);
109107

110108
// Add to new key - the instance will be updated when getOrCreate is called with the new URI
111-
PositronNotebookInstance._instanceMap.set(newKey, instance);
109+
PositronNotebookInstance._instanceMap.set(newUri, instance);
112110
}
113111
}
114112

@@ -447,7 +445,7 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot
447445
this._logService.info(this.id, 'dispose');
448446
this._positronNotebookService.unregisterInstance(this);
449447
// Remove from the instance map
450-
PositronNotebookInstance._instanceMap.delete(this.uri.toString());
448+
PositronNotebookInstance._instanceMap.delete(this.uri);
451449

452450
super.dispose();
453451
this.detachView();
@@ -635,6 +633,15 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot
635633
this.selectionStateMachine.selectCell(cell, CellSelectionType.Edit);
636634
}
637635

636+
hasCodeEditor(editor: ICodeEditor): boolean {
637+
for (const cell of this._cells) {
638+
if (cell.editor && cell.editor === editor) {
639+
return true;
640+
}
641+
}
642+
return false;
643+
}
644+
638645
async attachView(container: HTMLElement) {
639646
this.detachView();
640647
this._container = container;

src/vs/workbench/services/positronNotebook/browser/IPositronNotebookCell.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { VSBuffer } from '../../../../base/common/buffer.js';
77
import { Disposable } from '../../../../base/common/lifecycle.js';
88
import { ISettableObservable } from '../../../../base/common/observable.js';
99
import { URI } from '../../../../base/common/uri.js';
10+
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
1011
import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
1112
import { NotebookPreloadOutputResults } from '../../positronWebviewPreloads/browser/positronWebviewPreloadService.js';
1213

@@ -54,6 +55,11 @@ export interface IPositronNotebookCell extends Disposable {
5455
*/
5556
cellModel: PositronNotebookCellTextModel;
5657

58+
/**
59+
* The cell's code editor widget.
60+
*/
61+
editor: ICodeEditor | undefined;
62+
5763
/**
5864
* Get the handle number for cell from cell model
5965
*/

0 commit comments

Comments
 (0)