diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 74900107cb2fd..a87cfef068bf1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -131,6 +131,12 @@ export interface InlineChatWidgetViewState { placeholder: string; } +export interface IInlineChatWidgetConstructionOptions { + menuId: MenuId; + statusMenuId: MenuId; + feedbackMenuId: MenuId; +} + export class InlineChatWidget { private static _modelPool: number = 1; @@ -209,6 +215,7 @@ export class InlineChatWidget { constructor( private readonly parentEditor: ICodeEditor, + _options: IInlineChatWidgetConstructionOptions, @IModelService private readonly _modelService: IModelService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @@ -342,7 +349,7 @@ export class InlineChatWidget { // toolbars - const toolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, MENU_INLINE_CHAT_WIDGET, { + const toolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, _options.menuId, { telemetrySource: 'interactiveEditorWidget-toolbar', toolbarOptions: { primaryGroup: 'main' } }); @@ -362,7 +369,7 @@ export class InlineChatWidget { return undefined; } }; - const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, MENU_INLINE_CHAT_WIDGET_STATUS, workbenchMenubarOptions); + const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, _options.statusMenuId, workbenchMenubarOptions); this._store.add(statusButtonBar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); this._store.add(statusButtonBar); @@ -375,7 +382,7 @@ export class InlineChatWidget { } }; - const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, MENU_INLINE_CHAT_WIDGET_FEEDBACK, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); + const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, _options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); this._store.add(feedbackToolbar); @@ -864,7 +871,11 @@ export class InlineChatZoneWidget extends ZoneWidget { this._ctxCursorPosition.reset(); })); - this.widget = this._instaService.createInstance(InlineChatWidget, this.editor); + this.widget = this._instaService.createInstance(InlineChatWidget, this.editor, { + menuId: MENU_INLINE_CHAT_WIDGET, + statusMenuId: MENU_INLINE_CHAT_WIDGET_STATUS, + feedbackMenuId: MENU_INLINE_CHAT_WIDGET_FEEDBACK + }); this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout())); this._disposables.add(this.widget); this.create(); diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css index 7af5203684a1f..e891cdce5ab90 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css @@ -15,11 +15,23 @@ padding: 8px 8px 0px 8px; } -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body { +.monaco-workbench .notebookOverlay .cell-chat-part > .toolbar { + height: 22px; + margin: 10px 2px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat { + color: inherit; + margin-top: 6px; +} + +/* body */ + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .body { display: flex; } -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content { +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .body .content { display: flex; box-sizing: border-box; outline: 1px solid var(--vscode-inlineChatInput-border); @@ -27,11 +39,11 @@ border-radius: 2px; } -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content.synthetic-focus { +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .body .content.synthetic-focus { outline: 1px solid var(--vscode-inlineChatInput-focusBorder); } -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content .input { +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .body .content .input { display: flex; align-items: center; justify-content: space-between; @@ -40,11 +52,11 @@ cursor: text; } -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content .input .monaco-editor-background { +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .body .content .input .monaco-editor-background { background-color: var(--vscode-inlineChatInput-background); } -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content .input .editor-placeholder { +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .body .content .input .editor-placeholder { position: absolute; z-index: 1; color: var(--vscode-inlineChatInput-placeholderForeground); @@ -53,14 +65,14 @@ text-overflow: ellipsis; } -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content .input .editor-placeholder.hidden { +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .body .content .input .editor-placeholder.hidden { display: none; } -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .content .input .editor-container { +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .body .content .input .editor-container { vertical-align: middle; } -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .toolbar { +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .body .toolbar { display: flex; flex-direction: column; align-self: stretch; @@ -70,34 +82,276 @@ background: var(--vscode-inlineChatInput-background); } -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .body .toolbar .actions-container { +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .body .toolbar .actions-container { display: flex; flex-direction: row; gap: 4px; } -/* progress */ +/* progress bit */ -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .progress { +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .progress { position: relative; + width: calc(100% - 18px); + left: 19px; } -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .progress .monaco-progress-container { +/* UGLY - fighting against workbench styles */ +.monaco-workbench .part.editor>.content .monaco-editor .inline-chat .progress .monaco-progress-container { top: 0; } /* status */ -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .status { - height: 22px; - margin: 4px; +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status { + margin-top: 4px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status.actions { + margin-top: 4px; } +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .actions.hidden { + display: none; +} -.monaco-workbench .notebookOverlay .cell-chat-part .cell-chat-container .status span { +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .label { overflow: hidden; color: var(--vscode-descriptionForeground); font-size: 11px; align-self: baseline; display: flex; } + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .label.hidden { + display: none; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .label.info { + margin-right: auto; + padding-left: 2px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .label.info > .codicon { + padding: 0 5px; + font-size: 12px; + line-height: 18px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .label.status { + padding-left: 10px; + padding-right: 4px; + margin-left: auto; + align-self: center; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .label .slash-command-pill CODE { + border-radius: 3px; + padding: 0 1px; + background-color: var(--vscode-chat-slashCommandBackground); + color: var(--vscode-chat-slashCommandForeground); +} + + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage { + padding: 10px 5px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage.hidden { + display: none; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage .message * { + margin: unset; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage .message code { + font-family: var(--monaco-monospace-font); + font-size: 12px; + color: var(--vscode-textPreformat-foreground); + background-color: var(--vscode-textPreformat-background); + padding: 1px 3px; + border-radius: 4px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage .message .interactive-result-code-block { + margin: 16px 0; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage .message { + -webkit-line-clamp: initial; + -webkit-box-orient: vertical; + overflow: hidden; + display: -webkit-box; + -webkit-user-select: text; + user-select: text; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage .message[state="cropped"] { + -webkit-line-clamp: var(--vscode-inline-chat-cropped, 3); +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage .message[state="expanded"] { + -webkit-line-clamp: var(--vscode-inline-chat-expanded, 10); +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .label A { + color: var(--vscode-textLink-foreground); + cursor: pointer; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .label.error { + color: var(--vscode-errorForeground); +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .label.warn { + color: var(--vscode-editorWarning-foreground); +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .actions { + display: flex; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .actions > .monaco-button, +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .actions > .monaco-button-dropdown { + margin-right: 6px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { + display: flex; + align-items: center; + padding: 0 4px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .actions > .monaco-button.codicon { + display: flex; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .actions > .monaco-button.codicon::before { + align-self: center; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .actions .monaco-text-button { + padding: 2px 4px; + white-space: nowrap; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .monaco-toolbar .action-item { + padding: 0 2px; +} + +/* TODO@jrieken not needed? */ +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .monaco-toolbar .action-label.checked { + color: var(--vscode-inputOption-activeForeground); + background-color: var(--vscode-inputOption-activeBackground); + outline: 1px solid var(--vscode-inputOption-activeBorder); +} + + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .status .monaco-toolbar .action-item.button-item .action-label:is(:hover, :focus) { + background-color: var(--vscode-button-hoverBackground); +} + +/* preview */ + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .preview { + display: none; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .previewDiff { + display: inherit; + padding: 6px; + border: 1px solid var(--vscode-inlineChat-border); + border-top: none; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + margin: 0 2px 6px 2px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .previewCreateTitle { + padding-top: 6px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .previewCreate { + display: inherit; + padding: 6px; + border: 1px solid var(--vscode-inlineChat-border); + border-radius: 2px; + margin: 0 2px 6px 2px; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .previewDiff.hidden, +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .previewCreate.hidden, +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .previewCreateTitle.hidden { + display: none; +} + +/* decoration styles */ + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat-lines-deleted-range-inline { + text-decoration: line-through; + background-color: var(--vscode-diffEditor-removedTextBackground); + opacity: 0.6; +} +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat-lines-inserted-range { + background-color: var(--vscode-diffEditor-insertedTextBackground); +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat-block-selection { + background-color: var(--vscode-inlineChat-regionHighlight); +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat-slash-command { + opacity: 0; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat-slash-command-detail { + opacity: 0.5; +} + +/* diff zone */ + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat-diff-widget .monaco-diff-editor .monaco-editor-background, +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat-diff-widget .monaco-diff-editor .monaco-editor .margin-view-overlays { + background-color: var(--vscode-inlineChat-regionHighlight); +} + +/* create zone */ + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat-newfile-widget { + padding: 3px 0 6px 0; + background-color: var(--vscode-inlineChat-regionHighlight); +} + +.monaco-workbench .notebookOverlay .cell-chat-part .inline-chat-newfile-widget .title { + display: flex; + align-items: center; + justify-content: space-between; + padding: 3px 6px 3px 0; +} + +/* gutter decoration */ + +.monaco-workbench .notebookOverlay .cell-chat-part .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque, +.monaco-workbench .notebookOverlay .cell-chat-part .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { + display: block; + cursor: pointer; + transition: opacity .2s ease-in-out; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque { + opacity: 0.5; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { + opacity: 0; +} + +.monaco-workbench .notebookOverlay .cell-chat-part .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque:hover, +.monaco-workbench .notebookOverlay .cell-chat-part .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { + opacity: 1; +} + diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions.ts new file mode 100644 index 0000000000000..b09b3069da8f7 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions.ts @@ -0,0 +1,221 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { localize } from 'vs/nls'; +import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } 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_RESPONSE_TYPES, InlineChateResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { INotebookCellActionContext, NotebookAction, NotebookCellAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { insertNewCell } from 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; +import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS, MENU_CELL_CHAT_WIDGET_TOOLBAR, NotebookCellChatController } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; +import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; + + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.chat.accept', + title: { + value: localize('notebook.cell.chat.accept', "Make Request"), + original: 'Make Request' + }, + icon: Codicon.send, + keybinding: { + when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED), + weight: KeybindingWeight.EditorCore + 7, + primary: KeyCode.Enter + }, + menu: { + id: MENU_CELL_CHAT_WIDGET, + group: 'main', + order: 1, + when: CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.negate() + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const ctrl = NotebookCellChatController.get(context.cell); + if (!ctrl) { + return; + } + + ctrl.acceptInput(); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.chat.stop', + title: { + value: localize('notebook.cell.chat.stop', "Stop Request"), + original: 'Make Request' + }, + icon: Codicon.debugStop, + menu: { + id: MENU_CELL_CHAT_WIDGET, + group: 'main', + order: 1, + when: CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const ctrl = NotebookCellChatController.get(context.cell); + if (!ctrl) { + return; + } + + ctrl.cancelCurrentRequest(false); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.chat.close', + title: { + value: localize('notebook.cell.chat.close', "Close Chat"), + original: 'Close Chat' + }, + icon: Codicon.close, + menu: { + id: MENU_CELL_CHAT_WIDGET_TOOLBAR, + group: 'main', + order: 2 + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const ctrl = NotebookCellChatController.get(context.cell); + if (!ctrl) { + return; + } + + ctrl.dismiss(false); + } +}); + +registerAction2(class extends NotebookAction { + constructor() { + super( + { + id: 'notebook.cell.chat.acceptChanges', + title: { value: localize('apply1', 'Accept Changes'), original: 'Accept Changes' }, + shortTitle: localize('apply2', 'Accept'), + icon: Codicon.check, + tooltip: localize('apply1', 'Accept Changes'), + keybinding: { + when: ContextKeyExpr.and(CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_INLINE_CHAT_FOCUSED), + weight: KeybindingWeight.EditorContrib + 10, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + }, + menu: [ + { + id: MENU_CELL_CHAT_WIDGET_STATUS, + group: 'inline', + order: 0, + when: CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChateResponseTypes.OnlyMessages), + } + ] + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const ctrl = NotebookCellChatController.get(context.cell); + if (!ctrl) { + return; + } + + ctrl.acceptSession(); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'inlineChat.discard', + title: localize('discard', 'Discard'), + icon: Codicon.discard, + menu: { + id: MENU_CELL_CHAT_WIDGET_STATUS, + group: 'main', + order: 1 + } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const ctrl = NotebookCellChatController.get(context.cell); + if (!ctrl) { + return; + } + + // todo discard + ctrl.dismiss(true); + } +}); + +registerAction2(class extends NotebookCellAction { + constructor() { + super( + { + id: 'notebook.cell.insertCodeCellWithChat', + title: { + value: '$(sparkle) ' + localize('notebookActions.menu.insertCodeCellWithChat', "Generate"), + original: '$(sparkle) Generate', + }, + tooltip: localize('notebookActions.menu.insertCodeCellWithChat.tooltip', "Generate Code Cell with Chat"), + menu: [ + { + id: MenuId.NotebookCellBetween, + group: 'inline', + order: -1, + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + CTX_INLINE_CHAT_HAS_PROVIDER, + ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true) + ) + }, + { + id: MenuId.NotebookCellListTop, + group: 'inline', + order: -1, + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + CTX_INLINE_CHAT_HAS_PROVIDER, + ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true) + ) + }, + ] + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { + const newCell = await insertNewCell(accessor, context, CellKind.Code, 'below', true); + + if (!newCell) { + return; + } + await context.notebookEditor.focusNotebookCell(newCell, 'container'); + const ctrl = NotebookCellChatController.get(newCell); + if (!ctrl) { + return; + } + ctrl.show(); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts index a3f686b982325..e0426c4c66310 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Dimension, h } from 'vs/base/browser/dom'; import { CancelablePromise, Queue, createCancelablePromise, raceCancellationError } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Codicon } from 'vs/base/common/codicons'; import { Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { MovingAverage } from 'vs/base/common/numbers'; import { StopWatch } from 'vs/base/common/stopwatch'; @@ -18,33 +17,33 @@ import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; 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 { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { TextEdit } from 'vs/editor/common/languages'; import { ICursorStateComputer } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { localize } from 'vs/nls'; -import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; 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 { IInlineChatSessionService, ReplyResponse, Session, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { ProgressingEditsOptions, asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; -import { CTX_INLINE_CHAT_HAS_PROVIDER, EditMode, IInlineChatProgressItem, IInlineChatRequest } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { CELL_TITLE_CELL_GROUP_ID, INotebookCellActionContext, NotebookCellAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { insertNewCell } from 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; +import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { CTX_INLINE_CHAT_VISIBLE, EditMode, IInlineChatProgressItem, IInlineChatRequest } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CellChatWidget, MENU_NOTEBOOK_CELL_CHAT_WIDGET } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget'; -import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -const CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey('notebookChatHasActiveRequest', false, localize('notebookChatHasActiveRequest', "Whether the cell chat editor has an active request")); +export const CTX_NOTEBOOK_CELL_CHAT_FOCUSED = new RawContextKey('notebookCellChatFocused', false, localize('notebookCellChatFocused', "Whether the cell chat editor is focused")); +export const CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey('notebookChatHasActiveRequest', false, localize('notebookChatHasActiveRequest', "Whether the cell chat editor has an active request")); +export const MENU_CELL_CHAT_WIDGET = MenuId.for('cellChatWidget'); +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; - getWidget(): CellChatWidget; } export class NotebookCellChatController extends Disposable { @@ -58,23 +57,30 @@ export class NotebookCellChatController extends Disposable { private _activeSession?: Session; private readonly _ctxHasActiveRequest: IContextKey; private _isVisible: boolean = false; - private _strategy: EditStrategy = new EditStrategy(); + private _strategy: EditStrategy | undefined; private _inlineChatListener: IDisposable | undefined; - + private _widget: InlineChatWidget | undefined; + private readonly _toolbarDOM = h('div.toolbar@editorToolbar'); + private _toolbar: MenuWorkbenchToolBar | undefined; + private readonly _ctxVisible: IContextKey; + private readonly _ctxCellWidgetFocused: IContextKey; 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 _instaService: IInstantiationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { 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._register(this._cell.onDidChangeEditorAttachState(() => { const editor = this._getCellEditor(); @@ -84,10 +90,26 @@ export class NotebookCellChatController extends Disposable { return; } + if (!this._widget) { + this._widget = this._instantiationService.createInstance(InlineChatWidget, editor, { + menuId: MENU_CELL_CHAT_WIDGET, + statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, + feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK + }); + + this._partContainer.appendChild(this._widget.domNode); + this._partContainer.appendChild(this._toolbarDOM.editorToolbar); + + this._toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._toolbarDOM.editorToolbar, MENU_CELL_CHAT_WIDGET_TOOLBAR, { + telemetrySource: 'interactiveEditorWidget-toolbar', + toolbarOptions: { primaryGroup: 'main' } + })); + } + const inlineChatController = InlineChatController.get(editor); if (inlineChatController) { this._inlineChatListener = inlineChatController.onWillStartSession(() => { - this.dismiss(); + this.dismiss(false); }); } })); @@ -96,27 +118,44 @@ export class NotebookCellChatController extends Disposable { public override dispose(): void { if (this._isVisible) { // detach the chat widget - this._chatPart.getWidget().hide(); + this._widget?.reset(); this._sessionCtor?.cancel(); this._sessionCtor = undefined; } + if (this._widget) { + this._partContainer.removeChild(this._widget.domNode); + } + + this._partContainer.removeChild(this._toolbarDOM.editorToolbar); + this._inlineChatListener?.dispose(); + this._toolbar?.dispose(); this._inlineChatListener = undefined; this._ctxHasActiveRequest.reset(); + this._ctxVisible.reset(); NotebookCellChatController._cellChatControllers.delete(this._cell); super.dispose(); } layout() { - if (this._isVisible) { - this._chatPart.getWidget().layout(); + if (this._isVisible && this._widget) { + const innerEditorWidth = this._cell.layoutInfo.editorWidth; + const height = 82 + 8 * 2 /* vertical margin*/; + + this._widget.layout(new Dimension(innerEditorWidth, height)); } } async show() { this._isVisible = true; - this._chatPart.getWidget().show(this._cell); + 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._cell.chatHeight = 82 + 8 * 2 /* vertical margin*/; + this._sessionCtor = createCancelablePromise(async token => { if (this._cell.editorAttached) { const editor = this._getCellEditor(); @@ -135,7 +174,11 @@ export class NotebookCellChatController extends Disposable { } } - this._chatPart.getWidget().placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question"); + 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(); + } }); } @@ -154,20 +197,24 @@ export class NotebookCellChatController extends Disposable { this._inlineChatSessionService.releaseSession(this._activeSession); } - - const session = await this._inlineChatSessionService.createSession( editor, { editMode: EditMode.LivePreview }, token ); + if (!session) { + return; + } + this._activeSession = session; + this._strategy = new EditStrategy(session); } async acceptInput() { assertType(this._activeSession); - this._activeSession.addInput(new SessionPrompt(this._getInput())); + assertType(this._widget); + this._activeSession.addInput(new SessionPrompt(this._widget.value)); assertType(this._activeSession.lastInput); @@ -180,7 +227,7 @@ export class NotebookCellChatController extends Disposable { const editor = editors[1]; this._ctxHasActiveRequest.set(true); - this._chatPart.getWidget().updateProgress(true); + this._widget?.updateProgress(true); const request: IInlineChatRequest = { requestId: generateUuid(), @@ -204,6 +251,11 @@ export class NotebookCellChatController extends Disposable { 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'); @@ -235,20 +287,26 @@ export class NotebookCellChatController extends Disposable { if (!reply) { this._ctxHasActiveRequest.set(false); - this._chatPart.getWidget().updateProgress(false); + this._widget?.updateProgress(false); return; } const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); - const replyResponse = this._instaService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits); + const replyResponse = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits); for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) { await this._makeChanges(editor, replyResponse.allLocalEdits[i], undefined); } this._ctxHasActiveRequest.set(false); - this._chatPart.getWidget().updateProgress(false); + this._widget?.updateProgress(false); + this._widget?.updateInfo(''); + this._widget?.updateToolbar(true); } - async cancelCurrentRequest() { + async cancelCurrentRequest(discard: boolean) { + if (discard) { + this._strategy?.cancel(); + } + if (this._activeSession) { this._inlineChatSessionService.releaseSession(this._activeSession); } @@ -256,18 +314,34 @@ export class NotebookCellChatController extends Disposable { this._activeSession = undefined; } - async dismiss() { - this._isVisible = false; - this.cancelCurrentRequest(); - this._chatPart.getWidget().hide(); + 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); } - private _getInput() { - return this._chatPart.getWidget().getInput(); + async dismiss(discard: boolean) { + this._isVisible = false; + this._partContainer.style.display = 'none'; + this.cancelCurrentRequest(discard); + this._ctxCellWidgetFocused.set(false); + this._ctxVisible.set(false); + this._widget?.reset(); + this._cell.chatHeight = 0; } 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); // this._log('edits from PROVIDER and after making them MORE MINIMAL', this._activeSession.provider.debugName, edits, moreMinimalEdits); @@ -298,6 +372,12 @@ export class NotebookCellChatController extends Disposable { class EditStrategy { private _editCount: number = 0; + constructor( + protected readonly _session: Session, + ) { + + } + async makeProgressiveChanges(editor: IActiveCodeEditor, edits: ISingleEditOperation[], opts: ProgressingEditsOptions): Promise { // push undo stop before first edit if (++this._editCount === 1) { @@ -329,174 +409,29 @@ class EditStrategy { } editor.executeEdits('inline-chat-live', edits, cursorStateComputerAndInlineDiffCollection); } -} - -registerAction2(class extends NotebookCellAction { - constructor() { - super( - { - id: 'notebook.cell.chat.start', - title: { - value: localize('notebook.cell.chat.start', "Start Chat"), - original: 'Start Chat' - }, - icon: Codicon.sparkle, - menu: { - id: MenuId.NotebookCellTitle, - group: CELL_TITLE_CELL_GROUP_ID, - order: 0, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_PROVIDER, EditorContextKeys.writable, ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true)) - } - }); - } - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { - return; + async apply(editor: IActiveCodeEditor) { + if (this._editCount > 0) { + editor.pushUndoStop(); } - - await ctrl.show(); - } -}); - -registerAction2(class extends NotebookCellAction { - constructor() { - super( - { - id: 'notebook.cell.chat.accept', - title: { - value: localize('notebook.cell.chat.accept', "Make Request"), - original: 'Make Request' - }, - icon: Codicon.send, - keybinding: { - when: CTX_NOTEBOOK_CELL_CHAT_FOCUSED, - weight: KeybindingWeight.EditorCore + 7, - primary: KeyCode.Enter - }, - menu: { - id: MENU_NOTEBOOK_CELL_CHAT_WIDGET, - group: 'main', - order: 1, - when: CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.negate() - } - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { + if (!(this._session.lastExchange?.response instanceof ReplyResponse)) { return; } - - ctrl.acceptInput(); - } -}); - -registerAction2(class extends NotebookCellAction { - constructor() { - super( - { - id: 'notebook.cell.chat.stop', - title: { - value: localize('notebook.cell.chat.stop', "Stop Request"), - original: 'Make Request' - }, - icon: Codicon.debugStop, - menu: { - id: MENU_NOTEBOOK_CELL_CHAT_WIDGET, - group: 'main', - order: 1, - when: CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST - } - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { - return; + const { untitledTextModel } = this._session.lastExchange.response; + if (untitledTextModel && !untitledTextModel.isDisposed() && untitledTextModel.isDirty()) { + await untitledTextModel.save({ reason: SaveReason.EXPLICIT }); } - - ctrl.cancelCurrentRequest(); - } -}); - -registerAction2(class extends NotebookCellAction { - constructor() { - super( - { - id: 'notebook.cell.chat.close', - title: { - value: localize('notebook.cell.chat.close', "Close Chat"), - original: 'Close Chat' - }, - icon: Codicon.close, - menu: { - id: MENU_NOTEBOOK_CELL_CHAT_WIDGET, - group: 'main', - order: 2 - } - }); } - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const ctrl = NotebookCellChatController.get(context.cell); - if (!ctrl) { + async cancel() { + const { textModelN: modelN, textModelNAltVersion, textModelNSnapshotAltVersion } = this._session; + if (modelN.isDisposed()) { return; } - ctrl.dismiss(); - } -}); - -registerAction2(class extends NotebookCellAction { - constructor() { - super( - { - id: 'notebook.cell.insertCodeCellWithChat', - title: { - value: '$(sparkle) ' + localize('notebookActions.menu.insertCodeCellWithChat', "Generate"), - original: '$(sparkle) Generate', - }, - tooltip: localize('notebookActions.menu.insertCodeCellWithChat.tooltip', "Generate Code Cell with Chat"), - menu: [ - { - id: MenuId.NotebookCellBetween, - group: 'inline', - order: -1, - when: ContextKeyExpr.and( - NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), - CTX_INLINE_CHAT_HAS_PROVIDER, - ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true) - ) - }, - { - id: MenuId.NotebookCellListTop, - group: 'inline', - order: -1, - when: ContextKeyExpr.and( - NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), - CTX_INLINE_CHAT_HAS_PROVIDER, - ContextKeyExpr.equals(`config.${NotebookSetting.cellChat}`, true) - ) - }, - ] - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { - const newCell = await insertNewCell(accessor, context, CellKind.Code, 'below', true); - - if (!newCell) { - return; + const targetAltVersion = textModelNSnapshotAltVersion ?? textModelNAltVersion; + while (targetAltVersion < modelN.getAlternativeVersionId() && modelN.canUndo()) { + modelN.undo(); } - await context.notebookEditor.focusNotebookCell(newCell, 'container'); - const ctrl = NotebookCellChatController.get(newCell); - if (!ctrl) { - return; - } - ctrl.show(); } -}); +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts index a1bd2d808b04a..7bd5d43e88a67 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatPart.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Lazy } from 'vs/base/common/lazy'; 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 { CellChatWidget } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget'; + +import 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatActions'; export class CellChatPart extends CellContentPart { private _controller: NotebookCellChatController | undefined; @@ -17,25 +17,17 @@ export class CellChatPart extends CellContentPart { return this.currentCell; } - private _widget: Lazy; - constructor( private readonly _notebookEditor: INotebookEditorDelegate, - partContainer: HTMLElement, + private readonly _partContainer: HTMLElement, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); - - this._widget = new Lazy(() => this._instantiationService.createInstance(CellChatWidget, this._notebookEditor, partContainer)); - } - - getWidget() { - return this._widget.value; } override didRenderCell(element: ICellViewModel): void { this._controller?.dispose(); - this._controller = this._instantiationService.createInstance(NotebookCellChatController, this._notebookEditor, this, element); + this._controller = this._instantiationService.createInstance(NotebookCellChatController, this._notebookEditor, this, element, this._partContainer); super.didRenderCell(element); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget.ts deleted file mode 100644 index b0bf8c662d8d7..0000000000000 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatWidget.ts +++ /dev/null @@ -1,174 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { $, Dimension, addDisposableListener, append, getTotalWidth, h } from 'vs/base/browser/dom'; -import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { MarshalledId } from 'vs/base/common/marshallingIds'; -import { URI } from 'vs/base/common/uri'; -import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ITextModel } from 'vs/editor/common/model'; -import { IModelService } from 'vs/editor/common/services/model'; -import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; -import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; -import { localize } from 'vs/nls'; -import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; -import { MenuId } from 'vs/platform/actions/common/actions'; -import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { _inputEditorOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; -import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { CellFocusMode, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; - -export const CTX_NOTEBOOK_CELL_CHAT_FOCUSED = new RawContextKey('notebookCellChatFocused', false, localize('notebookCellChatFocused', "Whether the cell chat editor is focused")); -export const MENU_NOTEBOOK_CELL_CHAT_WIDGET = MenuId.for('notebookCellChatWidget'); - -export class CellChatWidget extends Disposable { - private static _modelPool: number = 1; - - private readonly _elements = h( - 'div.cell-chat-container@root', - [ - h('div.body', [ - h('div.content@content', [ - h('div.input@input', [ - h('div.editor-placeholder@placeholder'), - h('div.editor-container@editor'), - ]), - h('div.toolbar@editorToolbar'), - ]), - ]), - h('div.progress@progress'), - h('div.status@status') - ] - ); - private readonly _progressBar: ProgressBar; - private readonly _toolbar: MenuWorkbenchToolBar; - - private readonly _inputEditor: IActiveCodeEditor; - private readonly _inputModel: ITextModel; - private readonly _ctxInputEditorFocused: IContextKey; - - private _activeCell: ICellViewModel | undefined; - - set placeholder(value: string) { - this._elements.placeholder.innerText = value; - } - - - constructor( - private readonly _notebookEditor: INotebookEditorDelegate, - private readonly _partContainer: HTMLElement, - @IModelService private readonly _modelService: IModelService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService - ) { - super(); - append(_partContainer, this._elements.root); - this._elements.input.style.height = '24px'; - - const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - isSimpleWidget: true, - contributions: EditorExtensionsRegistry.getSomeEditorContributions([ - SnippetController2.ID, - SuggestController.ID - ]) - }; - - this._inputEditor = this._instantiationService.createInstance(CodeEditorWidget, this._elements.editor, { - ..._inputEditorOptions, - ariaLabel: localize('cell-chat-aria-label', "Cell Chat Input"), - }, codeEditorWidgetOptions); - this._register(this._inputEditor); - const uri = URI.from({ scheme: 'vscode', authority: 'inline-chat', path: `/notebook-cell-chat/model${CellChatWidget._modelPool++}.txt` }); - this._inputModel = this._register(this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri)); - this._inputEditor.setModel(this._inputModel); - - // placeholder - this._elements.placeholder.style.fontSize = `${this._inputEditor.getOption(EditorOption.fontSize)}px`; - this._elements.placeholder.style.lineHeight = `${this._inputEditor.getOption(EditorOption.lineHeight)}px`; - this._register(addDisposableListener(this._elements.placeholder, 'click', () => this._inputEditor.focus())); - - const togglePlaceholder = () => { - const hasText = this._inputModel.getValueLength() > 0; - this._elements.placeholder.classList.toggle('hidden', hasText); - }; - this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder)); - togglePlaceholder(); - - // toolbar - this._toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, MENU_NOTEBOOK_CELL_CHAT_WIDGET, { - telemetrySource: 'interactiveEditorWidget-toolbar', - toolbarOptions: { primaryGroup: 'main' } - })); - - // Create chat response div - const copilotGeneratedCodeSpan = $('span.copilot-generated-code', {}, 'Copilot generated code may be incorrect'); - this._elements.status.appendChild(copilotGeneratedCodeSpan); - - this._register(this._inputEditor.onDidFocusEditorWidget(() => { - if (this._activeCell) { - this._activeCell.focusMode = CellFocusMode.ChatInput; - } - })); - - this._ctxInputEditorFocused = CTX_NOTEBOOK_CELL_CHAT_FOCUSED.bindTo(this._contextKeyService); - this._register(this._inputEditor.onDidFocusEditorWidget(() => { - this._ctxInputEditorFocused.set(true); - })); - this._register(this._inputEditor.onDidBlurEditorWidget(() => { - this._ctxInputEditorFocused.set(false); - })); - - this._progressBar = new ProgressBar(this._elements.progress); - this._register(this._progressBar); - } - - show(element: ICellViewModel) { - this._partContainer.style.display = 'block'; - - this._activeCell = element; - - this._toolbar.context = { - ui: true, - cell: element, - notebookEditor: this._notebookEditor, - $mid: MarshalledId.NotebookCellActionContext - }; - - this.layout(); - this._inputEditor.focus(); - this._activeCell.chatHeight = 82 + 8 /* bottom margin*/; - } - - hide() { - this._partContainer.style.display = 'none'; - if (this._activeCell) { - this._activeCell.chatHeight = 0; - } - } - - getInput() { - return this._inputEditor.getValue(); - } - - updateProgress(show: boolean) { - if (show) { - this._progressBar.infinite(); - } else { - this._progressBar.stop(); - } - } - - layout() { - if (this._activeCell) { - const innerEditorWidth = this._activeCell.layoutInfo.editorWidth - (getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */); - this._inputEditor.layout(new Dimension(innerEditorWidth, this._inputEditor.getContentHeight())); - } - } -}