From d279e74d2b2249f04ef77870acfcebe88e0cec38 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Wed, 23 Oct 2024 15:25:38 -0700 Subject: [PATCH 1/4] Include output from interactive cells in Foyle requests This fixes a bug in the serializaiton of the notebook before sending it to Foyle that caused the output of interactive cells not to be included in the requests. The problem is that we need to call addExecInfo before converting the VSCode NotebookData representation to the proto. That handles copying the output of the interactive terminals into the NotebookData structure. This necessitated some code refactoring. In order to call addExecInfo we need an instance of the kernel. We create a new Converter class to keep track of the kernel and also provide reuse in the logic for converting notebook data to protos for Foyle. Since addExecInfo is async we need to change buildReq to return a promise and refactor some of the logic to be non blocking. * Fix jlewi/foyle#286 --- src/extension/ai/converters.ts | 25 +++++++++++++ src/extension/ai/generate.ts | 10 +++-- src/extension/ai/ghost.ts | 59 +++++++++++++++-------------- src/extension/ai/manager.ts | 12 ++++-- src/extension/ai/stream.ts | 68 +++++++++++++++++----------------- src/extension/extension.ts | 2 +- 6 files changed, 104 insertions(+), 72 deletions(-) diff --git a/src/extension/ai/converters.ts b/src/extension/ai/converters.ts index 39caa5867..349e2e568 100644 --- a/src/extension/ai/converters.ts +++ b/src/extension/ai/converters.ts @@ -9,6 +9,31 @@ import { ServerLifecycleIdentity, getServerConfigurationValue } from '../../util import { Serializer } from '../../types' import * as serializerTypes from '../grpc/serializerTypes' import * as serializer from '../serializer' +import { Kernel } from '../kernel' + +import * as protos from './protos' + +// Converter provides converstion routines from vscode data types to protocol buffer types. +// It is a class because in order to handle the conversion we need to keep track of the kernel +// because we need to add execution information to the cells before serializing. +export class Converter { + kernel: Kernel + constructor(kernel: Kernel) { + this.kernel = kernel + } + + // notebokDataToProto converts a VSCode NotebookData to a RunMe Notebook proto. + // It adds execution information to the cells before converting. + public async notebookDataToProto(notebookData: vscode.NotebookData): Promise { + // We need to add the execution info to the cells so that the AI model can use that information. + const cellDataWithExec = await serializer.SerializerBase.addExecInfo(notebookData, this.kernel) + let notebookDataWithExec = new vscode.NotebookData(cellDataWithExec) + // marshalNotebook returns a protocol buffer using the ts client library from buf we need to + // convert it to es + let notebookProto = serializer.GrpcSerializer.marshalNotebook(notebookDataWithExec) + return protos.notebookTSToES(notebookProto) + } +} // cellToCellData converts a NotebookCell to a NotebookCellData. // NotebookCell is an interface used by the editor. diff --git a/src/extension/ai/generate.ts b/src/extension/ai/generate.ts index a5cb31ebc..f1fa5eab3 100644 --- a/src/extension/ai/generate.ts +++ b/src/extension/ai/generate.ts @@ -6,9 +6,9 @@ import { GenerateCellsResponse, } from '@buf/jlewi_foyle.bufbuild_es/foyle/v1alpha1/agent_pb' -import * as serializer from '../serializer' import getLogger from '../logger' +import { Converter } from './converters' import * as protos from './protos' import * as converters from './converters' const log = getLogger('AIGenerate') @@ -17,9 +17,11 @@ const log = getLogger('AIGenerate') // It generates a single completion export class CompletionGenerator { client: PromiseClient + converter: Converter - constructor(client: PromiseClient) { + constructor(client: PromiseClient, converter: Converter) { this.client = client + this.converter = converter } public generateCompletion = async () => { @@ -43,10 +45,10 @@ export class CompletionGenerator { let cellData = editor?.notebook.getCells().map((cell) => converters.cellToCellData(cell)) let notebookData = new vscode.NotebookData(cellData) - let notebookProto = serializer.GrpcSerializer.marshalNotebook(notebookData) + let notebookProto = await this.converter.notebookDataToProto(notebookData) const req = new GenerateCellsRequest() - req.notebook = protos.notebookTSToES(notebookProto) + req.notebook = notebookProto req.selectedIndex = lastSelectedCell this.client diff --git a/src/extension/ai/ghost.ts b/src/extension/ai/ghost.ts index d5dde8e51..da36b60c8 100644 --- a/src/extension/ai/ghost.ts +++ b/src/extension/ai/ghost.ts @@ -2,7 +2,6 @@ import * as vscode from 'vscode' import * as agent_pb from '@buf/jlewi_foyle.bufbuild_es/foyle/v1alpha1/agent_pb' import getLogger from '../logger' -import * as serializer from '../serializer' import { RUNME_CELL_ID } from '../constants' import * as converters from './converters' @@ -38,15 +37,16 @@ const ghostDecoration = vscode.window.createTextEditorDecorationType({ // the cell contents have changed. export class GhostCellGenerator implements stream.CompletionHandlers { private notebookState: Map - + private converter: converters.Converter // contextID is the ID of the context we are generating completions for. // It is used to detect whether a completion response is stale and should be // discarded because the context has changed. - constructor() { + constructor(converter: converters.Converter) { this.notebookState = new Map() // Generate a random context ID. This should be unnecessary because presumable the event to change // the active cell will be sent before any requests are sent but it doesn't hurt to be safe. + this.converter = converter } // Updated method to check and initialize notebook state @@ -64,7 +64,7 @@ export class GhostCellGenerator implements stream.CompletionHandlers { buildRequest( cellChangeEvent: stream.CellChangeEvent, firstRequest: boolean, - ): agent_pb.StreamGenerateRequest | null { + ): Promise { // TODO(jeremy): Is there a more efficient way to find the cell and notebook? // Can we cache it in the class? Since we keep track of notebooks in NotebookState // Is there a way we can go from the URI of the cell to the URI of the notebook directly @@ -77,7 +77,7 @@ export class GhostCellGenerator implements stream.CompletionHandlers { if (notebook === undefined) { log.error(`notebook for cell ${cellChangeEvent.notebookUri} NOT found`) // TODO(jermey): Should we change the return type to be nullable? - return null + return Promise.resolve(null) } // Get the notebook state; this will initialize it if this is the first time we @@ -108,38 +108,37 @@ export class GhostCellGenerator implements stream.CompletionHandlers { let cellData = notebook.getCells().map((cell) => converters.cellToCellData(cell)) let notebookData = new vscode.NotebookData(cellData) - let notebookProto = serializer.GrpcSerializer.marshalNotebook(notebookData) - let request = new agent_pb.StreamGenerateRequest({ - contextId: SessionManager.getManager().getID(), - request: { - case: 'fullContext', - value: new agent_pb.FullContext({ - notebook: protos.notebookTSToES(notebookProto), - selected: matchedCell.index, - notebookUri: notebook.uri.toString(), - }), - }, + return this.converter.notebookDataToProto(notebookData).then((notebookProto) => { + let request = new agent_pb.StreamGenerateRequest({ + contextId: SessionManager.getManager().getID(), + request: { + case: 'fullContext', + value: new agent_pb.FullContext({ + notebook: notebookProto, + selected: matchedCell.index, + notebookUri: notebook.uri.toString(), + }), + }, + }) + return request }) - - return request } else { let cellData = converters.cellToCellData(matchedCell) let notebookData = new vscode.NotebookData([cellData]) - let notebookProto = serializer.GrpcSerializer.marshalNotebook(notebookData) - let notebook = protos.notebookTSToES(notebookProto) // Generate an update request - let request = new agent_pb.StreamGenerateRequest({ - contextId: SessionManager.getManager().getID(), - request: { - case: 'update', - value: new agent_pb.UpdateContext({ - cell: notebook.cells[0], - }), - }, + return this.converter.notebookDataToProto(notebookData).then((notebookProto) => { + let request = new agent_pb.StreamGenerateRequest({ + contextId: SessionManager.getManager().getID(), + request: { + case: 'update', + value: new agent_pb.UpdateContext({ + cell: notebookProto.cells[0], + }), + }, + }) + return request }) - - return request } } diff --git a/src/extension/ai/manager.ts b/src/extension/ai/manager.ts index a2b16727e..63c8de011 100644 --- a/src/extension/ai/manager.ts +++ b/src/extension/ai/manager.ts @@ -3,8 +3,10 @@ import { createPromiseClient, PromiseClient, Transport } from '@connectrpc/conne import { createConnectTransport } from '@connectrpc/connect-node' import { AIService } from '@buf/jlewi_foyle.connectrpc_es/foyle/v1alpha1/agent_connect' +import { Kernel } from '../kernel' import getLogger from '../logger' +import { Converter } from './converters' import * as ghost from './ghost' import * as stream from './stream' import * as generate from './generate' @@ -16,13 +18,17 @@ export class AIManager { subscriptions: vscode.Disposable[] = [] client: PromiseClient completionGenerator: generate.CompletionGenerator - constructor() { + + converter: Converter + constructor(kernel: Kernel) { this.log = getLogger('AIManager') this.log.info('AI: Initializing AI Manager') const config = vscode.workspace.getConfiguration('runme.experiments') const autoComplete = config.get('aiAutoCell', false) this.client = this.createAIClient() - this.completionGenerator = new generate.CompletionGenerator(this.client) + + this.converter = new Converter(kernel) + this.completionGenerator = new generate.CompletionGenerator(this.client, this.converter) if (autoComplete) { this.registerGhostCellEvents() @@ -44,7 +50,7 @@ export class AIManager { // as well as when cells change. This is used to create ghost cells. registerGhostCellEvents() { this.log.info('AI: Enabling AutoCell Generation') - let cellGenerator = new ghost.GhostCellGenerator() + let cellGenerator = new ghost.GhostCellGenerator(this.converter) // Create a stream creator. The StreamCreator is a class that effectively windows events // and turns each window into an AsyncIterable of streaming requests. diff --git a/src/extension/ai/stream.ts b/src/extension/ai/stream.ts index 863a33274..42184571b 100644 --- a/src/extension/ai/stream.ts +++ b/src/extension/ai/stream.ts @@ -20,7 +20,7 @@ export interface CompletionHandlers { buildRequest: ( cellChangeEvent: CellChangeEvent, firstRequest: boolean, - ) => StreamGenerateRequest | null + ) => Promise // processResponse is a function that processes a StreamGenerateResponse processResponse: (response: StreamGenerateResponse) => void @@ -82,45 +82,45 @@ export class StreamCreator { } log.info('handleEvent: building request') - let req = this.handlers.buildRequest(event, firstRequest) + this.handlers.buildRequest(event, firstRequest).then((req) => { + if (req === null) { + log.info(`Notebook: ${event.notebookUri}; no request generated`) + return + } - if (req === null) { - log.info(`Notebook: ${event.notebookUri}; no request generated`) - return - } + // If the request is a fullContext request we need to start a new stream + if (req.request.case === 'fullContext') { + firstRequest = true + } - // If the request is a fullContext request we need to start a new stream - if (req.request.case === 'fullContext') { - firstRequest = true - } + if (this.lastIterator !== undefined && this.lastIterator !== null && firstRequest === true) { + console.log('Stopping the current stream') + this.lastIterator.close() + this.lastIterator = null + } - if (this.lastIterator !== undefined && this.lastIterator !== null && firstRequest === true) { - console.log('Stopping the current stream') - this.lastIterator.close() - this.lastIterator = null - } + if (this.lastIterator === null) { + // n.b. we need to define newIterator and then refer to newIterator in the closure + let newIterator = new PromiseIterator() + this.lastIterator = newIterator + // start the bidirectional stream + let iterable = { + [Symbol.asyncIterator]: () => { + // n.b. We don't want to refer to this.lastIterator because we need to create a closure + // this.lastIterator is a reference that will get be updated over time. We don't want the iterator though + // to change. + return newIterator + }, + } - if (this.lastIterator === null) { - // n.b. we need to define newIterator and then refer to newIterator in the closure - let newIterator = new PromiseIterator() - this.lastIterator = newIterator - // start the bidirectional stream - let iterable = { - [Symbol.asyncIterator]: () => { - // n.b. We don't want to refer to this.lastIterator because we need to create a closure - // this.lastIterator is a reference that will get be updated over time. We don't want the iterator though - // to change. - return newIterator - }, + const responseIterable = this.client.streamGenerate(iterable) + // Start a coroutine to process responses from the completion service + this.processResponses(responseIterable) } - const responseIterable = this.client.streamGenerate(iterable) - // Start a coroutine to process responses from the completion service - this.processResponses(responseIterable) - } - - // Enqueue the request - this.lastIterator.enQueue(req) + // Enqueue the request + this.lastIterator.enQueue(req) + }) } processResponses = async (responses: AsyncIterable) => { diff --git a/src/extension/extension.ts b/src/extension/extension.ts index d6c8dcd37..573d30080 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -179,7 +179,7 @@ export class RunmeExtension { } // Start the AIManager. This will enable the AI services if the user has enabled them. - const aiManager = new manager.AIManager() + const aiManager = new manager.AIManager(kernel) // We need to hang onto a reference to the AIManager so it doesn't get garbage collected until the // extension is deactivated. context.subscriptions.push(aiManager) From 2e2dcd69570400a6ea80c934d64fc850d6b6d2c4 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Fri, 25 Oct 2024 17:58:26 -0700 Subject: [PATCH 2/4] Update to use await. --- src/extension/ai/ghost.ts | 87 ++++++++++++++++++++----------------- src/extension/ai/manager.ts | 2 + src/extension/ai/stream.ts | 68 ++++++++++++++--------------- 3 files changed, 84 insertions(+), 73 deletions(-) diff --git a/src/extension/ai/ghost.ts b/src/extension/ai/ghost.ts index 09e402831..5c180ce00 100644 --- a/src/extension/ai/ghost.ts +++ b/src/extension/ai/ghost.ts @@ -70,7 +70,7 @@ export class GhostCellGenerator implements stream.CompletionHandlers { // This is a stateful transformation because we need to decide whether to send the full document or // the incremental changes. It will return a null request if the event should be ignored or if there // is an error preventing it from computing a proper request. - buildRequest( + async buildRequest( cellChangeEvent: stream.CellChangeEvent, firstRequest: boolean, ): Promise { @@ -121,37 +121,35 @@ export class GhostCellGenerator implements stream.CompletionHandlers { let cellData = notebook.getCells().map((cell) => converters.cellToCellData(cell)) let notebookData = new vscode.NotebookData(cellData) - return this.converter.notebookDataToProto(notebookData).then((notebookProto) => { - let request = new agent_pb.StreamGenerateRequest({ - contextId: SessionManager.getManager().getID(), - request: { - case: 'fullContext', - value: new agent_pb.FullContext({ - notebook: notebookProto, - selected: matchedCell.index, - notebookUri: notebook.uri.toString(), - }), - }, - }) - return request + let notebookProto = await this.converter.notebookDataToProto(notebookData) + let request = new agent_pb.StreamGenerateRequest({ + contextId: SessionManager.getManager().getID(), + request: { + case: 'fullContext', + value: new agent_pb.FullContext({ + notebook: notebookProto, + selected: matchedCell.index, + notebookUri: notebook.uri.toString(), + }), + }, }) + return request } else { let cellData = converters.cellToCellData(matchedCell) let notebookData = new vscode.NotebookData([cellData]) // Generate an update request - return this.converter.notebookDataToProto(notebookData).then((notebookProto) => { - let request = new agent_pb.StreamGenerateRequest({ - contextId: SessionManager.getManager().getID(), - request: { - case: 'update', - value: new agent_pb.UpdateContext({ - cell: notebookProto.cells[0], - }), - }, - }) - return request + let notebookProto = await this.converter.notebookDataToProto(notebookData) + let request = new agent_pb.StreamGenerateRequest({ + contextId: SessionManager.getManager().getID(), + request: { + case: 'update', + value: new agent_pb.UpdateContext({ + cell: notebookProto.cells[0], + }), + }, }) + return request } } @@ -344,13 +342,16 @@ export class CellChangeEventGenerator { return } - this.streamCreator.handleEvent( - new stream.CellChangeEvent( - notebook.uri.toString(), - matchedCell.index, - StreamGenerateRequest_Trigger.CELL_TEXT_CHANGE, - ), - ) + // N.B. handlEvent is aysnc. So we need to use "then" to make sure the event gets processed + this.streamCreator + .handleEvent( + new stream.CellChangeEvent( + notebook.uri.toString(), + matchedCell.index, + StreamGenerateRequest_Trigger.CELL_TEXT_CHANGE, + ), + ) + .then(() => {}) } // handleOnDidChangeVisibleTextEditors is called when the visible text editors change. @@ -381,7 +382,13 @@ export class CellChangeEventGenerator { } handleOnDidChangeNotebookDocument = (event: vscode.NotebookDocumentChangeEvent) => { + // N.B. For non-interactive cells this will trigger each time the output is updated. + // For interactive cells this doesn't appear to trigger each time the cell output is updated. + // For example, if you have a long running command (e.g. a bash for loop with a sleep that + // echos a message on each iteration) then this won't trigger on each iteration for + // an interactive cell but will for non-interactive. event.cellChanges.forEach((change) => { + log.info(`handleOnDidChangeNotebookDocument: change: ${change}`) if (change.outputs !== undefined) { // If outputs change then we want to trigger completions. @@ -396,13 +403,15 @@ export class CellChangeEventGenerator { // In particular its possible that the cell that changed is not the active cell. Therefore // we may not want to generate completions for it. For example, you can have multiple cells // running. So in principle the active cell could be different from the cell that changed. - this.streamCreator.handleEvent( - new stream.CellChangeEvent( - change.cell.notebook.uri.toString(), - change.cell.index, - StreamGenerateRequest_Trigger.CELL_OUTPUT_CHANGE, - ), - ) + this.streamCreator + .handleEvent( + new stream.CellChangeEvent( + change.cell.notebook.uri.toString(), + change.cell.index, + StreamGenerateRequest_Trigger.CELL_OUTPUT_CHANGE, + ), + ) + .then(() => {}) } }) } diff --git a/src/extension/ai/manager.ts b/src/extension/ai/manager.ts index 290c5d417..64fad0e7a 100644 --- a/src/extension/ai/manager.ts +++ b/src/extension/ai/manager.ts @@ -79,6 +79,8 @@ export class AIManager { vscode.window.onDidChangeActiveTextEditor(cellGenerator.handleOnDidChangeActiveTextEditor), ) + // We use onDidChangeNotebookDocument to listen for changes to outputs. + // We use this to trigger updates in response to a cell's output being updated. this.subscriptions.push( vscode.workspace.onDidChangeNotebookDocument( eventGenerator.handleOnDidChangeNotebookDocument, diff --git a/src/extension/ai/stream.ts b/src/extension/ai/stream.ts index dee37aae2..9df45a03f 100644 --- a/src/extension/ai/stream.ts +++ b/src/extension/ai/stream.ts @@ -75,7 +75,7 @@ export class StreamCreator { // handleEvent processes a request // n.b we use arror function definition to ensure this gets properly bound // see https://www.typescriptlang.org/docs/handbook/2/classes.html#this-at-runtime-in-classes - handleEvent = (event: CellChangeEvent): void => { + handleEvent = async (event: CellChangeEvent): Promise => { // We need to generate a new request let firstRequest = false if (this.lastIterator === undefined || this.lastIterator === null) { @@ -83,45 +83,45 @@ export class StreamCreator { } log.info('handleEvent: building request') - this.handlers.buildRequest(event, firstRequest).then((req) => { - if (req === null) { - log.info(`Notebook: ${event.notebookUri}; no request generated`) - return - } + let req = await this.handlers.buildRequest(event, firstRequest) - // If the request is a fullContext request we need to start a new stream - if (req.request.case === 'fullContext') { - firstRequest = true - } + if (req === null) { + log.info(`Notebook: ${event.notebookUri}; no request generated`) + return + } - if (this.lastIterator !== undefined && this.lastIterator !== null && firstRequest === true) { - console.log('Stopping the current stream') - this.lastIterator.close() - this.lastIterator = null - } + // If the request is a fullContext request we need to start a new stream + if (req.request.case === 'fullContext') { + firstRequest = true + } - if (this.lastIterator === null) { - // n.b. we need to define newIterator and then refer to newIterator in the closure - let newIterator = new PromiseIterator() - this.lastIterator = newIterator - // start the bidirectional stream - let iterable = { - [Symbol.asyncIterator]: () => { - // n.b. We don't want to refer to this.lastIterator because we need to create a closure - // this.lastIterator is a reference that will get be updated over time. We don't want the iterator though - // to change. - return newIterator - }, - } + if (this.lastIterator !== undefined && this.lastIterator !== null && firstRequest === true) { + console.log('Stopping the current stream') + this.lastIterator.close() + this.lastIterator = null + } - const responseIterable = this.client.streamGenerate(iterable) - // Start a coroutine to process responses from the completion service - this.processResponses(responseIterable) + if (this.lastIterator === null) { + // n.b. we need to define newIterator and then refer to newIterator in the closure + let newIterator = new PromiseIterator() + this.lastIterator = newIterator + // start the bidirectional stream + let iterable = { + [Symbol.asyncIterator]: () => { + // n.b. We don't want to refer to this.lastIterator because we need to create a closure + // this.lastIterator is a reference that will get be updated over time. We don't want the iterator though + // to change. + return newIterator + }, } - // Enqueue the request - this.lastIterator.enQueue(req) - }) + const responseIterable = this.client.streamGenerate(iterable) + // Start a coroutine to process responses from the completion service + this.processResponses(responseIterable) + } + + // Enqueue the request + this.lastIterator.enQueue(req) } processResponses = async (responses: AsyncIterable) => { From 328740de4587038f3b39d5e731448a409ce2077e Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Mon, 28 Oct 2024 08:08:26 -0700 Subject: [PATCH 3/4] Add a comment. --- src/extension/ai/ghost.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/extension/ai/ghost.ts b/src/extension/ai/ghost.ts index 5c180ce00..e942818b4 100644 --- a/src/extension/ai/ghost.ts +++ b/src/extension/ai/ghost.ts @@ -403,6 +403,8 @@ export class CellChangeEventGenerator { // In particular its possible that the cell that changed is not the active cell. Therefore // we may not want to generate completions for it. For example, you can have multiple cells // running. So in principle the active cell could be different from the cell that changed. + // + // n.b. Do we need a .then to make sure the event gets processed? this.streamCreator .handleEvent( new stream.CellChangeEvent( From 67df2809df8a7b222fdaa4987dde22ad7563ccf1 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Tue, 29 Oct 2024 16:03:54 -0700 Subject: [PATCH 4/4] Use await. --- src/extension/ai/ghost.ts | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/extension/ai/ghost.ts b/src/extension/ai/ghost.ts index e942818b4..203a9b61f 100644 --- a/src/extension/ai/ghost.ts +++ b/src/extension/ai/ghost.ts @@ -316,7 +316,7 @@ export class CellChangeEventGenerator { this.streamCreator = streamCreator } - handleOnDidChangeNotebookCell = (event: vscode.TextDocumentChangeEvent) => { + handleOnDidChangeNotebookCell = async (event: vscode.TextDocumentChangeEvent) => { if (![vsCodeCellScheme].includes(event.document.uri.scheme)) { return } @@ -342,16 +342,13 @@ export class CellChangeEventGenerator { return } - // N.B. handlEvent is aysnc. So we need to use "then" to make sure the event gets processed - this.streamCreator - .handleEvent( - new stream.CellChangeEvent( - notebook.uri.toString(), - matchedCell.index, - StreamGenerateRequest_Trigger.CELL_TEXT_CHANGE, - ), - ) - .then(() => {}) + await this.streamCreator.handleEvent( + new stream.CellChangeEvent( + notebook.uri.toString(), + matchedCell.index, + StreamGenerateRequest_Trigger.CELL_TEXT_CHANGE, + ), + ) } // handleOnDidChangeVisibleTextEditors is called when the visible text editors change. @@ -387,7 +384,7 @@ export class CellChangeEventGenerator { // For example, if you have a long running command (e.g. a bash for loop with a sleep that // echos a message on each iteration) then this won't trigger on each iteration for // an interactive cell but will for non-interactive. - event.cellChanges.forEach((change) => { + event.cellChanges.forEach(async (change) => { log.info(`handleOnDidChangeNotebookDocument: change: ${change}`) if (change.outputs !== undefined) { // If outputs change then we want to trigger completions. @@ -404,16 +401,13 @@ export class CellChangeEventGenerator { // we may not want to generate completions for it. For example, you can have multiple cells // running. So in principle the active cell could be different from the cell that changed. // - // n.b. Do we need a .then to make sure the event gets processed? - this.streamCreator - .handleEvent( - new stream.CellChangeEvent( - change.cell.notebook.uri.toString(), - change.cell.index, - StreamGenerateRequest_Trigger.CELL_OUTPUT_CHANGE, - ), - ) - .then(() => {}) + await this.streamCreator.handleEvent( + new stream.CellChangeEvent( + change.cell.notebook.uri.toString(), + change.cell.index, + StreamGenerateRequest_Trigger.CELL_OUTPUT_CHANGE, + ), + ) } }) }