mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-14 01:34:20 +01:00
Adopt view zone api for notebook cell chat
This commit is contained in:
+36
-138
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
+443
-359
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user