Adopt view zone api for notebook cell chat

This commit is contained in:
rebornix
2024-02-09 16:47:29 -08:00
parent 2697c604f3
commit 0ca2dbb4ea
3 changed files with 482 additions and 519 deletions
@@ -6,7 +6,6 @@
import { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { localize, localize2 } from 'vs/nls';
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility';
import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
@@ -15,11 +14,9 @@ import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkey
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations';
import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS, NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController';
import { INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, getEditorFromArgsOrActivePane } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
import { insertNewCell } from 'vs/workbench/contrib/notebook/browser/controller/insertCellActions';
import { CellEditState, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS, NotebookCellChatController } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController';
import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
@@ -46,12 +43,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const ctrl = NotebookCellChatController.get(context.cell);
if (!ctrl) {
return;
}
ctrl.acceptInput();
NotebookChatController.get(context.notebookEditor)?.acceptInput();
}
});
@@ -115,9 +107,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const editor = context.notebookEditor;
const activeCell = context.cell;
await editor.focusNotebookCell(activeCell, 'editor');
await NotebookChatController.get(context.notebookEditor)?.focusNext();
}
});
@@ -146,14 +136,8 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const activeCell = context.cell;
// Navigate to cell chat widget if it exists
const controller = NotebookCellChatController.get(activeCell);
if (controller && controller.isWidgetVisible()) {
controller.focusWidget();
return;
}
const index = context.notebookEditor.getCellIndex(context.cell);
await NotebookChatController.get(context.notebookEditor)?.focusNearestWidget(index, 'above');
}
});
@@ -182,29 +166,8 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const editor = context.notebookEditor;
const activeCell = context.cell;
const idx = editor.getCellIndex(activeCell);
if (typeof idx !== 'number') {
return;
}
if (idx >= editor.getLength() - 1) {
// last one
return;
}
const targetCell = editor.cellAt(idx + 1);
if (targetCell) {
// Navigate to cell chat widget if it exists
const controller = NotebookCellChatController.get(targetCell);
if (controller && controller.isWidgetVisible()) {
controller.focusWidget();
return;
}
}
const index = context.notebookEditor.getCellIndex(context.cell);
await NotebookChatController.get(context.notebookEditor)?.focusNearestWidget(index, 'below');
}
});
@@ -225,12 +188,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const ctrl = NotebookCellChatController.get(context.cell);
if (!ctrl) {
return;
}
ctrl.cancelCurrentRequest(false);
NotebookChatController.get(context.notebookEditor)?.cancelCurrentRequest(false);
}
});
@@ -250,12 +208,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const ctrl = NotebookCellChatController.get(context.cell);
if (!ctrl) {
return;
}
ctrl.dismiss(false);
NotebookChatController.get(context.notebookEditor)?.dismiss();
}
});
@@ -285,12 +238,7 @@ registerAction2(class extends NotebookAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const ctrl = NotebookCellChatController.get(context.cell);
if (!ctrl) {
return;
}
ctrl.acceptSession();
NotebookChatController.get(context.notebookEditor)?.acceptSession();
}
});
@@ -315,15 +263,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const ctrl = NotebookCellChatController.get(context.cell);
if (!ctrl) {
return;
}
// todo discard
ctrl.dismiss(true);
// focus on the cell editor container
context.notebookEditor.focusNotebookCell(context.cell, 'container');
NotebookChatController.get(context.notebookEditor)?.discard();
}
});
@@ -343,12 +283,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const ctrl = NotebookCellChatController.get(context.cell);
if (!ctrl) {
return;
}
ctrl.feedbackLast(InlineChatResponseFeedbackKind.Helpful);
NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Helpful);
}
});
@@ -368,12 +303,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const ctrl = NotebookCellChatController.get(context.cell);
if (!ctrl) {
return;
}
ctrl.feedbackLast(InlineChatResponseFeedbackKind.Unhelpful);
NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Unhelpful);
}
});
@@ -393,12 +323,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const ctrl = NotebookCellChatController.get(context.cell);
if (!ctrl) {
return;
}
ctrl.feedbackLast(InlineChatResponseFeedbackKind.Bug);
NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Bug);
}
});
@@ -458,7 +383,22 @@ registerAction2(class extends NotebookAction {
override getEditorContextFromArgsOrActive(accessor: ServicesAccessor, ...args: any[]): IInsertCellWithChatArgs | undefined {
const [firstArg] = args;
if (!firstArg) {
return undefined;
const notebookEditor = getEditorFromArgsOrActivePane(accessor);
if (!notebookEditor) {
return undefined;
}
const activeCell = notebookEditor.getActiveCell();
if (!activeCell) {
return undefined;
}
return {
cell: activeCell,
notebookEditor,
input: undefined,
autoSend: undefined
};
}
if (typeof firstArg !== 'object' || typeof firstArg.index !== 'number') {
@@ -481,33 +421,9 @@ registerAction2(class extends NotebookAction {
}
async runWithContext(accessor: ServicesAccessor, context: IInsertCellWithChatArgs) {
let newCell: ICellViewModel | null = null;
if (!context.cell) {
// insert at the top
const languageService = accessor.get(ILanguageService);
newCell = insertCell(languageService, context.notebookEditor, 0, CellKind.Code, 'above', undefined, true);
} else {
newCell = insertNewCell(accessor, context, CellKind.Code, 'below', true);
}
if (!newCell) {
return;
}
await context.notebookEditor.focusNotebookCell(newCell, 'container');
const ctrl = NotebookCellChatController.get(newCell);
if (!ctrl) {
return;
}
context.notebookEditor.getCellsInRange().forEach(cell => {
const cellCtrl = NotebookCellChatController.get(cell);
if (cellCtrl) {
cellCtrl.dismiss(false);
}
});
ctrl.show(context.input, context.autoSend);
const index = Math.max(0, context.cell ? context.notebookEditor.getCellIndex(context.cell) + 1 : 0);
context.notebookEditor.focusContainer();
NotebookChatController.get(context.notebookEditor)?.run(index, context.input, context.autoSend);
}
});
@@ -537,26 +453,8 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) {
const languageService = accessor.get(ILanguageService);
const newCell = insertCell(languageService, context.notebookEditor, 0, CellKind.Code, 'above', undefined, true);
if (!newCell) {
return;
}
await context.notebookEditor.focusNotebookCell(newCell, 'container');
const ctrl = NotebookCellChatController.get(newCell);
if (!ctrl) {
return;
}
context.notebookEditor.getCellsInRange().forEach(cell => {
const cellCtrl = NotebookCellChatController.get(cell);
if (cellCtrl) {
cellCtrl.dismiss(false);
}
});
ctrl.show();
context.notebookEditor.focusContainer();
NotebookChatController.get(context.notebookEditor)?.run(0, '', false);
}
});
@@ -3,25 +3,29 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Dimension, WindowIntervalTimer } from 'vs/base/browser/dom';
import { CancelablePromise, Queue, createCancelablePromise, raceCancellationError } from 'vs/base/common/async';
import { Dimension, WindowIntervalTimer, getWindow, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
import { CancelablePromise, Queue, createCancelablePromise, disposableTimeout, raceCancellationError } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Event } from 'vs/base/common/event';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { MovingAverage } from 'vs/base/common/numbers';
import { StopWatch } from 'vs/base/common/stopwatch';
import { assertType } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { Position } from 'vs/editor/common/core/position';
import { Selection } from 'vs/editor/common/core/selection';
import { TextEdit } from 'vs/editor/common/languages';
import { ICursorStateComputer } from 'vs/editor/common/model';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { ICursorStateComputer, ITextModel } from 'vs/editor/common/model';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { IModelService } from 'vs/editor/common/services/model';
import { localize } from 'vs/nls';
import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
import { MenuId } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
@@ -29,16 +33,21 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { AsyncProgress } from 'vs/platform/progress/common/progress';
import { SaveReason } from 'vs/workbench/common/editor';
import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService';
import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService';
import { ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies';
import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/utils';
import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget';
import { CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, EditMode, IInlineChatProgressItem, IInlineChatRequest, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService';
import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/utils';
import { CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, EditMode, IInlineChatProgressItem, IInlineChatRequest, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { insertCell, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations';
import { INotebookEditor, INotebookEditorContribution, INotebookViewZone, ScrollToRevealBehavior } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import 'vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions';
export const CTX_NOTEBOOK_CELL_CHAT_FOCUSED = new RawContextKey<boolean>('notebookCellChatFocused', false, localize('notebookCellChatFocused', "Whether the cell chat editor is focused"));
export const CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey<boolean>('notebookChatHasActiveRequest', false, localize('notebookChatHasActiveRequest', "Whether the cell chat editor has an active request"));
@@ -48,219 +57,374 @@ export const MENU_CELL_CHAT_WIDGET_STATUS = MenuId.for('cellChatWidget.status');
export const MENU_CELL_CHAT_WIDGET_FEEDBACK = MenuId.for('cellChatWidget.feedback');
export const MENU_CELL_CHAT_WIDGET_TOOLBAR = MenuId.for('cellChatWidget.toolbar');
interface ICellChatPart {
activeCell: ICellViewModel | undefined;
}
const WIDGET_MARGIN_BOTTOM = 16;
export class NotebookCellChatController extends Disposable {
private static _cellChatControllers = new WeakMap<ICellViewModel, NotebookCellChatController>();
class NotebookChatWidget extends Disposable implements INotebookViewZone {
private _afterModelPosition: number;
static get(cell: ICellViewModel): NotebookCellChatController | undefined {
return NotebookCellChatController._cellChatControllers.get(cell);
set afterModelPosition(afterModelPosition: number) {
this._afterModelPosition = afterModelPosition;
}
private _sessionCtor: CancelablePromise<void> | undefined;
private _activeSession?: Session;
private readonly _ctxHasActiveRequest: IContextKey<boolean>;
private _isVisible: boolean = false;
private _strategy: EditStrategy | undefined;
get afterModelPosition(): number {
return this._afterModelPosition;
}
private _heightInPx: number;
set heightInPx(heightInPx: number) {
this._heightInPx = heightInPx;
}
get heightInPx(): number {
return this._heightInPx;
}
private _editingCell: CellViewModel | null = null;
private _inlineChatListener: IDisposable | undefined;
private _widget: InlineChatWidget | undefined;
private _toolbar: MenuWorkbenchToolBar | undefined;
private readonly _ctxVisible: IContextKey<boolean>;
private readonly _ctxCellWidgetFocused: IContextKey<boolean>;
private readonly _ctxLastResponseType: IContextKey<undefined | InlineChatResponseType>;
private _widgetDisposableStore: DisposableStore = this._register(new DisposableStore());
constructor(
private readonly _notebookEditor: INotebookEditorDelegate,
private readonly _chatPart: ICellChatPart,
private readonly _cell: ICellViewModel,
private readonly _partContainer: HTMLElement,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService,
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService,
@ICommandService private readonly _commandService: ICommandService,
@IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService,
private readonly _notebookEditor: INotebookEditor,
readonly id: string,
readonly domNode: HTMLElement,
readonly widgetContainer: HTMLElement,
readonly inlineChatWidget: InlineChatWidget,
readonly parentEditor: CodeEditorWidget,
afterModelPosition: number,
heightInPx: number,
private readonly _languageService: ILanguageService,
) {
super();
NotebookCellChatController._cellChatControllers.set(this._cell, this);
this._ctxHasActiveRequest = CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.bindTo(this._contextKeyService);
this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(_contextKeyService);
this._ctxCellWidgetFocused = CTX_NOTEBOOK_CELL_CHAT_FOCUSED.bindTo(this._contextKeyService);
this._ctxLastResponseType = CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.bindTo(this._contextKeyService);
this._afterModelPosition = afterModelPosition;
this._heightInPx = heightInPx;
this._register(this._cell.onDidChangeEditorAttachState(() => {
const editor = this._getCellEditor();
this._inlineChatListener?.dispose();
if (!editor) {
return;
}
if (!this._widget && this._isVisible) {
this._initialize(editor);
}
const inlineChatController = InlineChatController.get(editor);
if (inlineChatController) {
this._inlineChatListener = inlineChatController.onWillStartSession(() => {
this.dismiss(false);
});
}
this._register(inlineChatWidget.onDidChangeHeight(() => {
this.heightInPx = inlineChatWidget.getHeight() + WIDGET_MARGIN_BOTTOM;
this._notebookEditor.changeViewZones(accessor => {
accessor.layoutZone(id);
});
this._layoutWidget(inlineChatWidget, widgetContainer);
}));
this._layoutWidget(inlineChatWidget, widgetContainer);
}
private _initialize(editor: IActiveCodeEditor) {
this._widget = this._instantiationService.createInstance(InlineChatWidget, editor, {
menuId: MENU_CELL_CHAT_INPUT,
widgetMenuId: MENU_CELL_CHAT_WIDGET,
statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS,
feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK
focus() {
this.inlineChatWidget.focus();
}
async getEditingCellEditor() {
if (this._editingCell) {
await this._notebookEditor.focusNotebookCell(this._editingCell, 'editor');
return this._notebookEditor.activeCodeEditor;
}
if (!this._notebookEditor.hasModel()) {
return undefined;
}
this._editingCell = insertCell(this._languageService, this._notebookEditor, this._afterModelPosition, CellKind.Code, 'above');
if (!this._editingCell) {
return undefined;
}
await this._notebookEditor.focusNotebookCell(this._editingCell, 'editor', { revealBehavior: ScrollToRevealBehavior.firstLine });
return this._notebookEditor.activeCodeEditor;
}
async discardChange() {
if (this._notebookEditor.hasModel() && this._editingCell) {
// remove the cell from the notebook
runDeleteAction(this._notebookEditor, this._editingCell);
}
}
private _layoutWidget(inlineChatWidget: InlineChatWidget, widgetContainer: HTMLElement) {
const layoutConfiguration = this._notebookEditor.notebookOptions.getLayoutConfiguration();
const rightMargin = layoutConfiguration.cellRightMargin;
const leftMargin = this._notebookEditor.notebookOptions.getCellEditorContainerLeftMargin();
const maxWidth = !inlineChatWidget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER;
const width = Math.min(maxWidth, this._notebookEditor.getLayoutInfo().width - leftMargin - rightMargin);
inlineChatWidget.layout(new Dimension(width, 80 + WIDGET_MARGIN_BOTTOM));
inlineChatWidget.domNode.style.width = `${width}px`;
widgetContainer.style.left = `${leftMargin}px`;
}
override dispose() {
this._notebookEditor.changeViewZones(accessor => {
accessor.removeZone(this.id);
});
this._widgetDisposableStore.add(this._widget.onDidChangeHeight(() => {
this._updateHeight();
}));
this._widgetDisposableStore.add(this._notebookExecutionStateService.onDidChangeExecution(e => {
if (e.notebook.toString() !== this._notebookEditor.textModel?.uri.toString()) {
return;
}
if (e.type === NotebookExecutionType.cell && e.affectsCell(this._cell.uri) && e.changed === undefined /** complete */) {
// check if execution is successfull
const { lastRunSuccess } = this._cell.internalMetadata;
if (lastRunSuccess) {
this._strategy?.createSnapshot();
}
}
}));
this._partContainer.appendChild(this._widget.domNode);
}
public override dispose(): void {
if (this._isVisible) {
// detach the chat widget
this._widget?.reset();
this._sessionCtor?.cancel();
this._sessionCtor = undefined;
}
try {
if (this._widget) {
this._partContainer.removeChild(this._widget.domNode);
}
} catch (_ex) {
// might not be attached
}
// dismiss since we can't restore the widget properly now
this.dismiss(false);
this._widget?.dispose();
this._inlineChatListener?.dispose();
this._toolbar?.dispose();
this._inlineChatListener = undefined;
this._ctxHasActiveRequest.reset();
this._ctxVisible.reset();
NotebookCellChatController._cellChatControllers.delete(this._cell);
this.domNode.remove();
super.dispose();
}
}
isWidgetVisible() {
return this._isVisible;
export class NotebookChatController extends Disposable implements INotebookEditorContribution {
static id: string = 'workbench.notebook.chatController';
static counter: number = 0;
public static get(editor: INotebookEditor): NotebookChatController | null {
return editor.getContribution<NotebookChatController>(NotebookChatController.id);
}
private _strategy: EditStrategy | undefined;
private _sessionCtor: CancelablePromise<void> | undefined;
private _activeSession?: Session;
private readonly _ctxHasActiveRequest: IContextKey<boolean>;
private readonly _ctxCellWidgetFocused: IContextKey<boolean>;
private readonly _ctxLastResponseType: IContextKey<undefined | InlineChatResponseType>;
private _widget: NotebookChatWidget | undefined;
constructor(
private readonly _notebookEditor: INotebookEditor,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@ICommandService private readonly _commandService: ICommandService,
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
@IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService,
@IModelService private readonly _modelService: IModelService,
@ILanguageService private readonly _languageService: ILanguageService,
) {
super();
this._ctxHasActiveRequest = CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.bindTo(this._contextKeyService);
this._ctxCellWidgetFocused = CTX_NOTEBOOK_CELL_CHAT_FOCUSED.bindTo(this._contextKeyService);
this._ctxLastResponseType = CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.bindTo(this._contextKeyService);
}
layout() {
if (this._isVisible && this._widget) {
const width = this._notebookEditor.getLayoutInfo().width - (/** margin */ 16 + 6) - (/** padding */ 6 * 2);
const height = this._widget.getHeight();
this._widget.layout(new Dimension(width, height));
}
}
run(index: number, input: string | undefined, autoSend: boolean | undefined): void {
if (this._widget) {
if (this._widget.afterModelPosition === index) {
// this._chatZone
// chatZone focus
} else {
const window = getWindow(this._widget.domNode);
this._widget.dispose();
this._widget = undefined;
private _updateHeight() {
const surrounding = 6 * 2 /** padding */ + 6 /** cell chat widget margin bottom */ + 2 /** border */;
const heightWithPadding = this._isVisible && this._widget
? (this._widget.getHeight() - 8 /** shadow */ - 18 /** padding */ - 6 /** widget's internal margin top */ + surrounding)
: 0;
scheduleAtNextAnimationFrame(window, () => {
this._createWidget(index, input, autoSend);
});
}
if (this._cell.chatHeight === heightWithPadding) {
return;
}
this._cell.chatHeight = heightWithPadding;
this._partContainer.style.height = `${heightWithPadding - surrounding}px`;
this._createWidget(index, input, autoSend);
// TODO: reveal widget to the center if it's out of the viewport
}
async show(input?: string, autoSend?: boolean) {
this._isVisible = true;
if (!this._widget) {
const editor = this._getCellEditor();
if (editor) {
this._initialize(editor);
private _createWidget(index: number, input: string | undefined, autoSend: boolean | undefined) {
const viewZoneContainer = document.createElement('div');
viewZoneContainer.classList.add('monaco-editor');
const widgetContainer = document.createElement('div');
widgetContainer.style.position = 'absolute';
viewZoneContainer.appendChild(widgetContainer);
const fakeParentEditorElement = document.createElement('div');
const fakeParentEditor = this._instantiationService.createInstance(
CodeEditorWidget,
fakeParentEditorElement,
{
},
{ isSimpleWidget: true }
);
const inputBoxPath = `/notebook-chat-input0-${NotebookChatController.counter++}`;
const inputUri = URI.from({ scheme: Schemas.untitled, path: inputBoxPath });
const result: ITextModel = this._modelService.createModel('', null, inputUri, false);
fakeParentEditor.setModel(result);
const inlineChatWidget = this._instantiationService.createInstance(
InlineChatWidget,
fakeParentEditor,
{
menuId: MENU_CELL_CHAT_INPUT,
widgetMenuId: MENU_CELL_CHAT_WIDGET,
statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS,
feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK
}
}
);
inlineChatWidget.placeholder = localize('default.placeholder', "Ask a question");
inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated code may be incorrect"));
widgetContainer.appendChild(inlineChatWidget.domNode);
this._partContainer.style.display = 'flex';
this._widget?.focus();
this._widget?.updateInfo(localize('welcome.1', "AI-generated code may be incorrect"));
this._ctxVisible.set(true);
this._ctxCellWidgetFocused.set(true);
this._updateHeight();
this._notebookEditor.changeViewZones(accessor => {
const id = accessor.addZone({
afterModelPosition: index,
heightInPx: 80 + WIDGET_MARGIN_BOTTOM,
domNode: viewZoneContainer
});
this._sessionCtor = createCancelablePromise<void>(async token => {
if (this._cell.editorAttached) {
const editor = this._getCellEditor();
if (editor) {
await this._startSession(editor, token);
this._widget = new NotebookChatWidget(
this._notebookEditor,
id,
viewZoneContainer,
widgetContainer,
inlineChatWidget,
fakeParentEditor,
index,
80 + WIDGET_MARGIN_BOTTOM,
this._languageService
);
disposableTimeout(() => {
this._ctxCellWidgetFocused.set(true);
this._widget?.focus();
}, 0, this._store);
this._sessionCtor = createCancelablePromise<void>(async token => {
if (fakeParentEditor.hasModel()) {
this._startSession(fakeParentEditor, token);
if (this._widget) {
this._widget.inlineChatWidget.placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question");
this._widget.inlineChatWidget.updateInfo(this._activeSession?.session.message ?? localize('welcome.1', "AI-generated code may be incorrect"));
this._widget.focus();
}
if (this._widget && input) {
this._widget.inlineChatWidget.value = input;
if (autoSend) {
this.acceptInput();
}
}
}
} else {
await Event.toPromise(Event.once(this._cell.onDidChangeEditorAttachState));
if (token.isCancellationRequested) {
return;
}
const editor = this._getCellEditor();
if (editor) {
await this._startSession(editor, token);
}
}
if (this._widget) {
this._widget.placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question");
this._widget.updateInfo(this._activeSession?.session.message ?? localize('welcome.1', "AI-generated code may be incorrect"));
this._widget.focus();
}
if (this._widget && input) {
this._widget.value = input;
if (autoSend) {
this.acceptInput();
}
}
});
});
}
async focusWidget() {
this._widget?.focus();
}
async acceptInput() {
assertType(this._activeSession);
assertType(this._widget);
this._activeSession.addInput(new SessionPrompt(this._widget.inlineChatWidget.value));
private _getCellEditor() {
const editors = this._notebookEditor.codeEditors.find(editor => editor[0] === this._chatPart.activeCell);
if (!editors || !editors[1].hasModel()) {
assertType(this._activeSession.lastInput);
const value = this._activeSession.lastInput.value;
const editor = this._widget.parentEditor;
const model = editor.getModel();
if (!editor.hasModel() || !model) {
return;
}
const editor = editors[1];
return editor;
this._ctxHasActiveRequest.set(true);
this._widget?.inlineChatWidget.updateProgress(true);
const request: IInlineChatRequest = {
requestId: generateUuid(),
prompt: value,
attempt: 0,
selection: { selectionStartLineNumber: 1, selectionStartColumn: 1, positionLineNumber: 1, positionColumn: 1 },
wholeRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
live: true,
previewDocument: model.uri,
withIntentDetection: true, // TODO: don't hard code but allow in corresponding UI to run without intent detection?
};
//TODO: update progress in a newly inserted cell below the widget instead of the fake editor
const requestCts = new CancellationTokenSource();
const progressEdits: TextEdit[][] = [];
const progressiveEditsQueue = new Queue();
const progressiveEditsClock = StopWatch.create();
const progressiveEditsAvgDuration = new MovingAverage();
const progressiveEditsCts = new CancellationTokenSource(requestCts.token);
const progress = new AsyncProgress<IInlineChatProgressItem>(async data => {
// console.log('received chunk', data, request);
if (requestCts.token.isCancellationRequested) {
return;
}
if (data.message) {
this._widget?.inlineChatWidget.updateToolbar(false);
this._widget?.inlineChatWidget.updateInfo(data.message);
}
if (data.edits?.length) {
if (!request.live) {
throw new Error('Progress in NOT supported in non-live mode');
}
progressEdits.push(data.edits);
progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed());
progressiveEditsClock.reset();
progressiveEditsQueue.queue(async () => {
// making changes goes into a queue because otherwise the async-progress time will
// influence the time it takes to receive the changes and progressive typing will
// become infinitely fast
await this._makeChanges(data.edits!, data.editsShouldBeInstant
? undefined
: { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token }
);
});
}
});
const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, progress, requestCts.token);
let response: ReplyResponse | ErrorResponse | EmptyResponse;
try {
this._widget?.inlineChatWidget.updateChatMessage(undefined);
this._widget?.inlineChatWidget.updateFollowUps(undefined);
this._widget?.inlineChatWidget.updateProgress(true);
this._widget?.inlineChatWidget.updateInfo(!this._activeSession.lastExchange ? localize('thinking', "Thinking\u2026") : '');
this._ctxHasActiveRequest.set(true);
const reply = await raceCancellationError(Promise.resolve(task), requestCts.token);
if (progressiveEditsQueue.size > 0) {
// we must wait for all edits that came in via progress to complete
await Event.toPromise(progressiveEditsQueue.onDrained);
}
await progress.drain();
if (!reply) {
response = new EmptyResponse();
} else {
const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false });
const replyResponse = response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits, request.requestId);
for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) {
await this._makeChanges(replyResponse.allLocalEdits[i], undefined);
}
if (this._activeSession?.provider.provideFollowups) {
const followupCts = new CancellationTokenSource();
const followups = await this._activeSession.provider.provideFollowups(this._activeSession.session, replyResponse.raw, followupCts.token);
if (followups && this._widget) {
const widget = this._widget;
widget.inlineChatWidget.updateFollowUps(followups, async followup => {
if (followup.kind === 'reply') {
widget.inlineChatWidget.value = followup.message;
this.acceptInput();
} else {
await this.acceptSession();
this._commandService.executeCommand(followup.commandId, ...(followup.args ?? []));
}
});
}
}
}
} catch (e) {
response = new ErrorResponse(e);
} finally {
this._ctxHasActiveRequest.set(false);
this._widget?.inlineChatWidget.updateProgress(false);
this._widget?.inlineChatWidget.updateInfo('');
this._widget?.inlineChatWidget.updateToolbar(true);
}
this._ctxHasActiveRequest.set(false);
this._widget?.inlineChatWidget.updateProgress(false);
this._widget?.inlineChatWidget.updateInfo('');
this._widget?.inlineChatWidget.updateToolbar(true);
this._activeSession.addExchange(new SessionExchange(this._activeSession.lastInput, response));
this._ctxLastResponseType.set(response instanceof ReplyResponse ? response.raw.type : undefined);
}
private async _startSession(editor: IActiveCodeEditor, token: CancellationToken) {
@@ -282,184 +446,18 @@ export class NotebookCellChatController extends Disposable {
this._strategy = new EditStrategy(session);
}
async acceptInput() {
private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined) {
assertType(this._activeSession);
assertType(this._strategy);
assertType(this._widget);
this._activeSession.addInput(new SessionPrompt(this._widget.value));
assertType(this._activeSession.lastInput);
const editor = await this._widget.getEditingCellEditor();
const value = this._activeSession.lastInput.value;
const editors = this._notebookEditor.codeEditors.find(editor => editor[0] === this._chatPart.activeCell);
if (!editors || !editors[1].hasModel()) {
if (!editor || !editor.hasModel()) {
return;
}
const editor = editors[1];
this._ctxHasActiveRequest.set(true);
this._widget?.updateProgress(true);
const request: IInlineChatRequest = {
requestId: generateUuid(),
prompt: value,
attempt: 0,
selection: { selectionStartLineNumber: 1, selectionStartColumn: 1, positionLineNumber: 1, positionColumn: 1 },
wholeRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },
live: true,
previewDocument: editor.getModel().uri,
withIntentDetection: true, // TODO: don't hard code but allow in corresponding UI to run without intent detection?
};
const requestCts = new CancellationTokenSource();
const progressEdits: TextEdit[][] = [];
const progressiveEditsQueue = new Queue();
const progressiveEditsClock = StopWatch.create();
const progressiveEditsAvgDuration = new MovingAverage();
const progressiveEditsCts = new CancellationTokenSource(requestCts.token);
const progress = new AsyncProgress<IInlineChatProgressItem>(async data => {
// console.log('received chunk', data, request);
if (requestCts.token.isCancellationRequested) {
return;
}
if (data.message) {
this._widget?.updateToolbar(false);
this._widget?.updateInfo(data.message);
}
if (data.edits?.length) {
if (!request.live) {
throw new Error('Progress in NOT supported in non-live mode');
}
progressEdits.push(data.edits);
progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed());
progressiveEditsClock.reset();
progressiveEditsQueue.queue(async () => {
// making changes goes into a queue because otherwise the async-progress time will
// influence the time it takes to receive the changes and progressive typing will
// become infinitely fast
await this._makeChanges(editor, data.edits!, data.editsShouldBeInstant
? undefined
: { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token }
);
});
}
});
const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, progress, requestCts.token);
let response: ReplyResponse | ErrorResponse | EmptyResponse;
try {
this._widget?.updateChatMessage(undefined);
this._widget?.updateFollowUps(undefined);
this._widget?.updateProgress(true);
this._widget?.updateInfo(!this._activeSession.lastExchange ? localize('thinking', "Thinking\u2026") : '');
this._ctxHasActiveRequest.set(true);
const reply = await raceCancellationError(Promise.resolve(task), requestCts.token);
if (progressiveEditsQueue.size > 0) {
// we must wait for all edits that came in via progress to complete
await Event.toPromise(progressiveEditsQueue.onDrained);
}
await progress.drain();
if (!reply) {
response = new EmptyResponse();
} else {
const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false });
const replyResponse = response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits, request.requestId);
for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) {
await this._makeChanges(editor, replyResponse.allLocalEdits[i], undefined);
}
if (this._activeSession?.provider.provideFollowups) {
const followupCts = new CancellationTokenSource();
const followups = await this._activeSession.provider.provideFollowups(this._activeSession.session, replyResponse.raw, followupCts.token);
if (followups && this._widget) {
const widget = this._widget;
widget.updateFollowUps(followups, async followup => {
if (followup.kind === 'reply') {
widget.value = followup.message;
this.acceptInput();
} else {
await this.acceptSession();
this._commandService.executeCommand(followup.commandId, ...(followup.args ?? []));
}
});
}
}
}
} catch (e) {
response = new ErrorResponse(e);
} finally {
this._ctxHasActiveRequest.set(false);
this._widget?.updateProgress(false);
this._widget?.updateInfo('');
this._widget?.updateToolbar(true);
}
this._ctxHasActiveRequest.set(false);
this._widget?.updateProgress(false);
this._widget?.updateInfo('');
this._widget?.updateToolbar(true);
this._activeSession.addExchange(new SessionExchange(this._activeSession.lastInput, response));
this._ctxLastResponseType.set(response instanceof ReplyResponse ? response.raw.type : undefined);
}
async cancelCurrentRequest(discard: boolean) {
if (discard) {
this._strategy?.cancel();
}
if (this._activeSession) {
this._inlineChatSessionService.releaseSession(this._activeSession);
}
this._activeSession = undefined;
}
async acceptSession() {
assertType(this._activeSession);
assertType(this._strategy);
const editor = this._getCellEditor();
assertType(editor);
try {
await this._strategy.apply(editor);
} catch (_err) { }
this._inlineChatSessionService.releaseSession(this._activeSession);
this.dismiss(false);
}
async dismiss(discard: boolean) {
this._isVisible = false;
this._partContainer.style.display = 'none';
this.cancelCurrentRequest(discard);
this._ctxCellWidgetFocused.set(false);
this._ctxVisible.set(false);
this._ctxLastResponseType.reset();
this._widget?.reset();
this._updateHeight();
}
async feedbackLast(kind: InlineChatResponseFeedbackKind) {
if (this._activeSession?.lastExchange && this._activeSession.lastExchange.response instanceof ReplyResponse) {
this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, kind);
this._widget?.updateStatus('Thank you for your feedback!', { resetAfter: 1250 });
}
}
private async _makeChanges(editor: IActiveCodeEditor, edits: TextEdit[], opts: ProgressingEditsOptions | undefined) {
assertType(this._activeSession);
assertType(this._strategy);
const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(this._activeSession.textModelN.uri, edits);
const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(editor.getModel().uri, edits);
// this._log('edits from PROVIDER and after making them MORE MINIMAL', this._activeSession.provider.debugName, edits, moreMinimalEdits);
if (moreMinimalEdits?.length === 0) {
@@ -484,9 +482,91 @@ export class NotebookCellChatController extends Disposable {
// this._ignoreModelContentChanged = false;
}
}
async acceptSession() {
assertType(this._activeSession);
assertType(this._strategy);
const editor = this._widget?.parentEditor;
if (!editor?.hasModel()) {
return;
}
try {
await this._strategy.apply(editor);
} catch (_err) { }
this._inlineChatSessionService.releaseSession(this._activeSession);
this.dismiss();
}
async focusNext() {
if (!this._widget) {
return;
}
const index = this._widget.afterModelPosition;
const cell = this._notebookEditor.cellAt(index);
if (!cell) {
return;
}
await this._notebookEditor.focusNotebookCell(cell, 'editor');
}
focusNearestWidget(index: number, direction: 'above' | 'below') {
switch (direction) {
case 'above':
if (this._widget?.afterModelPosition === index) {
this._widget.focus();
}
break;
case 'below':
if (this._widget?.afterModelPosition === index + 1) {
this._widget.focus();
}
break;
default:
break;
}
}
async cancelCurrentRequest(discard: boolean) {
if (discard) {
this._strategy?.cancel();
}
if (this._activeSession) {
this._inlineChatSessionService.releaseSession(this._activeSession);
}
this._activeSession = undefined;
}
discard() {
this._strategy?.cancel();
this._widget?.discardChange();
}
async feedbackLast(kind: InlineChatResponseFeedbackKind) {
if (this._activeSession?.lastExchange && this._activeSession.lastExchange.response instanceof ReplyResponse) {
this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, kind);
this._widget?.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 });
}
}
dismiss() {
this._ctxCellWidgetFocused.set(false);
this._sessionCtor?.cancel();
this._sessionCtor = undefined;
this._widget?.dispose();
this._widget = undefined;
}
}
class EditStrategy {
export class EditStrategy {
private _editCount: number = 0;
constructor(
@@ -558,3 +638,7 @@ class EditStrategy {
}
}
}
registerNotebookContribution(NotebookChatController.id, NotebookChatController);
@@ -3,54 +3,35 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart';
import { NotebookCellChatController } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController';
import 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
export class CellChatPart extends CellContentPart {
private _controller: NotebookCellChatController | undefined;
// private _controller: NotebookCellChatController | undefined;
get activeCell() {
return this.currentCell;
}
constructor(
private readonly _notebookEditor: INotebookEditorDelegate,
private readonly _partContainer: HTMLElement,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
_notebookEditor: INotebookEditorDelegate,
_partContainer: HTMLElement,
) {
super();
}
override didRenderCell(element: ICellViewModel): void {
this._controller?.dispose();
const enabled = this._configurationService.getValue<boolean>(NotebookSetting.cellChat);
if (enabled) {
this._controller = this._instantiationService.createInstance(NotebookCellChatController, this._notebookEditor, this, element, this._partContainer);
}
super.didRenderCell(element);
}
override unrenderCell(element: ICellViewModel): void {
this._controller?.dispose();
this._controller = undefined;
super.unrenderCell(element);
}
override updateInternalLayoutNow(element: ICellViewModel): void {
this._controller?.layout();
}
override dispose() {
this._controller?.dispose();
this._controller = undefined;
super.dispose();
}
}