mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-10 09:31:57 -05:00
Merge pull request #308797 from microsoft/joh/private-fields-async-fix
build: fix async private method token fusion; inlineChat: adopt native private fields
This commit is contained in:
@@ -115,7 +115,7 @@ export function convertPrivateFields(code: string, filename: string): ConvertPri
|
||||
lastEnd = edit.end;
|
||||
}
|
||||
parts.push(code.substring(lastEnd));
|
||||
return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: edits.length, elapsed: Date.now() - t1, edits };
|
||||
return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: edits.length, elapsed: Date.now() - t1, edits };
|
||||
|
||||
// --- AST walking ---
|
||||
|
||||
@@ -209,10 +209,15 @@ return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: ed
|
||||
if (ts.isPrivateIdentifier(child)) {
|
||||
const resolved = resolvePrivateName(child.text);
|
||||
if (resolved !== undefined) {
|
||||
const start = child.getStart(sourceFile);
|
||||
edits.push({
|
||||
start: child.getStart(sourceFile),
|
||||
start,
|
||||
end: child.getEnd(),
|
||||
newText: resolved
|
||||
// In minified code, `async#run()` has no space before `#`.
|
||||
// The `#` naturally starts a new token, but `$` does not —
|
||||
// `async$a` would fuse into one identifier. Insert a space
|
||||
// when the preceding character is an identifier character.
|
||||
newText: (start > 0 && isIdentifierChar(code.charCodeAt(start - 1))) ? ' ' + resolved : resolved
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -234,6 +239,11 @@ return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: ed
|
||||
}
|
||||
}
|
||||
|
||||
function isIdentifierChar(ch: number): boolean {
|
||||
// a-z, A-Z, 0-9, _, $
|
||||
return (ch >= 97 && ch <= 122) || (ch >= 65 && ch <= 90) || (ch >= 48 && ch <= 57) || ch === 95 || ch === 36;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts a source map to account for text edits applied to the generated JS.
|
||||
*
|
||||
|
||||
@@ -292,6 +292,54 @@ suite('convertPrivateFields', () => {
|
||||
assert.deepStrictEqual(result.edits, []);
|
||||
});
|
||||
|
||||
test('async private method — replacement must not merge with async keyword', async () => {
|
||||
// In minified output, there is no space between `async` and `#method`:
|
||||
// class Foo{async#run(){await Promise.resolve(1)}}
|
||||
// Replacing `#run` with `$a` naively produces `async$a()` which is a
|
||||
// single identifier, not `async $a()`. The `await` inside then becomes
|
||||
// invalid because the method is no longer async.
|
||||
const code = 'class Foo{async#run(){return await Promise.resolve(1)}call(){return this.#run()}}';
|
||||
const result = convertPrivateFields(code, 'test.js');
|
||||
assert.ok(!result.code.includes('#run'), 'should replace #run');
|
||||
// The replacement must NOT fuse with `async` into a single token
|
||||
assert.doesNotThrow(() => new Function(result.code), 'transformed code must be valid JS');
|
||||
// Verify it actually executes (the async method should still work)
|
||||
const exec = new Function(`
|
||||
${result.code}
|
||||
return new Foo().call();
|
||||
`);
|
||||
const val = await exec();
|
||||
assert.strictEqual(val, 1);
|
||||
});
|
||||
|
||||
test('async private method — space inserted in declaration and not in usage', () => {
|
||||
// More readable version: ensure that `async #method()` becomes
|
||||
// `async $a()` (with space), while `this.#method()` becomes
|
||||
// `this.$a()` (no extra space needed since `.` separates tokens).
|
||||
const code = [
|
||||
'class Foo {',
|
||||
' async #doWork() { return await 42; }',
|
||||
' run() { return this.#doWork(); }',
|
||||
'}',
|
||||
].join('\n');
|
||||
const result = convertPrivateFields(code, 'test.js');
|
||||
assert.ok(!result.code.includes('#doWork'), 'should replace #doWork');
|
||||
assert.doesNotThrow(() => new Function(result.code), 'transformed code must be valid JS');
|
||||
});
|
||||
|
||||
test('static async private method — no token fusion', async () => {
|
||||
const code = 'class Foo{static async#init(){return await Promise.resolve(1)}static go(){return Foo.#init()}}';
|
||||
const result = convertPrivateFields(code, 'test.js');
|
||||
assert.doesNotThrow(() => new Function(result.code),
|
||||
'static async private method must produce valid JS, got:\n' + result.code);
|
||||
const exec = new Function(`
|
||||
${result.code}
|
||||
return Foo.go();
|
||||
`);
|
||||
const value = await exec();
|
||||
assert.strictEqual(value, 1);
|
||||
});
|
||||
|
||||
test('heritage clause — extends expression resolves outer private field, not inner', () => {
|
||||
const code = [
|
||||
'class Outer {',
|
||||
|
||||
@@ -112,64 +112,94 @@ export class InlineChatController implements IEditorContribution {
|
||||
* Stores the user's explicitly chosen model (qualified name) from a previous inline chat request in the same session.
|
||||
* When set, this takes priority over the inlineChat.defaultModel setting.
|
||||
*/
|
||||
private static _userSelectedModel: string | undefined;
|
||||
static #userSelectedModel: string | undefined;
|
||||
|
||||
private readonly _store = new DisposableStore();
|
||||
private readonly _isActiveController = observableValue(this, false);
|
||||
private readonly _renderMode: IObservable<'zone' | 'hover'>;
|
||||
private readonly _zone: Lazy<InlineChatZoneWidget>;
|
||||
readonly #store = new DisposableStore();
|
||||
readonly #isActiveController = observableValue(this, false);
|
||||
readonly #renderMode: IObservable<'zone' | 'hover'>;
|
||||
readonly #zone: Lazy<InlineChatZoneWidget>;
|
||||
readonly inputOverlayWidget: InlineChatAffordance;
|
||||
private readonly _inputWidget: InlineChatInputWidget;
|
||||
readonly #inputWidget: InlineChatInputWidget;
|
||||
|
||||
private readonly _currentSession: IObservable<IInlineChatSession2 | undefined>;
|
||||
readonly #currentSession: IObservable<IInlineChatSession2 | undefined>;
|
||||
|
||||
readonly #editor: ICodeEditor;
|
||||
readonly #instaService: IInstantiationService;
|
||||
readonly #notebookEditorService: INotebookEditorService;
|
||||
readonly #inlineChatSessionService: IInlineChatSessionService;
|
||||
readonly #configurationService: IConfigurationService;
|
||||
readonly #webContentExtractorService: ISharedWebContentExtractorService;
|
||||
readonly #fileService: IFileService;
|
||||
readonly #chatAttachmentResolveService: IChatAttachmentResolveService;
|
||||
readonly #editorService: IEditorService;
|
||||
readonly #markerDecorationsService: IMarkerDecorationsService;
|
||||
readonly #languageModelService: ILanguageModelsService;
|
||||
readonly #logService: ILogService;
|
||||
readonly #chatEditingService: IChatEditingService;
|
||||
readonly #chatService: IChatService;
|
||||
|
||||
get widget(): EditorBasedInlineChatWidget {
|
||||
return this._zone.value.widget;
|
||||
return this.#zone.value.widget;
|
||||
}
|
||||
|
||||
get isActive() {
|
||||
return Boolean(this._currentSession.get());
|
||||
return Boolean(this.#currentSession.get());
|
||||
}
|
||||
|
||||
get inputWidget(): InlineChatInputWidget {
|
||||
return this._inputWidget;
|
||||
return this.#inputWidget;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
@INotebookEditorService private readonly _notebookEditorService: INotebookEditorService,
|
||||
@IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService,
|
||||
editor: ICodeEditor,
|
||||
@IInstantiationService instaService: IInstantiationService,
|
||||
@INotebookEditorService notebookEditorService: INotebookEditorService,
|
||||
@IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService,
|
||||
@ICodeEditorService codeEditorService: ICodeEditorService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService,
|
||||
@ILanguageModelsService private readonly _languageModelService: ILanguageModelsService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IChatEditingService private readonly _chatEditingService: IChatEditingService,
|
||||
@IChatService private readonly _chatService: IChatService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@ISharedWebContentExtractorService webContentExtractorService: ISharedWebContentExtractorService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IChatAttachmentResolveService chatAttachmentResolveService: IChatAttachmentResolveService,
|
||||
@IEditorService editorService: IEditorService,
|
||||
@IMarkerDecorationsService markerDecorationsService: IMarkerDecorationsService,
|
||||
@ILanguageModelsService languageModelService: ILanguageModelsService,
|
||||
@ILogService logService: ILogService,
|
||||
@IChatEditingService chatEditingService: IChatEditingService,
|
||||
@IChatService chatService: IChatService,
|
||||
) {
|
||||
const editorObs = observableCodeEditor(_editor);
|
||||
this.#editor = editor;
|
||||
this.#instaService = instaService;
|
||||
this.#notebookEditorService = notebookEditorService;
|
||||
this.#inlineChatSessionService = inlineChatSessionService;
|
||||
this.#configurationService = configurationService;
|
||||
this.#webContentExtractorService = webContentExtractorService;
|
||||
this.#fileService = fileService;
|
||||
this.#chatAttachmentResolveService = chatAttachmentResolveService;
|
||||
this.#editorService = editorService;
|
||||
this.#markerDecorationsService = markerDecorationsService;
|
||||
this.#languageModelService = languageModelService;
|
||||
this.#logService = logService;
|
||||
this.#chatEditingService = chatEditingService;
|
||||
this.#chatService = chatService;
|
||||
|
||||
const editorObs = observableCodeEditor(editor);
|
||||
|
||||
const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService);
|
||||
const ctxFileBelongsToChat = CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.bindTo(contextKeyService);
|
||||
const ctxPendingConfirmation = CTX_INLINE_CHAT_PENDING_CONFIRMATION.bindTo(contextKeyService);
|
||||
const ctxTerminated = CTX_INLINE_CHAT_TERMINATED.bindTo(contextKeyService);
|
||||
const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService);
|
||||
this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService);
|
||||
const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this.#configurationService);
|
||||
this.#renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this.#configurationService);
|
||||
|
||||
// Track whether the current editor's file is being edited by any chat editing session
|
||||
this._store.add(autorun(r => {
|
||||
this.#store.add(autorun(r => {
|
||||
const model = editorObs.model.read(r);
|
||||
if (!model) {
|
||||
ctxFileBelongsToChat.set(false);
|
||||
return;
|
||||
}
|
||||
const sessions = this._chatEditingService.editingSessionsObs.read(r);
|
||||
const sessions = this.#chatEditingService.editingSessionsObs.read(r);
|
||||
let hasEdits = false;
|
||||
for (const session of sessions) {
|
||||
const entries = session.entries.read(r);
|
||||
@@ -186,25 +216,25 @@ export class InlineChatController implements IEditorContribution {
|
||||
ctxFileBelongsToChat.set(hasEdits);
|
||||
}));
|
||||
|
||||
const overlayWidget = this._inputWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs));
|
||||
const sessionOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatSessionOverlayWidget, editorObs));
|
||||
this.inputOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget));
|
||||
const overlayWidget = this.#inputWidget = this.#store.add(this.#instaService.createInstance(InlineChatInputWidget, editorObs));
|
||||
const sessionOverlayWidget = this.#store.add(this.#instaService.createInstance(InlineChatSessionOverlayWidget, editorObs));
|
||||
this.inputOverlayWidget = this.#store.add(this.#instaService.createInstance(InlineChatAffordance, this.#editor, overlayWidget));
|
||||
|
||||
this._zone = new Lazy<InlineChatZoneWidget>(() => {
|
||||
this.#zone = new Lazy<InlineChatZoneWidget>(() => {
|
||||
|
||||
assertType(this._editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model');
|
||||
assertType(this.#editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model');
|
||||
|
||||
const location: IChatWidgetLocationOptions = {
|
||||
location: ChatAgentLocation.EditorInline,
|
||||
resolveData: () => {
|
||||
assertType(this._editor.hasModel());
|
||||
const wholeRange = this._editor.getSelection();
|
||||
const document = this._editor.getModel().uri;
|
||||
assertType(this.#editor.hasModel());
|
||||
const wholeRange = this.#editor.getSelection();
|
||||
const document = this.#editor.getModel().uri;
|
||||
|
||||
return {
|
||||
type: ChatAgentLocation.EditorInline,
|
||||
id: getEditorId(this._editor, this._editor.getModel()),
|
||||
selection: this._editor.getSelection(),
|
||||
id: getEditorId(this.#editor, this.#editor.getModel()),
|
||||
selection: this.#editor.getSelection(),
|
||||
document,
|
||||
wholeRange
|
||||
};
|
||||
@@ -214,22 +244,22 @@ export class InlineChatController implements IEditorContribution {
|
||||
// inline chat in notebooks
|
||||
// check if this editor is part of a notebook editor
|
||||
// if so, update the location and use the notebook specific widget
|
||||
const notebookEditor = this._notebookEditorService.getNotebookForPossibleCell(this._editor);
|
||||
const notebookEditor = this.#notebookEditorService.getNotebookForPossibleCell(this.#editor);
|
||||
if (!!notebookEditor) {
|
||||
location.location = ChatAgentLocation.Notebook;
|
||||
if (notebookAgentConfig.get()) {
|
||||
location.resolveData = () => {
|
||||
assertType(this._editor.hasModel());
|
||||
assertType(this.#editor.hasModel());
|
||||
|
||||
return {
|
||||
type: ChatAgentLocation.Notebook,
|
||||
sessionInputUri: this._editor.getModel().uri,
|
||||
sessionInputUri: this.#editor.getModel().uri,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result = this._instaService.createInstance(InlineChatZoneWidget,
|
||||
const result = this.#instaService.createInstance(InlineChatZoneWidget,
|
||||
location,
|
||||
{
|
||||
enableWorkingSet: 'implicit',
|
||||
@@ -249,33 +279,33 @@ export class InlineChatController implements IEditorContribution {
|
||||
},
|
||||
defaultMode: ChatMode.Ask
|
||||
},
|
||||
{ editor: this._editor, notebookEditor },
|
||||
{ editor: this.#editor, notebookEditor },
|
||||
() => Promise.resolve(),
|
||||
);
|
||||
|
||||
this._store.add(result);
|
||||
this.#store.add(result);
|
||||
|
||||
result.domNode.classList.add('inline-chat-2');
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessionService.onDidChangeSessions);
|
||||
const sessionsSignal = observableSignalFromEvent(this, inlineChatSessionService.onDidChangeSessions);
|
||||
|
||||
this._currentSession = derived(r => {
|
||||
this.#currentSession = derived(r => {
|
||||
sessionsSignal.read(r);
|
||||
const model = editorObs.model.read(r);
|
||||
const session = model && _inlineChatSessionService.getSessionByTextModel(model.uri);
|
||||
const session = model && inlineChatSessionService.getSessionByTextModel(model.uri);
|
||||
return session ?? undefined;
|
||||
});
|
||||
|
||||
|
||||
let lastSession: IInlineChatSession2 | undefined = undefined;
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
const session = this._currentSession.read(r);
|
||||
this.#store.add(autorun(r => {
|
||||
const session = this.#currentSession.read(r);
|
||||
if (!session) {
|
||||
this._isActiveController.set(false, undefined);
|
||||
this.#isActiveController.set(false, undefined);
|
||||
|
||||
if (lastSession && !lastSession.chatModel.hasRequests) {
|
||||
const state = lastSession.chatModel.inputModel.state.read(undefined);
|
||||
@@ -291,23 +321,24 @@ export class InlineChatController implements IEditorContribution {
|
||||
|
||||
let foundOne = false;
|
||||
for (const editor of codeEditorService.listCodeEditors()) {
|
||||
if (Boolean(InlineChatController.get(editor)?._isActiveController.read(undefined))) {
|
||||
const ctrl = InlineChatController.get(editor);
|
||||
if (ctrl && ctrl.#isActiveController.read(undefined)) {
|
||||
foundOne = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundOne && editorObs.isFocused.read(r)) {
|
||||
this._isActiveController.set(true, undefined);
|
||||
this.#isActiveController.set(true, undefined);
|
||||
}
|
||||
}));
|
||||
|
||||
const visibleSessionObs = observableValue<IInlineChatSession2 | undefined>(this, undefined);
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
this.#store.add(autorun(r => {
|
||||
|
||||
const model = editorObs.model.read(r);
|
||||
const session = this._currentSession.read(r);
|
||||
const isActive = this._isActiveController.read(r);
|
||||
const session = this.#currentSession.read(r);
|
||||
const isActive = this.#isActiveController.read(r);
|
||||
|
||||
if (!session || !isActive || !model) {
|
||||
visibleSessionObs.set(undefined, undefined);
|
||||
@@ -322,42 +353,42 @@ export class InlineChatController implements IEditorContribution {
|
||||
: localize('placeholderWithSelection', "Modify selected code");
|
||||
});
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
this.#store.add(autorun(r => {
|
||||
const session = visibleSessionObs.read(r);
|
||||
ctxTerminated.set(!!session?.terminationState.read(r));
|
||||
}));
|
||||
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
this.#store.add(autorun(r => {
|
||||
|
||||
// HIDE/SHOW
|
||||
const session = visibleSessionObs.read(r);
|
||||
const renderMode = this._renderMode.read(r);
|
||||
const renderMode = this.#renderMode.read(r);
|
||||
if (!session) {
|
||||
this._zone.rawValue?.hide();
|
||||
this._zone.rawValue?.widget.chatWidget.setModel(undefined);
|
||||
_editor.focus();
|
||||
this.#zone.rawValue?.hide();
|
||||
this.#zone.rawValue?.widget.chatWidget.setModel(undefined);
|
||||
editor.focus();
|
||||
ctxInlineChatVisible.reset();
|
||||
} else if (renderMode === 'hover') {
|
||||
// hover mode: no zone widget needed, keep focus in editor
|
||||
ctxInlineChatVisible.set(true);
|
||||
} else {
|
||||
ctxInlineChatVisible.set(true);
|
||||
this._zone.value.widget.chatWidget.setModel(session.chatModel);
|
||||
if (!this._zone.value.position) {
|
||||
this._zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));
|
||||
this._zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug
|
||||
this._zone.value.show(session.initialPosition);
|
||||
this.#zone.value.widget.chatWidget.setModel(session.chatModel);
|
||||
if (!this.#zone.value.position) {
|
||||
this.#zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));
|
||||
this.#zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug
|
||||
this.#zone.value.show(session.initialPosition);
|
||||
}
|
||||
this._zone.value.reveal(this._zone.value.position!);
|
||||
this._zone.value.widget.focus();
|
||||
this.#zone.value.reveal(this.#zone.value.position!);
|
||||
this.#zone.value.widget.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
// Show progress overlay widget in hover mode when a request is in progress or edits are not yet settled
|
||||
this._store.add(autorun(r => {
|
||||
this.#store.add(autorun(r => {
|
||||
const session = visibleSessionObs.read(r);
|
||||
const renderMode = this._renderMode.read(r);
|
||||
const renderMode = this.#renderMode.read(r);
|
||||
if (!session || renderMode !== 'hover') {
|
||||
ctxPendingConfirmation.set(false);
|
||||
sessionOverlayWidget.hide();
|
||||
@@ -380,7 +411,7 @@ export class InlineChatController implements IEditorContribution {
|
||||
}
|
||||
}));
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
this.#store.add(autorun(r => {
|
||||
const session = visibleSessionObs.read(r);
|
||||
if (session) {
|
||||
const entries = session.editingSession.entries.read(r);
|
||||
@@ -398,7 +429,7 @@ export class InlineChatController implements IEditorContribution {
|
||||
for (const entry of otherEntries) {
|
||||
// OPEN other modified files in side group. This is a workaround, temp-solution until we have no more backend
|
||||
// that modifies other files
|
||||
this._editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError);
|
||||
this.#editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError);
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -419,40 +450,40 @@ export class InlineChatController implements IEditorContribution {
|
||||
});
|
||||
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
this.#store.add(autorun(r => {
|
||||
const session = visibleSessionObs.read(r);
|
||||
const response = lastResponseObs.read(r);
|
||||
const terminationState = session?.terminationState.read(r);
|
||||
|
||||
this._zone.rawValue?.widget.updateInfo('');
|
||||
this.#zone.rawValue?.widget.updateInfo('');
|
||||
|
||||
if (!response?.isInProgress.read(r)) {
|
||||
|
||||
if (response?.result?.errorDetails) {
|
||||
// ERROR case
|
||||
this._zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`);
|
||||
this.#zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`);
|
||||
alert(response.result.errorDetails.message);
|
||||
} else if (terminationState) {
|
||||
this._zone.rawValue?.widget.updateInfo(`$(info) ${renderAsPlaintext(terminationState)}`);
|
||||
this.#zone.rawValue?.widget.updateInfo(`$(info) ${renderAsPlaintext(terminationState)}`);
|
||||
}
|
||||
|
||||
// no response or not in progress
|
||||
this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false);
|
||||
this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));
|
||||
this.#zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false);
|
||||
this.#zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));
|
||||
|
||||
} else {
|
||||
this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true);
|
||||
this.#zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true);
|
||||
let placeholder = response.request?.message.text;
|
||||
const lastProgress = lastResponseProgressObs.read(r);
|
||||
if (lastProgress) {
|
||||
placeholder = renderAsPlaintext(lastProgress.content);
|
||||
}
|
||||
this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working..."));
|
||||
this.#zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working..."));
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
this.#store.add(autorun(r => {
|
||||
const session = visibleSessionObs.read(r);
|
||||
if (!session) {
|
||||
return;
|
||||
@@ -465,25 +496,25 @@ export class InlineChatController implements IEditorContribution {
|
||||
}));
|
||||
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
this.#store.add(autorun(r => {
|
||||
|
||||
const session = visibleSessionObs.read(r);
|
||||
const entry = session?.editingSession.readEntry(session.uri, r);
|
||||
|
||||
// make sure there is an editor integration
|
||||
const pane = this._editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this._editor || isNotebookWithCellEditor(candidate, this._editor));
|
||||
const pane = this.#editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this.#editor || isNotebookWithCellEditor(candidate, this.#editor));
|
||||
if (pane && entry) {
|
||||
entry?.getEditorIntegration(pane);
|
||||
}
|
||||
|
||||
// make sure the ZONE isn't inbetween a diff and move above if so
|
||||
if (entry?.diffInfo && this._zone.rawValue?.position) {
|
||||
const { position } = this._zone.rawValue;
|
||||
if (entry?.diffInfo && this.#zone.rawValue?.position) {
|
||||
const { position } = this.#zone.rawValue;
|
||||
const diff = entry.diffInfo.read(r);
|
||||
|
||||
for (const change of diff.changes) {
|
||||
if (change.modified.contains(position.lineNumber)) {
|
||||
this._zone.rawValue?.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1));
|
||||
this.#zone.rawValue?.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -492,35 +523,35 @@ export class InlineChatController implements IEditorContribution {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._store.dispose();
|
||||
this.#store.dispose();
|
||||
}
|
||||
|
||||
getWidgetPosition(): Position | undefined {
|
||||
return this._zone.rawValue?.position;
|
||||
return this.#zone.rawValue?.position;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this._zone.rawValue?.widget.focus();
|
||||
this.#zone.rawValue?.widget.focus();
|
||||
}
|
||||
|
||||
async run(arg?: InlineChatRunOptions): Promise<boolean> {
|
||||
assertType(this._editor.hasModel());
|
||||
const uri = this._editor.getModel().uri;
|
||||
assertType(this.#editor.hasModel());
|
||||
const uri = this.#editor.getModel().uri;
|
||||
|
||||
const existingSession = this._inlineChatSessionService.getSessionByTextModel(uri);
|
||||
const existingSession = this.#inlineChatSessionService.getSessionByTextModel(uri);
|
||||
if (existingSession) {
|
||||
await existingSession.editingSession.accept();
|
||||
existingSession.dispose();
|
||||
}
|
||||
|
||||
this._isActiveController.set(true, undefined);
|
||||
this.#isActiveController.set(true, undefined);
|
||||
|
||||
const session = this._inlineChatSessionService.createSession(this._editor);
|
||||
const session = this.#inlineChatSessionService.createSession(this.#editor);
|
||||
|
||||
if (this._renderMode.get() === 'hover') {
|
||||
return this._runHover(session, arg);
|
||||
if (this.#renderMode.get() === 'hover') {
|
||||
return this.#runHover(session, arg);
|
||||
} else {
|
||||
return this._runZone(session, arg);
|
||||
return this.#runZone(session, arg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,40 +559,40 @@ export class InlineChatController implements IEditorContribution {
|
||||
* Hover mode: submit requests directly via IChatService.sendRequest without
|
||||
* instantiating the zone widget.
|
||||
*/
|
||||
private async _runHover(session: IInlineChatSession2, arg?: InlineChatRunOptions): Promise<boolean> {
|
||||
assertType(this._editor.hasModel());
|
||||
const uri = this._editor.getModel().uri;
|
||||
async #runHover(session: IInlineChatSession2, arg?: InlineChatRunOptions): Promise<boolean> {
|
||||
assertType(this.#editor.hasModel());
|
||||
const uri = this.#editor.getModel().uri;
|
||||
|
||||
|
||||
// Apply editor adjustments from args
|
||||
if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) {
|
||||
if (arg.initialRange) {
|
||||
this._editor.revealRange(arg.initialRange);
|
||||
this.#editor.revealRange(arg.initialRange);
|
||||
}
|
||||
if (arg.initialSelection) {
|
||||
this._editor.setSelection(arg.initialSelection);
|
||||
this.#editor.setSelection(arg.initialSelection);
|
||||
}
|
||||
}
|
||||
|
||||
// Build location data (after selection adjustments)
|
||||
const { location, locationData } = this._buildLocationData();
|
||||
const { location, locationData } = this.#buildLocationData();
|
||||
|
||||
// Resolve model
|
||||
let userSelectedModelId: string | undefined;
|
||||
if (arg?.modelSelector) {
|
||||
userSelectedModelId = (await this._languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0);
|
||||
userSelectedModelId = (await this.#languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0);
|
||||
if (!userSelectedModelId) {
|
||||
throw new Error(`No language models found matching selector: ${JSON.stringify(arg.modelSelector)}.`);
|
||||
}
|
||||
} else {
|
||||
userSelectedModelId = await this._resolveModelId(location);
|
||||
userSelectedModelId = await this.#resolveModelId(location);
|
||||
}
|
||||
|
||||
// Collect attachments
|
||||
const attachedContext: IChatRequestVariableEntry[] = [];
|
||||
if (arg?.attachments) {
|
||||
for (const attachment of arg.attachments) {
|
||||
const resolved = await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);
|
||||
const resolved = await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);
|
||||
if (resolved) {
|
||||
attachedContext.push(resolved);
|
||||
}
|
||||
@@ -570,7 +601,7 @@ export class InlineChatController implements IEditorContribution {
|
||||
|
||||
// Send the request directly
|
||||
if (arg?.message && arg.autoSend) {
|
||||
await this._chatService.sendRequest(
|
||||
await this.#chatService.sendRequest(
|
||||
session.chatModel.sessionResource,
|
||||
arg.message,
|
||||
{
|
||||
@@ -606,15 +637,15 @@ export class InlineChatController implements IEditorContribution {
|
||||
/**
|
||||
* Zone mode: use the full zone widget and chat widget for request submission.
|
||||
*/
|
||||
private async _runZone(session: IInlineChatSession2, arg?: InlineChatRunOptions): Promise<boolean> {
|
||||
assertType(this._editor.hasModel());
|
||||
const uri = this._editor.getModel().uri;
|
||||
async #runZone(session: IInlineChatSession2, arg?: InlineChatRunOptions): Promise<boolean> {
|
||||
assertType(this.#editor.hasModel());
|
||||
const uri = this.#editor.getModel().uri;
|
||||
|
||||
// Store for tracking model changes during this session
|
||||
const sessionStore = new DisposableStore();
|
||||
|
||||
try {
|
||||
await this._applyModelDefaults(session, sessionStore);
|
||||
await this.#applyModelDefaults(session, sessionStore);
|
||||
|
||||
if (arg) {
|
||||
arg.attachDiagnostics ??= true;
|
||||
@@ -623,52 +654,52 @@ export class InlineChatController implements IEditorContribution {
|
||||
// ADD diagnostics (only when explicitly requested)
|
||||
if (arg?.attachDiagnostics) {
|
||||
const entries: IChatRequestVariableEntry[] = [];
|
||||
for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(uri)) {
|
||||
if (range.intersectRanges(this._editor.getSelection())) {
|
||||
for (const [range, marker] of this.#markerDecorationsService.getLiveMarkers(uri)) {
|
||||
if (range.intersectRanges(this.#editor.getSelection())) {
|
||||
const filter = IDiagnosticVariableEntryFilterData.fromMarker(marker);
|
||||
entries.push(IDiagnosticVariableEntryFilterData.toEntry(filter));
|
||||
}
|
||||
}
|
||||
if (entries.length > 0) {
|
||||
this._zone.value.widget.chatWidget.attachmentModel.addContext(...entries);
|
||||
this.#zone.value.widget.chatWidget.attachmentModel.addContext(...entries);
|
||||
const msg = entries.length > 1
|
||||
? localize('fixN', "Fix the attached problems")
|
||||
: localize('fix1', "Fix the attached problem");
|
||||
this._zone.value.widget.chatWidget.input.setValue(msg, true);
|
||||
this.#zone.value.widget.chatWidget.input.setValue(msg, true);
|
||||
arg.message = msg;
|
||||
this._zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));
|
||||
this.#zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Check args
|
||||
if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) {
|
||||
if (arg.initialRange) {
|
||||
this._editor.revealRange(arg.initialRange);
|
||||
this.#editor.revealRange(arg.initialRange);
|
||||
}
|
||||
if (arg.initialSelection) {
|
||||
this._editor.setSelection(arg.initialSelection);
|
||||
this.#editor.setSelection(arg.initialSelection);
|
||||
}
|
||||
if (arg.attachments) {
|
||||
await Promise.all(arg.attachments.map(async attachment => {
|
||||
await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment);
|
||||
await this.#zone.value.widget.chatWidget.attachmentModel.addFile(attachment);
|
||||
}));
|
||||
delete arg.attachments;
|
||||
}
|
||||
if (arg.modelSelector) {
|
||||
const id = (await this._languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0);
|
||||
const id = (await this.#languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0);
|
||||
if (!id) {
|
||||
throw new Error(`No language models found matching selector: ${JSON.stringify(arg.modelSelector)}.`);
|
||||
}
|
||||
const model = this._languageModelService.lookupLanguageModel(id);
|
||||
const model = this.#languageModelService.lookupLanguageModel(id);
|
||||
if (!model) {
|
||||
throw new Error(`Language model not loaded: ${id}.`);
|
||||
}
|
||||
this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id });
|
||||
this.#zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id });
|
||||
}
|
||||
if (arg.message) {
|
||||
this._zone.value.widget.chatWidget.setInput(arg.message);
|
||||
this.#zone.value.widget.chatWidget.setInput(arg.message);
|
||||
if (arg.autoSend) {
|
||||
await this._zone.value.widget.chatWidget.acceptInput();
|
||||
await this.#zone.value.widget.chatWidget.acceptInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -694,7 +725,7 @@ export class InlineChatController implements IEditorContribution {
|
||||
}
|
||||
|
||||
async acceptSession() {
|
||||
const session = this._currentSession.get();
|
||||
const session = this.#currentSession.get();
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
@@ -703,26 +734,26 @@ export class InlineChatController implements IEditorContribution {
|
||||
}
|
||||
|
||||
async rejectSession() {
|
||||
const session = this._currentSession.get();
|
||||
const session = this.#currentSession.get();
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
await this._chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject');
|
||||
await this.#chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject');
|
||||
await session.editingSession.reject();
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
async continueSessionInChat(): Promise<void> {
|
||||
const session = this._currentSession.get();
|
||||
const session = this.#currentSession.get();
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._instaService.invokeFunction(continueInPanelChat, session);
|
||||
await this.#instaService.invokeFunction(continueInPanelChat, session);
|
||||
}
|
||||
|
||||
async rephraseSession(): Promise<void> {
|
||||
const session = this._currentSession.get();
|
||||
const session = this.#currentSession.get();
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
@@ -734,21 +765,21 @@ export class InlineChatController implements IEditorContribution {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = this._editor.getSelection();
|
||||
const selection = this.#editor.getSelection();
|
||||
const placeholder = selection && !selection.isEmpty()
|
||||
? localize('placeholderWithSelectionHover', "Describe how to change this")
|
||||
: localize('placeholderNoSelectionHover', "Describe what to generate");
|
||||
await this.inputOverlayWidget.showMenuAtSelection(placeholder, requestText);
|
||||
}
|
||||
|
||||
private async _selectVendorDefaultModel(session: IInlineChatSession2): Promise<void> {
|
||||
const model = this._zone.value.widget.chatWidget.input.selectedLanguageModel.get();
|
||||
async #selectVendorDefaultModel(session: IInlineChatSession2): Promise<void> {
|
||||
const model = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.get();
|
||||
if (model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) {
|
||||
const ids = await this._languageModelService.selectLanguageModels({ vendor: model.metadata.vendor });
|
||||
const ids = await this.#languageModelService.selectLanguageModels({ vendor: model.metadata.vendor });
|
||||
for (const identifier of ids) {
|
||||
const candidate = this._languageModelService.lookupLanguageModel(identifier);
|
||||
const candidate = this.#languageModelService.lookupLanguageModel(identifier);
|
||||
if (candidate?.isDefaultForLocation[session.chatModel.initialLocation]) {
|
||||
this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier });
|
||||
this.#zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier });
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -761,32 +792,32 @@ export class InlineChatController implements IEditorContribution {
|
||||
*
|
||||
* Priority: user session choice > inlineChat.defaultModel setting > vendor default for location
|
||||
*/
|
||||
private async _resolveModelId(location: ChatAgentLocation): Promise<string | undefined> {
|
||||
const userSelectedModel = InlineChatController._userSelectedModel;
|
||||
const defaultModelSetting = this._configurationService.getValue<string>(InlineChatConfigKeys.DefaultModel);
|
||||
async #resolveModelId(location: ChatAgentLocation): Promise<string | undefined> {
|
||||
const userSelectedModel = InlineChatController.#userSelectedModel;
|
||||
const defaultModelSetting = this.#configurationService.getValue<string>(InlineChatConfigKeys.DefaultModel);
|
||||
|
||||
// 1. Try user's explicitly chosen model from a previous inline chat
|
||||
if (userSelectedModel) {
|
||||
const match = this._languageModelService.lookupLanguageModelByQualifiedName(userSelectedModel);
|
||||
const match = this.#languageModelService.lookupLanguageModelByQualifiedName(userSelectedModel);
|
||||
if (match) {
|
||||
return match.identifier;
|
||||
}
|
||||
// Previously selected model is no longer available
|
||||
InlineChatController._userSelectedModel = undefined;
|
||||
InlineChatController.#userSelectedModel = undefined;
|
||||
}
|
||||
|
||||
// 2. Try inlineChat.defaultModel setting
|
||||
if (defaultModelSetting) {
|
||||
const match = this._languageModelService.lookupLanguageModelByQualifiedName(defaultModelSetting);
|
||||
const match = this.#languageModelService.lookupLanguageModelByQualifiedName(defaultModelSetting);
|
||||
if (match) {
|
||||
return match.identifier;
|
||||
}
|
||||
this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`);
|
||||
this.#logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`);
|
||||
}
|
||||
|
||||
// 3. Fall back to vendor default for the given location
|
||||
for (const id of this._languageModelService.getLanguageModelIds()) {
|
||||
const metadata = this._languageModelService.lookupLanguageModel(id);
|
||||
for (const id of this.#languageModelService.getLanguageModelIds()) {
|
||||
const metadata = this.#languageModelService.lookupLanguageModel(id);
|
||||
if (metadata?.isDefaultForLocation[location]) {
|
||||
return id;
|
||||
}
|
||||
@@ -798,18 +829,18 @@ export class InlineChatController implements IEditorContribution {
|
||||
/**
|
||||
* Builds location data for chat requests without going through the zone widget.
|
||||
*/
|
||||
private _buildLocationData(): { location: ChatAgentLocation; locationData: IChatLocationData } {
|
||||
assertType(this._editor.hasModel());
|
||||
#buildLocationData(): { location: ChatAgentLocation; locationData: IChatLocationData } {
|
||||
assertType(this.#editor.hasModel());
|
||||
|
||||
const notebookEditor = this._notebookEditorService.getNotebookForPossibleCell(this._editor);
|
||||
const notebookEditor = this.#notebookEditorService.getNotebookForPossibleCell(this.#editor);
|
||||
if (notebookEditor) {
|
||||
const useNotebookAgent = this._configurationService.getValue<boolean>(InlineChatConfigKeys.notebookAgent);
|
||||
const useNotebookAgent = this.#configurationService.getValue<boolean>(InlineChatConfigKeys.notebookAgent);
|
||||
if (useNotebookAgent) {
|
||||
return {
|
||||
location: ChatAgentLocation.Notebook,
|
||||
locationData: {
|
||||
type: ChatAgentLocation.Notebook,
|
||||
sessionInputUri: this._editor.getModel().uri,
|
||||
sessionInputUri: this.#editor.getModel().uri,
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -819,10 +850,10 @@ export class InlineChatController implements IEditorContribution {
|
||||
location: ChatAgentLocation.Notebook,
|
||||
locationData: {
|
||||
type: ChatAgentLocation.EditorInline,
|
||||
id: getEditorId(this._editor, this._editor.getModel()),
|
||||
selection: this._editor.getSelection(),
|
||||
document: this._editor.getModel().uri,
|
||||
wholeRange: this._editor.getSelection(),
|
||||
id: getEditorId(this.#editor, this.#editor.getModel()),
|
||||
selection: this.#editor.getSelection(),
|
||||
document: this.#editor.getModel().uri,
|
||||
wholeRange: this.#editor.getSelection(),
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -831,10 +862,10 @@ export class InlineChatController implements IEditorContribution {
|
||||
location: ChatAgentLocation.EditorInline,
|
||||
locationData: {
|
||||
type: ChatAgentLocation.EditorInline,
|
||||
id: getEditorId(this._editor, this._editor.getModel()),
|
||||
selection: this._editor.getSelection(),
|
||||
document: this._editor.getModel().uri,
|
||||
wholeRange: this._editor.getSelection(),
|
||||
id: getEditorId(this.#editor, this.#editor.getModel()),
|
||||
selection: this.#editor.getSelection(),
|
||||
document: this.#editor.getModel().uri,
|
||||
wholeRange: this.#editor.getSelection(),
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -843,39 +874,39 @@ export class InlineChatController implements IEditorContribution {
|
||||
* Applies model defaults based on settings and tracks user model changes.
|
||||
* Prioritization: user session choice > inlineChat.defaultModel setting > vendor default
|
||||
*/
|
||||
private async _applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise<void> {
|
||||
const userSelectedModel = InlineChatController._userSelectedModel;
|
||||
const defaultModelSetting = this._configurationService.getValue<string>(InlineChatConfigKeys.DefaultModel);
|
||||
async #applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise<void> {
|
||||
const userSelectedModel = InlineChatController.#userSelectedModel;
|
||||
const defaultModelSetting = this.#configurationService.getValue<string>(InlineChatConfigKeys.DefaultModel);
|
||||
|
||||
let modelApplied = false;
|
||||
|
||||
// 1. Try user's explicitly chosen model from a previous inline chat in the same session
|
||||
if (userSelectedModel) {
|
||||
modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]);
|
||||
modelApplied = this.#zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]);
|
||||
if (!modelApplied) {
|
||||
// User's previously selected model is no longer available, clear it
|
||||
InlineChatController._userSelectedModel = undefined;
|
||||
InlineChatController.#userSelectedModel = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try inlineChat.defaultModel setting
|
||||
if (!modelApplied && defaultModelSetting) {
|
||||
modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]);
|
||||
modelApplied = this.#zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]);
|
||||
if (!modelApplied) {
|
||||
this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`);
|
||||
this.#logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fall back to vendor default
|
||||
if (!modelApplied) {
|
||||
await this._selectVendorDefaultModel(session);
|
||||
await this.#selectVendorDefaultModel(session);
|
||||
}
|
||||
|
||||
// Track model changes - store user's explicit choice in the given sessions.
|
||||
// NOTE: This currently detects any model change, not just user-initiated ones.
|
||||
let initialModelId: string | undefined;
|
||||
sessionStore.add(autorun(r => {
|
||||
const newModel = this._zone.value.widget.chatWidget.input.selectedLanguageModel.read(r);
|
||||
const newModel = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.read(r);
|
||||
if (!newModel) {
|
||||
return;
|
||||
}
|
||||
@@ -885,25 +916,25 @@ export class InlineChatController implements IEditorContribution {
|
||||
}
|
||||
if (initialModelId !== newModel.identifier) {
|
||||
// User explicitly changed model, store their choice as qualified name
|
||||
InlineChatController._userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata);
|
||||
InlineChatController.#userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata);
|
||||
initialModelId = newModel.identifier;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async createImageAttachment(attachment: URI): Promise<IChatRequestVariableEntry | undefined> {
|
||||
const value = this._currentSession.get();
|
||||
const value = this.#currentSession.get();
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
if (attachment.scheme === Schemas.file) {
|
||||
if (await this._fileService.canHandleResource(attachment)) {
|
||||
return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);
|
||||
if (await this.#fileService.canHandleResource(attachment)) {
|
||||
return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);
|
||||
}
|
||||
} else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) {
|
||||
const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None);
|
||||
const extractedImages = await this.#webContentExtractorService.readImage(attachment, CancellationToken.None);
|
||||
if (extractedImages) {
|
||||
return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages);
|
||||
return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -33,12 +33,13 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j
|
||||
|
||||
class QuickFixActionViewItem extends MenuEntryActionViewItem {
|
||||
|
||||
private readonly _lightBulbStore = this._store.add(new MutableDisposable<DisposableStore>());
|
||||
private _currentTitle: string | undefined;
|
||||
readonly #lightBulbStore = this._store.add(new MutableDisposable<DisposableStore>());
|
||||
#currentTitle: string | undefined;
|
||||
readonly #editor: ICodeEditor;
|
||||
|
||||
constructor(
|
||||
action: MenuItemAction,
|
||||
private readonly _editor: ICodeEditor,
|
||||
editor: ICodeEditor,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@@ -55,7 +56,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem {
|
||||
elementGetter: () => HTMLElement | undefined = () => undefined;
|
||||
|
||||
override async run(...args: unknown[]): Promise<void> {
|
||||
const controller = CodeActionController.get(_editor);
|
||||
const controller = CodeActionController.get(editor);
|
||||
const info = controller?.lightBulbState.get();
|
||||
const element = this.elementGetter();
|
||||
if (controller && info && element) {
|
||||
@@ -67,26 +68,27 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem {
|
||||
|
||||
super(wrappedAction, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService);
|
||||
|
||||
this.#editor = editor;
|
||||
wrappedAction.elementGetter = () => this.element;
|
||||
}
|
||||
|
||||
override render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
this._updateFromLightBulb();
|
||||
this.#updateFromLightBulb();
|
||||
}
|
||||
|
||||
protected override getTooltip(): string {
|
||||
return this._currentTitle ?? super.getTooltip();
|
||||
return this.#currentTitle ?? super.getTooltip();
|
||||
}
|
||||
|
||||
private _updateFromLightBulb(): void {
|
||||
const controller = CodeActionController.get(this._editor);
|
||||
#updateFromLightBulb(): void {
|
||||
const controller = CodeActionController.get(this.#editor);
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
const store = new DisposableStore();
|
||||
this._lightBulbStore.value = store;
|
||||
this.#lightBulbStore.value = store;
|
||||
|
||||
store.add(autorun(reader => {
|
||||
const info = controller.lightBulbState.read(reader);
|
||||
@@ -99,7 +101,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem {
|
||||
}
|
||||
|
||||
// Update tooltip
|
||||
this._currentTitle = info?.title;
|
||||
this.#currentTitle = info?.title;
|
||||
this.updateTooltip();
|
||||
}));
|
||||
}
|
||||
@@ -107,7 +109,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem {
|
||||
|
||||
class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem {
|
||||
|
||||
private readonly _kbLabel: string | undefined;
|
||||
readonly #kbLabel: string | undefined;
|
||||
|
||||
constructor(
|
||||
action: MenuItemAction,
|
||||
@@ -121,14 +123,14 @@ class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem {
|
||||
super(action, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService);
|
||||
this.options.label = true;
|
||||
this.options.icon = false;
|
||||
this._kbLabel = keybindingService.lookupKeybinding(action.id)?.getLabel() ?? undefined;
|
||||
this.#kbLabel = keybindingService.lookupKeybinding(action.id)?.getLabel() ?? undefined;
|
||||
}
|
||||
|
||||
protected override updateLabel(): void {
|
||||
if (this.label) {
|
||||
dom.reset(this.label,
|
||||
this.action.label,
|
||||
...(this._kbLabel ? [dom.$('span.inline-chat-keybinding', undefined, this._kbLabel)] : [])
|
||||
...(this.#kbLabel ? [dom.$('span.inline-chat-keybinding', undefined, this.#kbLabel)] : [])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -140,38 +142,42 @@ class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem {
|
||||
*/
|
||||
export class InlineChatEditorAffordance extends Disposable implements IContentWidget {
|
||||
|
||||
private static _idPool = 0;
|
||||
static #idPool = 0;
|
||||
|
||||
private readonly _id = `inline-chat-content-widget-${InlineChatEditorAffordance._idPool++}`;
|
||||
private readonly _domNode: HTMLElement;
|
||||
private _position: IContentWidgetPosition | null = null;
|
||||
private _isVisible = false;
|
||||
readonly #id = `inline-chat-content-widget-${InlineChatEditorAffordance.#idPool++}`;
|
||||
readonly #domNode: HTMLElement;
|
||||
#position: IContentWidgetPosition | null = null;
|
||||
#isVisible = false;
|
||||
|
||||
private readonly _onDidRunAction = this._store.add(new Emitter<string>());
|
||||
readonly onDidRunAction: Event<string> = this._onDidRunAction.event;
|
||||
readonly #onDidRunAction = this._store.add(new Emitter<string>());
|
||||
readonly onDidRunAction: Event<string> = this.#onDidRunAction.event;
|
||||
|
||||
readonly allowEditorOverflow = true;
|
||||
readonly suppressMouseDown = false;
|
||||
|
||||
readonly #editor: ICodeEditor;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
editor: ICodeEditor,
|
||||
selection: IObservable<Selection | undefined>,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.#editor = editor;
|
||||
|
||||
// Create the widget DOM
|
||||
this._domNode = dom.$('.inline-chat-content-widget');
|
||||
this.#domNode = dom.$('.inline-chat-content-widget');
|
||||
|
||||
// Create toolbar with the inline chat start action
|
||||
const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._domNode, MenuId.InlineChatEditorAffordance, {
|
||||
const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this.#domNode, MenuId.InlineChatEditorAffordance, {
|
||||
telemetrySource: 'inlineChatEditorAffordance',
|
||||
hiddenItemStrategy: HiddenItemStrategy.Ignore,
|
||||
menuOptions: { renderShortTitle: true },
|
||||
toolbarOptions: { primaryGroup: () => true, useSeparatorsInPrimaryActions: true },
|
||||
actionViewItemProvider: (action: IAction) => {
|
||||
if (action instanceof MenuItemAction && action.id === quickFixCommandId) {
|
||||
return instantiationService.createInstance(QuickFixActionViewItem, action, this._editor);
|
||||
return instantiationService.createInstance(QuickFixActionViewItem, action, this.#editor);
|
||||
}
|
||||
if (action instanceof MenuItemAction && (action.id === ACTION_START || action.id === ACTION_ASK_IN_CHAT || action.id === 'inlineChat.fixDiagnostics')) {
|
||||
return instantiationService.createInstance(LabelWithKeybindingActionViewItem, action);
|
||||
@@ -180,50 +186,50 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi
|
||||
}
|
||||
}));
|
||||
this._store.add(toolbar.actionRunner.onDidRun((e) => {
|
||||
this._onDidRunAction.fire(e.action.id);
|
||||
this._hide();
|
||||
this.#onDidRunAction.fire(e.action.id);
|
||||
this.#hide();
|
||||
}));
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
const sel = selection.read(r);
|
||||
if (sel) {
|
||||
this._show(sel);
|
||||
this.#show(sel);
|
||||
} else {
|
||||
this._hide();
|
||||
this.#hide();
|
||||
}
|
||||
}));
|
||||
|
||||
this._store.add(this._editor.onDidScrollChange(() => {
|
||||
this._store.add(this.#editor.onDidScrollChange(() => {
|
||||
const sel = selection.get();
|
||||
if (!sel) {
|
||||
return;
|
||||
}
|
||||
const isInViewport = this._isPositionInViewport();
|
||||
if (isInViewport && !this._isVisible) {
|
||||
this._show(sel);
|
||||
} else if (!isInViewport && this._isVisible) {
|
||||
this._hide();
|
||||
const isInViewport = this.#isPositionInViewport();
|
||||
if (isInViewport && !this.#isVisible) {
|
||||
this.#show(sel);
|
||||
} else if (!isInViewport && this.#isVisible) {
|
||||
this.#hide();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _show(selection: Selection): void {
|
||||
#show(selection: Selection): void {
|
||||
|
||||
if (selection.isEmpty()) {
|
||||
this._showAtLineStart(selection.getPosition().lineNumber);
|
||||
this.#showAtLineStart(selection.getPosition().lineNumber);
|
||||
} else {
|
||||
this._showAtSelection(selection);
|
||||
this.#showAtSelection(selection);
|
||||
}
|
||||
|
||||
if (this._isVisible) {
|
||||
this._editor.layoutContentWidget(this);
|
||||
if (this.#isVisible) {
|
||||
this.#editor.layoutContentWidget(this);
|
||||
} else {
|
||||
this._editor.addContentWidget(this);
|
||||
this._isVisible = true;
|
||||
this.#editor.addContentWidget(this);
|
||||
this.#isVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _showAtSelection(selection: Selection): void {
|
||||
#showAtSelection(selection: Selection): void {
|
||||
const cursorPosition = selection.getPosition();
|
||||
const direction = selection.getDirection();
|
||||
|
||||
@@ -231,20 +237,20 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi
|
||||
? ContentWidgetPositionPreference.ABOVE
|
||||
: ContentWidgetPositionPreference.BELOW;
|
||||
|
||||
this._position = {
|
||||
this.#position = {
|
||||
position: cursorPosition,
|
||||
preference: [preference],
|
||||
};
|
||||
}
|
||||
|
||||
private _showAtLineStart(lineNumber: number): void {
|
||||
const model = this._editor.getModel();
|
||||
#showAtLineStart(lineNumber: number): void {
|
||||
const model = this.#editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabSize = model.getOptions().tabSize;
|
||||
const fontInfo = this._editor.getOptions().get(EditorOption.fontInfo);
|
||||
const fontInfo = this.#editor.getOptions().get(EditorOption.fontInfo);
|
||||
const lineContent = model.getLineContent(lineNumber);
|
||||
const indent = computeIndentLevel(lineContent, tabSize);
|
||||
const lineHasSpace = indent < 0 ? true : fontInfo.spaceWidth * indent > 22;
|
||||
@@ -267,20 +273,20 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi
|
||||
|
||||
const effectiveColumnNumber = /^\S\s*$/.test(model.getLineContent(effectiveLineNumber)) ? 2 : 1;
|
||||
|
||||
this._position = {
|
||||
this.#position = {
|
||||
position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber },
|
||||
preference: [ContentWidgetPositionPreference.EXACT],
|
||||
};
|
||||
}
|
||||
|
||||
private _isPositionInViewport(): boolean {
|
||||
const widgetPosition = this._position?.position;
|
||||
#isPositionInViewport(): boolean {
|
||||
const widgetPosition = this.#position?.position;
|
||||
if (!widgetPosition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check vertical visibility
|
||||
const visibleRanges = this._editor.getVisibleRanges();
|
||||
const visibleRanges = this.#editor.getVisibleRanges();
|
||||
const isLineVisible = visibleRanges.some(range =>
|
||||
widgetPosition.lineNumber >= range.startLineNumber && widgetPosition.lineNumber <= range.endLineNumber
|
||||
);
|
||||
@@ -289,45 +295,45 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi
|
||||
}
|
||||
|
||||
// Check horizontal visibility
|
||||
const scrolledPos = this._editor.getScrolledVisiblePosition(widgetPosition);
|
||||
const scrolledPos = this.#editor.getScrolledVisiblePosition(widgetPosition);
|
||||
if (!scrolledPos) {
|
||||
return false;
|
||||
}
|
||||
const layoutInfo = this._editor.getOptions().get(EditorOption.layoutInfo);
|
||||
const layoutInfo = this.#editor.getOptions().get(EditorOption.layoutInfo);
|
||||
return scrolledPos.left >= 0 && scrolledPos.left <= layoutInfo.width;
|
||||
}
|
||||
|
||||
private _hide(): void {
|
||||
if (this._isVisible) {
|
||||
this._isVisible = false;
|
||||
this._editor.removeContentWidget(this);
|
||||
#hide(): void {
|
||||
if (this.#isVisible) {
|
||||
this.#isVisible = false;
|
||||
this.#editor.removeContentWidget(this);
|
||||
}
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return this._id;
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
return this.#domNode;
|
||||
}
|
||||
|
||||
getPosition(): IContentWidgetPosition | null {
|
||||
return this._position;
|
||||
return this.#position;
|
||||
}
|
||||
|
||||
beforeRender(): IDimension | null {
|
||||
const position = this._editor.getPosition();
|
||||
const lineHeight = position ? this._editor.getLineHeightForPosition(position) : this._editor.getOption(EditorOption.lineHeight);
|
||||
const position = this.#editor.getPosition();
|
||||
const lineHeight = position ? this.#editor.getLineHeightForPosition(position) : this.#editor.getOption(EditorOption.lineHeight);
|
||||
|
||||
this._domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`);
|
||||
this.#domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
if (this._isVisible) {
|
||||
this._editor.removeContentWidget(this);
|
||||
if (this.#isVisible) {
|
||||
this.#editor.removeContentWidget(this);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -27,14 +27,17 @@ const _capacity = 50;
|
||||
export class InlineChatHistoryService extends Disposable implements IInlineChatHistoryService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _history: HistoryNavigator2<string>;
|
||||
readonly #history: HistoryNavigator2<string>;
|
||||
readonly #storageService: IStorageService;
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const raw = this._storageService.get(_storageKey, StorageScope.PROFILE);
|
||||
this.#storageService = storageService;
|
||||
|
||||
const raw = this.#storageService.get(_storageKey, StorageScope.PROFILE);
|
||||
let entries: string[] = [''];
|
||||
if (raw) {
|
||||
try {
|
||||
@@ -51,44 +54,44 @@ export class InlineChatHistoryService extends Disposable implements IInlineChatH
|
||||
}
|
||||
}
|
||||
|
||||
this._history = new HistoryNavigator2<string>(entries, _capacity);
|
||||
this.#history = new HistoryNavigator2<string>(entries, _capacity);
|
||||
|
||||
this._store.add(this._storageService.onWillSaveState(() => {
|
||||
this._saveToStorage();
|
||||
this._store.add(this.#storageService.onWillSaveState(() => {
|
||||
this.#saveToStorage();
|
||||
}));
|
||||
}
|
||||
|
||||
private _saveToStorage(): void {
|
||||
const values = [...this._history].filter(v => v.length > 0);
|
||||
#saveToStorage(): void {
|
||||
const values = [...this.#history].filter(v => v.length > 0);
|
||||
if (values.length === 0) {
|
||||
this._storageService.remove(_storageKey, StorageScope.PROFILE);
|
||||
this.#storageService.remove(_storageKey, StorageScope.PROFILE);
|
||||
} else {
|
||||
this._storageService.store(_storageKey, JSON.stringify(values), StorageScope.PROFILE, StorageTarget.USER);
|
||||
this.#storageService.store(_storageKey, JSON.stringify(values), StorageScope.PROFILE, StorageTarget.USER);
|
||||
}
|
||||
}
|
||||
|
||||
addToHistory(value: string): void {
|
||||
this._history.replaceLast(value);
|
||||
this._history.add('');
|
||||
this.#history.replaceLast(value);
|
||||
this.#history.add('');
|
||||
}
|
||||
|
||||
previousValue(): string | undefined {
|
||||
return this._history.previous();
|
||||
return this.#history.previous();
|
||||
}
|
||||
|
||||
nextValue(): string | undefined {
|
||||
return this._history.next();
|
||||
return this.#history.next();
|
||||
}
|
||||
|
||||
isAtEnd(): boolean {
|
||||
return this._history.isAtEnd();
|
||||
return this.#history.isAtEnd();
|
||||
}
|
||||
|
||||
replaceLast(value: string): void {
|
||||
this._history.replaceLast(value);
|
||||
this.#history.replaceLast(value);
|
||||
}
|
||||
|
||||
resetCursor(): void {
|
||||
this._history.resetCursor();
|
||||
this.#history.resetCursor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js
|
||||
|
||||
export class InlineChatNotebookContribution {
|
||||
|
||||
private readonly _store = new DisposableStore();
|
||||
readonly #store = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
@IInlineChatSessionService sessionService: IInlineChatSessionService,
|
||||
@@ -22,7 +22,7 @@ export class InlineChatNotebookContribution {
|
||||
@INotebookEditorService notebookEditorService: INotebookEditorService,
|
||||
) {
|
||||
|
||||
this._store.add(sessionService.onWillStartSession(newSessionEditor => {
|
||||
this.#store.add(sessionService.onWillStartSession(newSessionEditor => {
|
||||
const candidate = CellUri.parse(newSessionEditor.getModel().uri);
|
||||
if (!candidate) {
|
||||
return;
|
||||
@@ -51,6 +51,6 @@ export class InlineChatNotebookContribution {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._store.dispose();
|
||||
this.#store.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,52 +50,58 @@ import { IInlineChatHistoryService } from './inlineChatHistoryService.js';
|
||||
*/
|
||||
export class InlineChatInputWidget extends Disposable {
|
||||
|
||||
private readonly _domNode: HTMLElement;
|
||||
private readonly _container: HTMLElement;
|
||||
private readonly _inputContainer: HTMLElement;
|
||||
private readonly _toolbarContainer: HTMLElement;
|
||||
private readonly _input: IActiveCodeEditor;
|
||||
private readonly _position = observableValue<IOverlayWidgetPosition | null>(this, null);
|
||||
readonly position: IObservable<IOverlayWidgetPosition | null> = this._position;
|
||||
readonly #domNode: HTMLElement;
|
||||
readonly #container: HTMLElement;
|
||||
readonly #inputContainer: HTMLElement;
|
||||
readonly #toolbarContainer: HTMLElement;
|
||||
readonly #input: IActiveCodeEditor;
|
||||
readonly #position = observableValue<IOverlayWidgetPosition | null>(this, null);
|
||||
readonly position: IObservable<IOverlayWidgetPosition | null> = this.#position;
|
||||
|
||||
private readonly _showStore = this._store.add(new DisposableStore());
|
||||
private readonly _stickyScrollHeight: IObservable<number>;
|
||||
private readonly _layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>;
|
||||
private _anchorLineNumber: number = 0;
|
||||
private _anchorLeft: number = 0;
|
||||
private _anchorAbove: boolean = false;
|
||||
readonly #showStore = this._store.add(new DisposableStore());
|
||||
readonly #stickyScrollHeight: IObservable<number>;
|
||||
readonly #layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>;
|
||||
#anchorLineNumber: number = 0;
|
||||
#anchorLeft: number = 0;
|
||||
#anchorAbove: boolean = false;
|
||||
|
||||
readonly #editorObs: ObservableCodeEditor;
|
||||
readonly #historyService: IInlineChatHistoryService;
|
||||
|
||||
constructor(
|
||||
private readonly _editorObs: ObservableCodeEditor,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IMenuService private readonly _menuService: IMenuService,
|
||||
editorObs: ObservableCodeEditor,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IMenuService menuService: IMenuService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IModelService modelService: IModelService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IInlineChatHistoryService private readonly _historyService: IInlineChatHistoryService,
|
||||
@IInlineChatHistoryService historyService: IInlineChatHistoryService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.#editorObs = editorObs;
|
||||
this.#historyService = historyService;
|
||||
|
||||
// Create container
|
||||
this._domNode = dom.$('.inline-chat-gutter-menu');
|
||||
this.#domNode = dom.$('.inline-chat-gutter-menu');
|
||||
|
||||
// Create inner container (background + focus border)
|
||||
this._container = dom.append(this._domNode, dom.$('.inline-chat-gutter-container'));
|
||||
this.#container = dom.append(this.#domNode, dom.$('.inline-chat-gutter-container'));
|
||||
|
||||
// Create input editor container
|
||||
this._inputContainer = dom.append(this._container, dom.$('.input'));
|
||||
this.#inputContainer = dom.append(this.#container, dom.$('.input'));
|
||||
|
||||
// Create toolbar container
|
||||
this._toolbarContainer = dom.append(this._container, dom.$('.toolbar'));
|
||||
this.#toolbarContainer = dom.append(this.#container, dom.$('.toolbar'));
|
||||
|
||||
// Create vertical actions bar below the input container
|
||||
const actionsContainer = dom.append(this._domNode, dom.$('.inline-chat-gutter-actions'));
|
||||
const actionsContainer = dom.append(this.#domNode, dom.$('.inline-chat-gutter-actions'));
|
||||
const actionBar = this._store.add(instantiationService.createInstance(WorkbenchActionBar, actionsContainer, {
|
||||
orientation: ActionsOrientation.VERTICAL,
|
||||
preventLoopNavigation: true,
|
||||
telemetrySource: 'inlineChatInput.actionBar',
|
||||
}));
|
||||
const actionsMenu = this._store.add(this._menuService.createMenu(MenuId.ChatEditorInlineMenu, this._contextKeyService));
|
||||
const actionsMenu = this._store.add(menuService.createMenu(MenuId.ChatEditorInlineMenu, contextKeyService));
|
||||
const updateActions = () => {
|
||||
const actions = getFlatActionBarActions(actionsMenu.getActions({ shouldForwardArgs: true }));
|
||||
actionBar.clear();
|
||||
@@ -130,13 +136,13 @@ export class InlineChatInputWidget extends Disposable {
|
||||
])
|
||||
};
|
||||
|
||||
this._input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this._inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor;
|
||||
this.#input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this.#inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor;
|
||||
|
||||
const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true));
|
||||
this._input.setModel(model);
|
||||
this.#input.setModel(model);
|
||||
|
||||
// Create toolbar
|
||||
const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._toolbarContainer, MenuId.InlineChatInput, {
|
||||
const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this.#toolbarContainer, MenuId.InlineChatInput, {
|
||||
telemetrySource: 'inlineChatInput.toolbar',
|
||||
hiddenItemStrategy: HiddenItemStrategy.NoHide,
|
||||
toolbarOptions: {
|
||||
@@ -146,8 +152,8 @@ export class InlineChatInputWidget extends Disposable {
|
||||
}));
|
||||
|
||||
// Initialize sticky scroll height observable
|
||||
const stickyScrollController = StickyScrollController.get(this._editorObs.editor);
|
||||
this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0);
|
||||
const stickyScrollController = StickyScrollController.get(this.#editorObs.editor);
|
||||
this.#stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0);
|
||||
|
||||
// Track toolbar width changes
|
||||
const toolbarWidth = observableValue<number>(this, 0);
|
||||
@@ -157,17 +163,17 @@ export class InlineChatInputWidget extends Disposable {
|
||||
this._store.add(resizeObserver);
|
||||
this._store.add(resizeObserver.observe(toolbar.getElement()));
|
||||
|
||||
const contentWidth = observableFromEvent(this, this._input.onDidChangeModelContent, () => this._input.getContentWidth());
|
||||
const contentHeight = observableFromEvent(this, this._input.onDidContentSizeChange, () => this._input.getContentHeight());
|
||||
const contentWidth = observableFromEvent(this, this.#input.onDidChangeModelContent, () => this.#input.getContentWidth());
|
||||
const contentHeight = observableFromEvent(this, this.#input.onDidContentSizeChange, () => this.#input.getContentHeight());
|
||||
|
||||
this._layoutData = derived(r => {
|
||||
this.#layoutData = derived(r => {
|
||||
const editorPad = 6;
|
||||
const totalWidth = contentWidth.read(r) + editorPad + toolbarWidth.read(r);
|
||||
const minWidth = 220;
|
||||
const maxWidth = 600;
|
||||
const midWidth = Math.round(maxWidth / 1.618);
|
||||
let clampedWidth: number;
|
||||
if (this._input.getOption(EditorOption.wordWrap) === 'on') {
|
||||
if (this.#input.getOption(EditorOption.wordWrap) === 'on') {
|
||||
clampedWidth = maxWidth;
|
||||
} else if (totalWidth <= minWidth) {
|
||||
clampedWidth = minWidth;
|
||||
@@ -177,12 +183,12 @@ export class InlineChatInputWidget extends Disposable {
|
||||
clampedWidth = maxWidth;
|
||||
}
|
||||
|
||||
const lineHeight = this._input.getOption(EditorOption.lineHeight);
|
||||
const lineHeight = this.#input.getOption(EditorOption.lineHeight);
|
||||
const clampedHeight = Math.min(contentHeight.read(r), (3 * lineHeight));
|
||||
|
||||
if (totalWidth > clampedWidth) {
|
||||
// enable word wrap
|
||||
this._input.updateOptions({ wordWrap: 'on', });
|
||||
this.#input.updateOptions({ wordWrap: 'on', });
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -195,55 +201,55 @@ export class InlineChatInputWidget extends Disposable {
|
||||
|
||||
// Update container width and editor layout when width changes
|
||||
this._store.add(autorun(r => {
|
||||
const { editorPad, toolbarWidth, totalWidth, height } = this._layoutData.read(r);
|
||||
const { editorPad, toolbarWidth, totalWidth, height } = this.#layoutData.read(r);
|
||||
|
||||
const inputWidth = totalWidth - toolbarWidth - editorPad;
|
||||
this._container.style.width = `${totalWidth}px`;
|
||||
this._inputContainer.style.width = `${inputWidth}px`;
|
||||
this._input.layout({ width: inputWidth, height });
|
||||
if (this._position.read(undefined) !== null) {
|
||||
this._updatePosition();
|
||||
this.#container.style.width = `${totalWidth}px`;
|
||||
this.#inputContainer.style.width = `${inputWidth}px`;
|
||||
this.#input.layout({ width: inputWidth, height });
|
||||
if (this.#position.read(undefined) !== null) {
|
||||
this.#updatePosition();
|
||||
}
|
||||
}));
|
||||
|
||||
// Toggle focus class on the container
|
||||
this._store.add(this._input.onDidFocusEditorText(() => this._container.classList.add('focused')));
|
||||
this._store.add(this._input.onDidBlurEditorText(() => this._container.classList.remove('focused')));
|
||||
this._store.add(this.#input.onDidFocusEditorText(() => this.#container.classList.add('focused')));
|
||||
this._store.add(this.#input.onDidBlurEditorText(() => this.#container.classList.remove('focused')));
|
||||
|
||||
// Toggle scroll decoration on the toolbar
|
||||
this._store.add(this._input.onDidScrollChange(e => {
|
||||
this._toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0);
|
||||
this._store.add(this.#input.onDidScrollChange(e => {
|
||||
this.#toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0);
|
||||
}));
|
||||
|
||||
|
||||
// Track input text for context key and adjust width based on content
|
||||
const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(this._contextKeyService);
|
||||
this._store.add(this._input.onDidChangeModelContent(() => {
|
||||
inputHasText.set(this._input.getModel().getValue().trim().length > 0);
|
||||
const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(contextKeyService);
|
||||
this._store.add(this.#input.onDidChangeModelContent(() => {
|
||||
inputHasText.set(this.#input.getModel().getValue().trim().length > 0);
|
||||
}));
|
||||
this._store.add(toDisposable(() => inputHasText.reset()));
|
||||
|
||||
// Track focus state
|
||||
const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(this._contextKeyService);
|
||||
this._store.add(this._input.onDidFocusEditorText(() => inputWidgetFocused.set(true)));
|
||||
this._store.add(this._input.onDidBlurEditorText(() => inputWidgetFocused.set(false)));
|
||||
const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(contextKeyService);
|
||||
this._store.add(this.#input.onDidFocusEditorText(() => inputWidgetFocused.set(true)));
|
||||
this._store.add(this.#input.onDidBlurEditorText(() => inputWidgetFocused.set(false)));
|
||||
this._store.add(toDisposable(() => inputWidgetFocused.reset()));
|
||||
|
||||
// Handle key events: ArrowUp/ArrowDown for history navigation and action bar focus
|
||||
this._store.add(this._input.onKeyDown(e => {
|
||||
this._store.add(this.#input.onKeyDown(e => {
|
||||
if (e.keyCode === KeyCode.UpArrow) {
|
||||
const position = this._input.getPosition();
|
||||
const position = this.#input.getPosition();
|
||||
if (position && position.lineNumber === 1) {
|
||||
this._showPreviousHistoryValue();
|
||||
this.#showPreviousHistoryValue();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
} else if (e.keyCode === KeyCode.DownArrow) {
|
||||
const model = this._input.getModel();
|
||||
const position = this._input.getPosition();
|
||||
const model = this.#input.getModel();
|
||||
const position = this.#input.getPosition();
|
||||
if (position && position.lineNumber === model.getLineCount()) {
|
||||
if (!this._historyService.isAtEnd()) {
|
||||
this._showNextHistoryValue();
|
||||
if (!this.#historyService.isAtEnd()) {
|
||||
this.#showNextHistoryValue();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else if (!actionBar.isEmpty()) {
|
||||
@@ -268,41 +274,41 @@ export class InlineChatInputWidget extends Disposable {
|
||||
if (firstItem?.element && dom.isAncestorOfActiveElement(firstItem.element)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this._input.focus();
|
||||
this.#input.focus();
|
||||
}
|
||||
}
|
||||
}, true));
|
||||
|
||||
// Track focus - hide when focus leaves
|
||||
const focusTracker = this._store.add(dom.trackFocus(this._domNode));
|
||||
const focusTracker = this._store.add(dom.trackFocus(this.#domNode));
|
||||
this._store.add(focusTracker.onDidBlur(() => this.hide()));
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this._input.getModel().getValue().trim();
|
||||
return this.#input.getModel().getValue().trim();
|
||||
}
|
||||
|
||||
addToHistory(value: string): void {
|
||||
this._historyService.addToHistory(value);
|
||||
this.#historyService.addToHistory(value);
|
||||
}
|
||||
|
||||
private _showPreviousHistoryValue(): void {
|
||||
if (this._historyService.isAtEnd()) {
|
||||
this._historyService.replaceLast(this._input.getModel().getValue());
|
||||
#showPreviousHistoryValue(): void {
|
||||
if (this.#historyService.isAtEnd()) {
|
||||
this.#historyService.replaceLast(this.#input.getModel().getValue());
|
||||
}
|
||||
const value = this._historyService.previousValue();
|
||||
const value = this.#historyService.previousValue();
|
||||
if (value !== undefined) {
|
||||
this._input.getModel().setValue(value);
|
||||
this.#input.getModel().setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
private _showNextHistoryValue(): void {
|
||||
if (this._historyService.isAtEnd()) {
|
||||
#showNextHistoryValue(): void {
|
||||
if (this.#historyService.isAtEnd()) {
|
||||
return;
|
||||
}
|
||||
const value = this._historyService.nextValue();
|
||||
const value = this.#historyService.nextValue();
|
||||
if (value !== undefined) {
|
||||
this._input.getModel().setValue(value);
|
||||
this.#input.getModel().setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,109 +319,105 @@ export class InlineChatInputWidget extends Disposable {
|
||||
* @param anchorAbove Whether to anchor above the position (widget grows upward)
|
||||
*/
|
||||
show(lineNumber: number, left: number, anchorAbove: boolean, placeholder: string, value?: string): void {
|
||||
this._showStore.clear();
|
||||
this.#showStore.clear();
|
||||
|
||||
// Reset history cursor to the end (current uncommitted text)
|
||||
this._historyService.resetCursor();
|
||||
this.#historyService.resetCursor();
|
||||
|
||||
// Clear input state
|
||||
this._input.updateOptions({ wordWrap: 'off', placeholder });
|
||||
this._input.getModel().setValue(value ?? '');
|
||||
this.#input.updateOptions({ wordWrap: 'off', placeholder });
|
||||
this.#input.getModel().setValue(value ?? '');
|
||||
|
||||
// Store anchor info for scroll updates
|
||||
this._anchorLineNumber = lineNumber;
|
||||
this._anchorLeft = left;
|
||||
this._anchorAbove = anchorAbove;
|
||||
this.#anchorLineNumber = lineNumber;
|
||||
this.#anchorLeft = left;
|
||||
this.#anchorAbove = anchorAbove;
|
||||
|
||||
// Set initial position
|
||||
this._updatePosition();
|
||||
this.#updatePosition();
|
||||
|
||||
// Create overlay widget via observable pattern
|
||||
this._showStore.add(this._editorObs.createOverlayWidget({
|
||||
domNode: this._domNode,
|
||||
position: this._position,
|
||||
this.#showStore.add(this.#editorObs.createOverlayWidget({
|
||||
domNode: this.#domNode,
|
||||
position: this.#position,
|
||||
minContentWidthInPx: constObservable(0),
|
||||
allowEditorOverflow: true,
|
||||
}));
|
||||
|
||||
// Re-adjust position after render to account for widget dimensions (offsetWidth/offsetHeight
|
||||
// are only available after the widget is added to the DOM)
|
||||
this._updatePosition();
|
||||
this.#updatePosition();
|
||||
|
||||
// Update position on scroll, hide if anchor line is out of view (only when input is empty)
|
||||
this._showStore.add(this._editorObs.editor.onDidScrollChange(() => {
|
||||
const visibleRanges = this._editorObs.editor.getVisibleRanges();
|
||||
this.#showStore.add(this.#editorObs.editor.onDidScrollChange(() => {
|
||||
const visibleRanges = this.#editorObs.editor.getVisibleRanges();
|
||||
const isLineVisible = visibleRanges.some(range =>
|
||||
this._anchorLineNumber >= range.startLineNumber && this._anchorLineNumber <= range.endLineNumber
|
||||
this.#anchorLineNumber >= range.startLineNumber && this.#anchorLineNumber <= range.endLineNumber
|
||||
);
|
||||
const hasContent = !!this._input.getModel().getValue();
|
||||
const hasContent = !!this.#input.getModel().getValue();
|
||||
if (!isLineVisible && !hasContent) {
|
||||
this.hide();
|
||||
} else {
|
||||
this._updatePosition();
|
||||
this.#updatePosition();
|
||||
}
|
||||
}));
|
||||
|
||||
// Update position when the editor resizes (e.g. sidebar toggle, window resize)
|
||||
this._showStore.add(this._editorObs.editor.onDidLayoutChange(() => {
|
||||
this._updatePosition();
|
||||
this.#showStore.add(this.#editorObs.editor.onDidLayoutChange(() => {
|
||||
this.#updatePosition();
|
||||
}));
|
||||
|
||||
// Focus the input editor
|
||||
setTimeout(() => {
|
||||
this._input.focus();
|
||||
this.#input.focus();
|
||||
if (value) {
|
||||
this._input.setSelection(this._input.getModel().getFullModelRange());
|
||||
this.#input.setSelection(this.#input.getModel().getFullModelRange());
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private _updatePosition(): void {
|
||||
const editor = this._editorObs.editor;
|
||||
#updatePosition(): void {
|
||||
const editor = this.#editorObs.editor;
|
||||
const lineHeight = editor.getOption(EditorOption.lineHeight);
|
||||
const top = editor.getTopForLineNumber(this._anchorLineNumber) - editor.getScrollTop();
|
||||
const top = editor.getTopForLineNumber(this.#anchorLineNumber) - editor.getScrollTop();
|
||||
let adjustedTop = top;
|
||||
|
||||
if (this._anchorAbove) {
|
||||
const widgetHeight = this._domNode.offsetHeight;
|
||||
if (this.#anchorAbove) {
|
||||
const widgetHeight = this.#domNode.offsetHeight;
|
||||
adjustedTop = top - widgetHeight;
|
||||
} else {
|
||||
adjustedTop = top + lineHeight;
|
||||
}
|
||||
|
||||
// Clamp to viewport bounds when anchor line is out of view
|
||||
const stickyScrollHeight = this._stickyScrollHeight.get();
|
||||
const stickyScrollHeight = this.#stickyScrollHeight.get();
|
||||
const layoutInfo = editor.getLayoutInfo();
|
||||
const widgetHeight = this._domNode.offsetHeight;
|
||||
const widgetWidth = this._domNode.offsetWidth;
|
||||
const widgetHeight = this.#domNode.offsetHeight;
|
||||
const widgetWidth = this.#domNode.offsetWidth;
|
||||
const minTop = stickyScrollHeight;
|
||||
const maxTop = layoutInfo.height - widgetHeight;
|
||||
const padding = 8;
|
||||
const maxLeft = layoutInfo.width - layoutInfo.verticalScrollbarWidth - layoutInfo.minimap.minimapWidth - widgetWidth - padding;
|
||||
|
||||
const clampedTop = Math.max(minTop, Math.min(adjustedTop, maxTop));
|
||||
const clampedLeft = Math.max(0, Math.min(this._anchorLeft, maxLeft));
|
||||
const isClamped = clampedTop !== adjustedTop || clampedLeft !== this._anchorLeft;
|
||||
this._domNode.classList.toggle('clamped', isClamped);
|
||||
const clampedLeft = Math.max(0, Math.min(this.#anchorLeft, maxLeft));
|
||||
const isClamped = clampedTop !== adjustedTop || clampedLeft !== this.#anchorLeft;
|
||||
this.#domNode.classList.toggle('clamped', isClamped);
|
||||
|
||||
this._position.set({
|
||||
this.#position.set({
|
||||
preference: { top: clampedTop, left: clampedLeft },
|
||||
stackOrdinal: 10000,
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the widget (removes from editor but does not dispose).
|
||||
*/
|
||||
hide(): void {
|
||||
// Focus editor if focus is still within the editor's DOM
|
||||
const editorDomNode = this._editorObs.editor.getDomNode();
|
||||
const editorDomNode = this.#editorObs.editor.getDomNode();
|
||||
if (editorDomNode && dom.isAncestorOfActiveElement(editorDomNode)) {
|
||||
this._editorObs.editor.focus();
|
||||
this.#editorObs.editor.focus();
|
||||
}
|
||||
this._position.set(null, undefined);
|
||||
this._input.getModel().setValue('');
|
||||
this._showStore.clear();
|
||||
this.#position.set(null, undefined);
|
||||
this.#input.getModel().setValue('');
|
||||
this.#showStore.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,73 +426,83 @@ export class InlineChatInputWidget extends Disposable {
|
||||
*/
|
||||
export class InlineChatSessionOverlayWidget extends Disposable {
|
||||
|
||||
private readonly _domNode: HTMLElement = document.createElement('div');
|
||||
private readonly _container: HTMLElement;
|
||||
private readonly _markdownContainer: HTMLElement;
|
||||
private readonly _markdownMessage: HTMLElement;
|
||||
private readonly _markdownScrollable: DomScrollableElement;
|
||||
private readonly _contentRow: HTMLElement;
|
||||
private readonly _statusNode: HTMLElement;
|
||||
private readonly _icon: HTMLElement;
|
||||
private readonly _message: HTMLElement;
|
||||
private readonly _toolbarNode: HTMLElement;
|
||||
readonly #domNode: HTMLElement = document.createElement('div');
|
||||
readonly #container: HTMLElement;
|
||||
readonly #markdownContainer: HTMLElement;
|
||||
readonly #markdownMessage: HTMLElement;
|
||||
readonly #markdownScrollable: DomScrollableElement;
|
||||
readonly #contentRow: HTMLElement;
|
||||
readonly #statusNode: HTMLElement;
|
||||
readonly #icon: HTMLElement;
|
||||
readonly #message: HTMLElement;
|
||||
readonly #toolbarNode: HTMLElement;
|
||||
|
||||
private readonly _showStore = this._store.add(new DisposableStore());
|
||||
private readonly _position = observableValue<IOverlayWidgetPosition | null>(this, null);
|
||||
private readonly _minContentWidthInPx = constObservable(0);
|
||||
readonly #showStore = this._store.add(new DisposableStore());
|
||||
readonly #position = observableValue<IOverlayWidgetPosition | null>(this, null);
|
||||
readonly #minContentWidthInPx = constObservable(0);
|
||||
|
||||
private readonly _stickyScrollHeight: IObservable<number>;
|
||||
readonly #stickyScrollHeight: IObservable<number>;
|
||||
|
||||
readonly #editorObs: ObservableCodeEditor;
|
||||
readonly #instaService: IInstantiationService;
|
||||
readonly #keybindingService: IKeybindingService;
|
||||
readonly #logService: ILogService;
|
||||
|
||||
constructor(
|
||||
private readonly _editorObs: ObservableCodeEditor,
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
editorObs: ObservableCodeEditor,
|
||||
@IInstantiationService instaService: IInstantiationService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@ILogService logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._domNode.classList.add('inline-chat-session-overlay-widget');
|
||||
this.#editorObs = editorObs;
|
||||
this.#instaService = instaService;
|
||||
this.#keybindingService = keybindingService;
|
||||
this.#logService = logService;
|
||||
|
||||
this._container = document.createElement('div');
|
||||
this._domNode.appendChild(this._container);
|
||||
this._container.classList.add('inline-chat-session-overlay-container');
|
||||
this.#domNode.classList.add('inline-chat-session-overlay-widget');
|
||||
|
||||
this._markdownContainer = document.createElement('div');
|
||||
this._markdownContainer.classList.add('markdown-scroll-container');
|
||||
this.#container = document.createElement('div');
|
||||
this.#domNode.appendChild(this.#container);
|
||||
this.#container.classList.add('inline-chat-session-overlay-container');
|
||||
|
||||
this._markdownMessage = document.createElement('div');
|
||||
this._markdownMessage.classList.add('markdown-message');
|
||||
this._markdownContainer.appendChild(this._markdownMessage);
|
||||
this._markdownScrollable = this._store.add(new DomScrollableElement(this._markdownContainer, {
|
||||
this.#markdownContainer = document.createElement('div');
|
||||
this.#markdownContainer.classList.add('markdown-scroll-container');
|
||||
|
||||
this.#markdownMessage = document.createElement('div');
|
||||
this.#markdownMessage.classList.add('markdown-message');
|
||||
this.#markdownContainer.appendChild(this.#markdownMessage);
|
||||
this.#markdownScrollable = this._store.add(new DomScrollableElement(this.#markdownContainer, {
|
||||
consumeMouseWheelIfScrollbarIsNeeded: true,
|
||||
horizontal: ScrollbarVisibility.Hidden,
|
||||
vertical: ScrollbarVisibility.Auto,
|
||||
}));
|
||||
this._container.appendChild(this._markdownScrollable.getDomNode());
|
||||
this.#container.appendChild(this.#markdownScrollable.getDomNode());
|
||||
|
||||
this._contentRow = document.createElement('div');
|
||||
this._contentRow.classList.add('content-row');
|
||||
this._container.appendChild(this._contentRow);
|
||||
this.#contentRow = document.createElement('div');
|
||||
this.#contentRow.classList.add('content-row');
|
||||
this.#container.appendChild(this.#contentRow);
|
||||
|
||||
// Create status node with icon and message
|
||||
this._statusNode = document.createElement('div');
|
||||
this._statusNode.classList.add('status');
|
||||
this._icon = dom.append(this._statusNode, dom.$('span'));
|
||||
this._message = dom.append(this._statusNode, dom.$('span.message'));
|
||||
this._contentRow.appendChild(this._statusNode);
|
||||
this.#statusNode = document.createElement('div');
|
||||
this.#statusNode.classList.add('status');
|
||||
this.#icon = dom.append(this.#statusNode, dom.$('span'));
|
||||
this.#message = dom.append(this.#statusNode, dom.$('span.message'));
|
||||
this.#contentRow.appendChild(this.#statusNode);
|
||||
|
||||
// Create toolbar node
|
||||
this._toolbarNode = document.createElement('div');
|
||||
this._toolbarNode.classList.add('toolbar');
|
||||
this.#toolbarNode = document.createElement('div');
|
||||
this.#toolbarNode.classList.add('toolbar');
|
||||
|
||||
// Initialize sticky scroll height observable
|
||||
const stickyScrollController = StickyScrollController.get(this._editorObs.editor);
|
||||
this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0);
|
||||
const stickyScrollController = StickyScrollController.get(this.#editorObs.editor);
|
||||
this.#stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0);
|
||||
}
|
||||
|
||||
show(session: IInlineChatSession2): void {
|
||||
assertType(this._editorObs.editor.hasModel());
|
||||
this._showStore.clear();
|
||||
assertType(this.#editorObs.editor.hasModel());
|
||||
this.#showStore.clear();
|
||||
|
||||
// Derived entry observable for this session
|
||||
const entry = derived(r => session.editingSession.readEntry(session.uri, r));
|
||||
@@ -558,71 +570,71 @@ export class InlineChatSessionOverlayWidget extends Disposable {
|
||||
}
|
||||
});
|
||||
|
||||
const markdownStore = this._showStore.add(new DisposableStore());
|
||||
const markdownStore = this.#showStore.add(new DisposableStore());
|
||||
|
||||
this._showStore.add(autorun(r => {
|
||||
this.#showStore.add(autorun(r => {
|
||||
const value = requestMessage.read(r);
|
||||
if (value) {
|
||||
if (value.message && value.icon) {
|
||||
this._message.innerText = renderAsPlaintext(value.message);
|
||||
this._icon.className = '';
|
||||
this._icon.classList.add(...ThemeIcon.asClassNameArray(value.icon));
|
||||
this._statusNode.classList.remove('hidden');
|
||||
this._contentRow.classList.remove('status-hidden');
|
||||
this.#message.innerText = renderAsPlaintext(value.message);
|
||||
this.#icon.className = '';
|
||||
this.#icon.classList.add(...ThemeIcon.asClassNameArray(value.icon));
|
||||
this.#statusNode.classList.remove('hidden');
|
||||
this.#contentRow.classList.remove('status-hidden');
|
||||
} else {
|
||||
this._message.innerText = '';
|
||||
this._icon.className = '';
|
||||
this._statusNode.classList.add('hidden');
|
||||
this._contentRow.classList.add('status-hidden');
|
||||
this.#message.innerText = '';
|
||||
this.#icon.className = '';
|
||||
this.#statusNode.classList.add('hidden');
|
||||
this.#contentRow.classList.add('status-hidden');
|
||||
}
|
||||
markdownStore.clear();
|
||||
this._markdownMessage.replaceChildren();
|
||||
this.#markdownMessage.replaceChildren();
|
||||
if (value.markdown) {
|
||||
this._markdownScrollable.getDomNode().classList.remove('hidden');
|
||||
this.#markdownScrollable.getDomNode().classList.remove('hidden');
|
||||
const markdown = typeof value.markdown === 'string' ? new MarkdownString(value.markdown) : value.markdown;
|
||||
const rendered = markdownStore.add(renderMarkdown(markdown));
|
||||
this._markdownMessage.appendChild(rendered.element);
|
||||
this._markdownScrollable.scanDomNode();
|
||||
this.#markdownMessage.appendChild(rendered.element);
|
||||
this.#markdownScrollable.scanDomNode();
|
||||
} else {
|
||||
this._markdownScrollable.getDomNode().classList.add('hidden');
|
||||
this.#markdownScrollable.getDomNode().classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
this._message.innerText = '';
|
||||
this._icon.className = '';
|
||||
this._statusNode.classList.add('hidden');
|
||||
this._contentRow.classList.add('status-hidden');
|
||||
this.#message.innerText = '';
|
||||
this.#icon.className = '';
|
||||
this.#statusNode.classList.add('hidden');
|
||||
this.#contentRow.classList.add('status-hidden');
|
||||
markdownStore.clear();
|
||||
this._markdownMessage.replaceChildren();
|
||||
this._markdownScrollable.getDomNode().classList.add('hidden');
|
||||
this.#markdownMessage.replaceChildren();
|
||||
this.#markdownScrollable.getDomNode().classList.add('hidden');
|
||||
}
|
||||
}));
|
||||
|
||||
// Log when pending confirmation changes
|
||||
this._showStore.add(autorun(r => {
|
||||
this.#showStore.add(autorun(r => {
|
||||
const response = session.chatModel.lastRequestObs.read(r)?.response;
|
||||
const pending = response?.isPendingConfirmation.read(r);
|
||||
if (pending) {
|
||||
this._logService.info(`[InlineChat] UNEXPECTED approval needed: ${pending.detail ?? 'unknown'}`);
|
||||
this.#logService.info(`[InlineChat] UNEXPECTED approval needed: ${pending.detail ?? 'unknown'}`);
|
||||
}
|
||||
}));
|
||||
|
||||
// Add toolbar
|
||||
this._contentRow.appendChild(this._toolbarNode);
|
||||
this._showStore.add(toDisposable(() => this._toolbarNode.remove()));
|
||||
this.#contentRow.appendChild(this.#toolbarNode);
|
||||
this.#showStore.add(toDisposable(() => this.#toolbarNode.remove()));
|
||||
|
||||
const that = this;
|
||||
|
||||
// Focus the owning editor before running any toolbar action so that
|
||||
// EditorAction2-based actions resolve the correct editor instance
|
||||
// even when the user has clicked into a different editor.
|
||||
const actionRunner = this._showStore.add(new class extends ActionRunner {
|
||||
const actionRunner = this.#showStore.add(new class extends ActionRunner {
|
||||
protected override async runAction(action: IAction, context?: unknown): Promise<void> {
|
||||
that._editorObs.editor.focus();
|
||||
that.#editorObs.editor.focus();
|
||||
return super.runAction(action, context);
|
||||
}
|
||||
});
|
||||
|
||||
this._showStore.add(this._instaService.createInstance(MenuWorkbenchToolBar, this._toolbarNode, MenuId.ChatEditorInlineExecute, {
|
||||
this.#showStore.add(this.#instaService.createInstance(MenuWorkbenchToolBar, this.#toolbarNode, MenuId.ChatEditorInlineExecute, {
|
||||
telemetrySource: 'inlineChatProgress.overlayToolbar',
|
||||
hiddenItemStrategy: HiddenItemStrategy.Ignore,
|
||||
actionRunner,
|
||||
@@ -639,56 +651,56 @@ export class InlineChatSessionOverlayWidget extends Disposable {
|
||||
return undefined; // use default action view item with label
|
||||
}
|
||||
|
||||
return new ChatEditingAcceptRejectActionViewItem(action, { ...options, keybinding: undefined }, entry, undefined, that._keybindingService, primaryActions);
|
||||
return new ChatEditingAcceptRejectActionViewItem(action, { ...options, keybinding: undefined }, entry, undefined, that.#keybindingService, primaryActions);
|
||||
}
|
||||
}));
|
||||
|
||||
// Position in top right of editor, below sticky scroll
|
||||
const lineHeight = this._editorObs.getOption(EditorOption.lineHeight);
|
||||
const lineHeight = this.#editorObs.getOption(EditorOption.lineHeight);
|
||||
|
||||
// Track widget width changes
|
||||
const widgetWidth = observableValue<number>(this, 0);
|
||||
const resizeObserver = new dom.DisposableResizeObserver(() => {
|
||||
widgetWidth.set(this._domNode.offsetWidth, undefined);
|
||||
widgetWidth.set(this.#domNode.offsetWidth, undefined);
|
||||
});
|
||||
this._showStore.add(resizeObserver);
|
||||
this._showStore.add(resizeObserver.observe(this._domNode));
|
||||
this.#showStore.add(resizeObserver);
|
||||
this.#showStore.add(resizeObserver.observe(this.#domNode));
|
||||
|
||||
this._showStore.add(autorun(r => {
|
||||
const layoutInfo = this._editorObs.layoutInfo.read(r);
|
||||
const stickyScrollHeight = this._stickyScrollHeight.read(r);
|
||||
this.#showStore.add(autorun(r => {
|
||||
const layoutInfo = this.#editorObs.layoutInfo.read(r);
|
||||
const stickyScrollHeight = this.#stickyScrollHeight.read(r);
|
||||
const width = widgetWidth.read(r);
|
||||
const padding = Math.round(lineHeight.read(r) * 2 / 3);
|
||||
|
||||
// Cap max-width to the editor viewport (content area)
|
||||
const maxWidth = Math.min(400, layoutInfo.contentWidth - 2 * padding);
|
||||
const maxHeight = Math.min(150, Math.floor(layoutInfo.height / 3));
|
||||
this._domNode.style.maxWidth = `${maxWidth}px`;
|
||||
this._markdownScrollable.getDomNode().style.maxHeight = `${maxHeight}px`;
|
||||
this._markdownContainer.style.maxHeight = `${maxHeight}px`;
|
||||
this._markdownScrollable.scanDomNode();
|
||||
this.#domNode.style.maxWidth = `${maxWidth}px`;
|
||||
this.#markdownScrollable.getDomNode().style.maxHeight = `${maxHeight}px`;
|
||||
this.#markdownContainer.style.maxHeight = `${maxHeight}px`;
|
||||
this.#markdownScrollable.scanDomNode();
|
||||
|
||||
// Position: top right, below sticky scroll with padding, left of minimap and scrollbar
|
||||
const top = stickyScrollHeight + padding;
|
||||
const left = layoutInfo.width - width - layoutInfo.verticalScrollbarWidth - layoutInfo.minimap.minimapWidth - padding;
|
||||
|
||||
this._position.set({
|
||||
this.#position.set({
|
||||
preference: { top, left },
|
||||
stackOrdinal: 10000,
|
||||
}, undefined);
|
||||
}));
|
||||
|
||||
// Create overlay widget
|
||||
this._showStore.add(this._editorObs.createOverlayWidget({
|
||||
domNode: this._domNode,
|
||||
position: this._position,
|
||||
minContentWidthInPx: this._minContentWidthInPx,
|
||||
this.#showStore.add(this.#editorObs.createOverlayWidget({
|
||||
domNode: this.#domNode,
|
||||
position: this.#position,
|
||||
minContentWidthInPx: this.#minContentWidthInPx,
|
||||
allowEditorOverflow: false,
|
||||
}));
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this._position.set(null, undefined);
|
||||
this._showStore.clear();
|
||||
this.#position.set(null, undefined);
|
||||
this.#showStore.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,56 +41,59 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
|
||||
declare _serviceBrand: undefined;
|
||||
|
||||
private readonly _store = new DisposableStore();
|
||||
private readonly _sessions = new ResourceMap<IInlineChatSession2>();
|
||||
readonly #store = new DisposableStore();
|
||||
readonly #sessions = new ResourceMap<IInlineChatSession2>();
|
||||
|
||||
private readonly _onWillStartSession = this._store.add(new Emitter<IActiveCodeEditor>());
|
||||
readonly onWillStartSession: Event<IActiveCodeEditor> = this._onWillStartSession.event;
|
||||
readonly #onWillStartSession = this.#store.add(new Emitter<IActiveCodeEditor>());
|
||||
readonly onWillStartSession: Event<IActiveCodeEditor> = this.#onWillStartSession.event;
|
||||
|
||||
private readonly _onDidChangeSessions = this._store.add(new Emitter<this>());
|
||||
readonly onDidChangeSessions: Event<this> = this._onDidChangeSessions.event;
|
||||
readonly #onDidChangeSessions = this.#store.add(new Emitter<this>());
|
||||
readonly onDidChangeSessions: Event<this> = this.#onDidChangeSessions.event;
|
||||
|
||||
readonly #chatService: IChatService;
|
||||
|
||||
constructor(
|
||||
@IChatService private readonly _chatService: IChatService,
|
||||
@IChatService chatService: IChatService,
|
||||
@IChatAgentService chatAgentService: IChatAgentService,
|
||||
) {
|
||||
this.#chatService = chatService;
|
||||
// Listen for agent changes and dispose all sessions when there is no agent
|
||||
const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline));
|
||||
this._store.add(autorun(r => {
|
||||
this.#store.add(autorun(r => {
|
||||
const agent = agentObs.read(r);
|
||||
if (!agent) {
|
||||
// No agent available, dispose all sessions
|
||||
dispose(this._sessions.values());
|
||||
this._sessions.clear();
|
||||
dispose(this.#sessions.values());
|
||||
this.#sessions.clear();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._store.dispose();
|
||||
this.#store.dispose();
|
||||
}
|
||||
|
||||
|
||||
createSession(editor: IActiveCodeEditor): IInlineChatSession2 {
|
||||
const uri = editor.getModel().uri;
|
||||
|
||||
if (this._sessions.has(uri)) {
|
||||
if (this.#sessions.has(uri)) {
|
||||
throw new Error('Session already exists');
|
||||
}
|
||||
|
||||
this._onWillStartSession.fire(editor);
|
||||
this.#onWillStartSession.fire(editor);
|
||||
|
||||
const chatModelRef = this._chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ });
|
||||
const chatModelRef = this.#chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ });
|
||||
const chatModel = chatModelRef.object;
|
||||
chatModel.startEditingSession(false);
|
||||
const terminationState = observableValue<InlineChatSessionTerminationState | undefined>(this, undefined);
|
||||
|
||||
const store = new DisposableStore();
|
||||
store.add(toDisposable(() => {
|
||||
void this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession');
|
||||
void this.#chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession');
|
||||
chatModel.editingSession?.reject();
|
||||
this._sessions.delete(uri);
|
||||
this._onDidChangeSessions.fire(this);
|
||||
this.#sessions.delete(uri);
|
||||
this.#onDidChangeSessions.fire(this);
|
||||
}));
|
||||
store.add(chatModelRef);
|
||||
|
||||
@@ -105,7 +108,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
if (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected) {
|
||||
const response = chatModel.getRequests().at(-1)?.response;
|
||||
if (response) {
|
||||
this._chatService.notifyUserAction({
|
||||
this.#chatService.notifyUserAction({
|
||||
sessionResource: response.session.sessionResource,
|
||||
requestId: response.requestId,
|
||||
agentId: response.agent?.id,
|
||||
@@ -140,20 +143,20 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
terminationState,
|
||||
setTerminationState: state => {
|
||||
terminationState.set(state, undefined);
|
||||
this._onDidChangeSessions.fire(this);
|
||||
this.#onDidChangeSessions.fire(this);
|
||||
},
|
||||
dispose: store.dispose.bind(store)
|
||||
};
|
||||
this._sessions.set(uri, result);
|
||||
this._onDidChangeSessions.fire(this);
|
||||
this.#sessions.set(uri, result);
|
||||
this.#onDidChangeSessions.fire(this);
|
||||
return result;
|
||||
}
|
||||
|
||||
getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined {
|
||||
let result = this._sessions.get(uri);
|
||||
let result = this.#sessions.get(uri);
|
||||
if (!result) {
|
||||
// no direct session, try to find an editing session which has a file entry for the uri
|
||||
for (const [_, candidate] of this._sessions) {
|
||||
for (const [_, candidate] of this.#sessions) {
|
||||
const entry = candidate.editingSession.getEntry(uri);
|
||||
if (entry) {
|
||||
result = candidate;
|
||||
@@ -165,7 +168,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
}
|
||||
|
||||
getSessionBySessionUri(sessionResource: URI): IInlineChatSession2 | undefined {
|
||||
for (const session of this._sessions.values()) {
|
||||
for (const session of this.#sessions.values()) {
|
||||
if (isEqual(session.chatModel.sessionResource, sessionResource)) {
|
||||
return session;
|
||||
}
|
||||
@@ -178,11 +181,11 @@ export class InlineChatEnabler {
|
||||
|
||||
static Id = 'inlineChat.enabler';
|
||||
|
||||
private readonly _ctxHasProvider2: IContextKey<boolean>;
|
||||
private readonly _ctxHasNotebookProvider: IContextKey<boolean>;
|
||||
private readonly _ctxPossible: IContextKey<boolean>;
|
||||
readonly #ctxHasProvider2: IContextKey<boolean>;
|
||||
readonly #ctxHasNotebookProvider: IContextKey<boolean>;
|
||||
readonly #ctxPossible: IContextKey<boolean>;
|
||||
|
||||
private readonly _store = new DisposableStore();
|
||||
readonly #store = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@@ -190,41 +193,41 @@ export class InlineChatEnabler {
|
||||
@IEditorService editorService: IEditorService,
|
||||
@IConfigurationService configService: IConfigurationService,
|
||||
) {
|
||||
this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService);
|
||||
this._ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService);
|
||||
this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService);
|
||||
this.#ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService);
|
||||
this.#ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService);
|
||||
this.#ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService);
|
||||
|
||||
const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline));
|
||||
const notebookAgentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Notebook));
|
||||
const notebookAgentConfigObs = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, configService);
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
this.#store.add(autorun(r => {
|
||||
const agent = agentObs.read(r);
|
||||
if (!agent) {
|
||||
this._ctxHasProvider2.reset();
|
||||
this.#ctxHasProvider2.reset();
|
||||
} else {
|
||||
this._ctxHasProvider2.set(true);
|
||||
this.#ctxHasProvider2.set(true);
|
||||
}
|
||||
}));
|
||||
|
||||
this._store.add(autorun(r => {
|
||||
this._ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r));
|
||||
this.#store.add(autorun(r => {
|
||||
this.#ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r));
|
||||
}));
|
||||
|
||||
const updateEditor = () => {
|
||||
const ctrl = editorService.activeEditorPane?.getControl();
|
||||
const isCodeEditorLike = isCodeEditor(ctrl) || isDiffEditor(ctrl) || isCompositeEditor(ctrl);
|
||||
this._ctxPossible.set(isCodeEditorLike);
|
||||
this.#ctxPossible.set(isCodeEditorLike);
|
||||
};
|
||||
|
||||
this._store.add(editorService.onDidActiveEditorChange(updateEditor));
|
||||
this.#store.add(editorService.onDidActiveEditorChange(updateEditor));
|
||||
updateEditor();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._ctxPossible.reset();
|
||||
this._ctxHasProvider2.reset();
|
||||
this._store.dispose();
|
||||
this.#ctxPossible.reset();
|
||||
this.#ctxHasProvider2.reset();
|
||||
this.#store.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +238,7 @@ export class InlineChatEscapeToolContribution extends Disposable {
|
||||
|
||||
static readonly DONT_ASK_AGAIN_KEY = 'inlineChat.dontAskMoveToPanelChat';
|
||||
|
||||
private static readonly _data: IToolData = {
|
||||
static readonly #data: IToolData = {
|
||||
id: 'inline_chat_exit',
|
||||
source: ToolDataSource.Internal,
|
||||
canBeReferencedInPrompt: false,
|
||||
@@ -269,7 +272,7 @@ export class InlineChatEscapeToolContribution extends Disposable {
|
||||
|
||||
super();
|
||||
|
||||
this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution._data, {
|
||||
this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution.#data, {
|
||||
invoke: async (invocation, _tokenCountFn, _progress, _token) => {
|
||||
|
||||
const sessionResource = invocation.context?.sessionResource;
|
||||
|
||||
@@ -93,38 +93,58 @@ export class InlineChatWidget {
|
||||
|
||||
protected readonly _store = new DisposableStore();
|
||||
|
||||
private readonly _ctxInputEditorFocused: IContextKey<boolean>;
|
||||
private readonly _ctxResponseFocused: IContextKey<boolean>;
|
||||
readonly #ctxInputEditorFocused: IContextKey<boolean>;
|
||||
readonly #ctxResponseFocused: IContextKey<boolean>;
|
||||
|
||||
private readonly _chatWidget: ChatWidget;
|
||||
readonly #chatWidget: ChatWidget;
|
||||
|
||||
protected readonly _onDidChangeHeight = this._store.add(new Emitter<void>());
|
||||
readonly onDidChangeHeight: Event<void> = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting);
|
||||
readonly onDidChangeHeight: Event<void> = Event.filter(this._onDidChangeHeight.event, _ => !this.#isLayouting);
|
||||
|
||||
private readonly _requestInProgress = observableValue(this, false);
|
||||
readonly requestInProgress: IObservable<boolean> = this._requestInProgress;
|
||||
readonly #requestInProgress = observableValue(this, false);
|
||||
readonly requestInProgress: IObservable<boolean> = this.#requestInProgress;
|
||||
|
||||
private _isLayouting: boolean = false;
|
||||
#isLayouting: boolean = false;
|
||||
|
||||
readonly scopedContextKeyService: IContextKeyService;
|
||||
|
||||
readonly #options: IInlineChatWidgetConstructionOptions;
|
||||
readonly #keybindingService: IKeybindingService;
|
||||
readonly #accessibilityService: IAccessibilityService;
|
||||
readonly #configurationService: IConfigurationService;
|
||||
readonly #accessibleViewService: IAccessibleViewService;
|
||||
readonly #modelService: IModelService;
|
||||
readonly #chatService: IChatService;
|
||||
readonly #chatEntitlementService: IChatEntitlementService;
|
||||
readonly #markdownRendererService: IMarkdownRendererService;
|
||||
|
||||
constructor(
|
||||
location: IChatWidgetLocationOptions,
|
||||
private readonly _options: IInlineChatWidgetConstructionOptions,
|
||||
options: IInlineChatWidgetConstructionOptions,
|
||||
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IAccessibilityService accessibilityService: IAccessibilityService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IAccessibleViewService accessibleViewService: IAccessibleViewService,
|
||||
@ITextModelService protected readonly _textModelResolverService: ITextModelService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IChatService private readonly _chatService: IChatService,
|
||||
@IHoverService private readonly _hoverService: IHoverService,
|
||||
@IChatEntitlementService private readonly _chatEntitlementService: IChatEntitlementService,
|
||||
@IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService,
|
||||
@IModelService modelService: IModelService,
|
||||
@IChatService chatService: IChatService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
@IChatEntitlementService chatEntitlementService: IChatEntitlementService,
|
||||
@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,
|
||||
) {
|
||||
this.scopedContextKeyService = this._store.add(_contextKeyService.createScoped(this._elements.chatWidget));
|
||||
this.#options = options;
|
||||
this.#keybindingService = keybindingService;
|
||||
this.#accessibilityService = accessibilityService;
|
||||
this.#configurationService = configurationService;
|
||||
this.#accessibleViewService = accessibleViewService;
|
||||
this.#modelService = modelService;
|
||||
this.#chatService = chatService;
|
||||
this.#chatEntitlementService = chatEntitlementService;
|
||||
this.#markdownRendererService = markdownRendererService;
|
||||
|
||||
this.scopedContextKeyService = this._store.add(contextKeyService.createScoped(this._elements.chatWidget));
|
||||
const scopedInstaService = _instantiationService.createChild(
|
||||
new ServiceCollection([
|
||||
IContextKeyService,
|
||||
@@ -133,7 +153,7 @@ export class InlineChatWidget {
|
||||
this._store
|
||||
);
|
||||
|
||||
this._chatWidget = scopedInstaService.createInstance(
|
||||
this.#chatWidget = scopedInstaService.createInstance(
|
||||
ChatWidget,
|
||||
location,
|
||||
{ isInlineChat: true },
|
||||
@@ -153,14 +173,14 @@ export class InlineChatWidget {
|
||||
if (emptyResponse) {
|
||||
return false;
|
||||
}
|
||||
if (item.response.value.every(item => item.kind === 'textEditGroup' && _options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) {
|
||||
if (item.response.value.every(item => item.kind === 'textEditGroup' && options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
dndContainer: this._elements.root,
|
||||
defaultMode: ChatMode.Ask,
|
||||
..._options.chatWidgetViewOptions
|
||||
...options.chatWidgetViewOptions
|
||||
},
|
||||
{
|
||||
listForeground: inlineChatForeground,
|
||||
@@ -170,11 +190,11 @@ export class InlineChatWidget {
|
||||
resultEditorBackground: editorBackground
|
||||
}
|
||||
);
|
||||
this._elements.root.classList.toggle('in-zone-widget', !!_options.inZoneWidget);
|
||||
this._chatWidget.render(this._elements.chatWidget);
|
||||
this._elements.root.classList.toggle('in-zone-widget', !!options.inZoneWidget);
|
||||
this.#chatWidget.render(this._elements.chatWidget);
|
||||
this._elements.chatWidget.style.setProperty(asCssVariableName(chatRequestBackground), asCssVariable(inlineChatBackground));
|
||||
this._chatWidget.setVisible(true);
|
||||
this._store.add(this._chatWidget);
|
||||
this.#chatWidget.setVisible(true);
|
||||
this._store.add(this.#chatWidget);
|
||||
|
||||
const ctxResponse = ChatContextKeys.isResponse.bindTo(this.scopedContextKeyService);
|
||||
const ctxResponseVote = ChatContextKeys.responseVote.bindTo(this.scopedContextKeyService);
|
||||
@@ -183,10 +203,10 @@ export class InlineChatWidget {
|
||||
const ctxResponseErrorFiltered = ChatContextKeys.responseIsFiltered.bindTo(this.scopedContextKeyService);
|
||||
|
||||
const viewModelStore = this._store.add(new DisposableStore());
|
||||
this._store.add(this._chatWidget.onDidChangeViewModel(() => {
|
||||
this._store.add(this.#chatWidget.onDidChangeViewModel(() => {
|
||||
viewModelStore.clear();
|
||||
|
||||
const viewModel = this._chatWidget.viewModel;
|
||||
const viewModel = this.#chatWidget.viewModel;
|
||||
if (!viewModel) {
|
||||
return;
|
||||
}
|
||||
@@ -202,7 +222,7 @@ export class InlineChatWidget {
|
||||
|
||||
viewModelStore.add(viewModel.onDidChange(() => {
|
||||
|
||||
this._requestInProgress.set(viewModel.model.requestInProgress.get(), undefined);
|
||||
this.#requestInProgress.set(viewModel.model.requestInProgress.get(), undefined);
|
||||
|
||||
const last = viewModel.getItems().at(-1);
|
||||
toolbar2.context = last;
|
||||
@@ -223,22 +243,22 @@ export class InlineChatWidget {
|
||||
}));
|
||||
|
||||
// context keys
|
||||
this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService);
|
||||
this.#ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(contextKeyService);
|
||||
const tracker = this._store.add(trackFocus(this.domNode));
|
||||
this._store.add(tracker.onDidBlur(() => this._ctxResponseFocused.set(false)));
|
||||
this._store.add(tracker.onDidFocus(() => this._ctxResponseFocused.set(true)));
|
||||
this._store.add(tracker.onDidBlur(() => this.#ctxResponseFocused.set(false)));
|
||||
this._store.add(tracker.onDidFocus(() => this.#ctxResponseFocused.set(true)));
|
||||
|
||||
this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(_contextKeyService);
|
||||
this._store.add(this._chatWidget.inputEditor.onDidFocusEditorWidget(() => this._ctxInputEditorFocused.set(true)));
|
||||
this._store.add(this._chatWidget.inputEditor.onDidBlurEditorWidget(() => this._ctxInputEditorFocused.set(false)));
|
||||
this.#ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(contextKeyService);
|
||||
this._store.add(this.#chatWidget.inputEditor.onDidFocusEditorWidget(() => this.#ctxInputEditorFocused.set(true)));
|
||||
this._store.add(this.#chatWidget.inputEditor.onDidBlurEditorWidget(() => this.#ctxInputEditorFocused.set(false)));
|
||||
|
||||
const statusMenuId = _options.statusMenuId instanceof MenuId ? _options.statusMenuId : _options.statusMenuId.menu;
|
||||
const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu;
|
||||
|
||||
// BUTTON bar
|
||||
const statusMenuOptions = _options.statusMenuId instanceof MenuId ? undefined : _options.statusMenuId.options;
|
||||
const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options;
|
||||
const statusButtonBar = scopedInstaService.createInstance(MenuWorkbenchButtonBar, this._elements.toolbar1, statusMenuId, {
|
||||
toolbarOptions: { primaryGroup: '0_main' },
|
||||
telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource,
|
||||
telemetrySource: options.chatWidgetViewOptions?.menus?.telemetrySource,
|
||||
menuOptions: { renderShortTitle: true },
|
||||
...statusMenuOptions,
|
||||
});
|
||||
@@ -246,8 +266,8 @@ export class InlineChatWidget {
|
||||
this._store.add(statusButtonBar);
|
||||
|
||||
// secondary toolbar
|
||||
const toolbar2 = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar2, _options.secondaryMenuId ?? MenuId.for(''), {
|
||||
telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource,
|
||||
const toolbar2 = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar2, options.secondaryMenuId ?? MenuId.for(''), {
|
||||
telemetrySource: options.chatWidgetViewOptions?.menus?.telemetrySource,
|
||||
menuOptions: { renderShortTitle: true, shouldForwardArgs: true },
|
||||
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
|
||||
return createActionViewItem(scopedInstaService, action, options);
|
||||
@@ -257,60 +277,60 @@ export class InlineChatWidget {
|
||||
this._store.add(toolbar2);
|
||||
|
||||
|
||||
this._store.add(this._configurationService.onDidChangeConfiguration(e => {
|
||||
this._store.add(this.#configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) {
|
||||
this._updateAriaLabel();
|
||||
this.#updateAriaLabel();
|
||||
}
|
||||
}));
|
||||
|
||||
this._elements.root.tabIndex = 0;
|
||||
this._elements.statusLabel.tabIndex = 0;
|
||||
this._updateAriaLabel();
|
||||
this._setupDisclaimer();
|
||||
this.#updateAriaLabel();
|
||||
this.#setupDisclaimer();
|
||||
|
||||
this._store.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => {
|
||||
this._store.add(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => {
|
||||
return this._elements.statusLabel.dataset['title'];
|
||||
}));
|
||||
|
||||
this._store.add(this._chatService.onDidPerformUserAction(e => {
|
||||
if (isEqual(e.sessionResource, this._chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') {
|
||||
this._store.add(this.#chatService.onDidPerformUserAction(e => {
|
||||
if (isEqual(e.sessionResource, this.#chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') {
|
||||
this.updateStatus(localize('feedbackThanks', "Thank you for your feedback!"), { resetAfter: 1250 });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _updateAriaLabel(): void {
|
||||
#updateAriaLabel(): void {
|
||||
|
||||
this._elements.root.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
|
||||
this._elements.root.ariaLabel = this.#accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
|
||||
|
||||
if (this._accessibilityService.isScreenReaderOptimized()) {
|
||||
if (this.#accessibilityService.isScreenReaderOptimized()) {
|
||||
let label = defaultAriaLabel;
|
||||
if (this._configurationService.getValue<boolean>(AccessibilityVerbositySettingId.InlineChat)) {
|
||||
const kbLabel = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel();
|
||||
if (this.#configurationService.getValue<boolean>(AccessibilityVerbositySettingId.InlineChat)) {
|
||||
const kbLabel = this.#keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel();
|
||||
label = kbLabel
|
||||
? localize('inlineChat.accessibilityHelp', "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", kbLabel)
|
||||
: localize('inlineChat.accessibilityHelpNoKb', "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information.");
|
||||
}
|
||||
this._chatWidget.inputEditor.updateOptions({ ariaLabel: label });
|
||||
this.#chatWidget.inputEditor.updateOptions({ ariaLabel: label });
|
||||
}
|
||||
}
|
||||
|
||||
private _setupDisclaimer(): void {
|
||||
#setupDisclaimer(): void {
|
||||
const disposables = this._store.add(new DisposableStore());
|
||||
|
||||
this._store.add(autorun(reader => {
|
||||
disposables.clear();
|
||||
reset(this._elements.disclaimerLabel);
|
||||
|
||||
const sentiment = this._chatEntitlementService.sentimentObs.read(reader);
|
||||
const anonymous = this._chatEntitlementService.anonymousObs.read(reader);
|
||||
const requestInProgress = this._chatService.requestInProgressObs.read(reader);
|
||||
const sentiment = this.#chatEntitlementService.sentimentObs.read(reader);
|
||||
const anonymous = this.#chatEntitlementService.anonymousObs.read(reader);
|
||||
const requestInProgress = this.#chatService.requestInProgressObs.read(reader);
|
||||
|
||||
const showDisclaimer = !sentiment.completed && anonymous && !requestInProgress;
|
||||
this._elements.disclaimerLabel.classList.toggle('hidden', !showDisclaimer);
|
||||
|
||||
if (showDisclaimer) {
|
||||
const renderedMarkdown = disposables.add(this._markdownRendererService.render(new MarkdownString(localize({ key: 'termsDisclaimer', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''), { isTrusted: true })));
|
||||
const renderedMarkdown = disposables.add(this.#markdownRendererService.render(new MarkdownString(localize({ key: 'termsDisclaimer', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''), { isTrusted: true })));
|
||||
this._elements.disclaimerLabel.appendChild(renderedMarkdown.element);
|
||||
}
|
||||
|
||||
@@ -327,20 +347,20 @@ export class InlineChatWidget {
|
||||
}
|
||||
|
||||
get chatWidget(): ChatWidget {
|
||||
return this._chatWidget;
|
||||
return this.#chatWidget;
|
||||
}
|
||||
|
||||
saveState() {
|
||||
this._chatWidget.saveState();
|
||||
this.#chatWidget.saveState();
|
||||
}
|
||||
|
||||
layout(widgetDim: Dimension) {
|
||||
const contentHeight = this.contentHeight;
|
||||
this._isLayouting = true;
|
||||
this.#isLayouting = true;
|
||||
try {
|
||||
this._doLayout(widgetDim);
|
||||
} finally {
|
||||
this._isLayouting = false;
|
||||
this.#isLayouting = false;
|
||||
|
||||
if (this.contentHeight !== contentHeight) {
|
||||
this._onDidChangeHeight.fire();
|
||||
@@ -357,7 +377,7 @@ export class InlineChatWidget {
|
||||
this._elements.root.style.height = `${dimension.height - extraHeight}px`;
|
||||
this._elements.root.style.width = `${dimension.width}px`;
|
||||
|
||||
this._chatWidget.layout(
|
||||
this.#chatWidget.layout(
|
||||
dimension.height - statusHeight - extraHeight,
|
||||
dimension.width
|
||||
);
|
||||
@@ -368,7 +388,7 @@ export class InlineChatWidget {
|
||||
*/
|
||||
get contentHeight(): number {
|
||||
const data = {
|
||||
chatWidgetContentHeight: this._chatWidget.contentHeight,
|
||||
chatWidgetContentHeight: this.#chatWidget.contentHeight,
|
||||
statusHeight: getTotalHeight(this._elements.status),
|
||||
extraHeight: this._getExtraHeight()
|
||||
};
|
||||
@@ -381,7 +401,7 @@ export class InlineChatWidget {
|
||||
// at least "maxWidgetHeight" high and at most the content height.
|
||||
|
||||
let maxWidgetOutputHeight = 100;
|
||||
for (const item of this._chatWidget.viewModel?.getItems() ?? []) {
|
||||
for (const item of this.#chatWidget.viewModel?.getItems() ?? []) {
|
||||
if (isResponseVM(item) && item.response.value.some(r => r.kind === 'textEditGroup' && !r.state?.applied)) {
|
||||
maxWidgetOutputHeight = 270;
|
||||
break;
|
||||
@@ -389,29 +409,29 @@ export class InlineChatWidget {
|
||||
}
|
||||
|
||||
let value = this.contentHeight;
|
||||
value -= this._chatWidget.contentHeight;
|
||||
value += Math.min(this._chatWidget.input.height.get() + maxWidgetOutputHeight, this._chatWidget.contentHeight);
|
||||
value -= this.#chatWidget.contentHeight;
|
||||
value += Math.min(this.#chatWidget.input.height.get() + maxWidgetOutputHeight, this.#chatWidget.contentHeight);
|
||||
return value;
|
||||
}
|
||||
|
||||
protected _getExtraHeight(): number {
|
||||
return this._options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/);
|
||||
return this.#options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/);
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this._chatWidget.getInput();
|
||||
return this.#chatWidget.getInput();
|
||||
}
|
||||
|
||||
set value(value: string) {
|
||||
this._chatWidget.setInput(value);
|
||||
this.#chatWidget.setInput(value);
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
this._chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));
|
||||
this.#chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));
|
||||
}
|
||||
|
||||
set placeholder(value: string) {
|
||||
this._chatWidget.setInputPlaceholder(value);
|
||||
this.#chatWidget.setInputPlaceholder(value);
|
||||
}
|
||||
|
||||
toggleStatus(show: boolean) {
|
||||
@@ -432,7 +452,7 @@ export class InlineChatWidget {
|
||||
}
|
||||
|
||||
async getCodeBlockInfo(codeBlockIndex: number): Promise<ITextModel | undefined> {
|
||||
const { viewModel } = this._chatWidget;
|
||||
const { viewModel } = this.#chatWidget;
|
||||
if (!viewModel) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -443,10 +463,10 @@ export class InlineChatWidget {
|
||||
}
|
||||
|
||||
// Look for the code block in the rendered response
|
||||
const codeBlocks = this._chatWidget.getCodeBlockInfosForResponse(item);
|
||||
const codeBlocks = this.#chatWidget.getCodeBlockInfosForResponse(item);
|
||||
const info = codeBlocks[codeBlockIndex];
|
||||
if (info?.uri) {
|
||||
return this._modelService.getModel(info.uri) ?? undefined;
|
||||
return this.#modelService.getModel(info.uri) ?? undefined;
|
||||
}
|
||||
|
||||
// Fallback: if the code block hasn't been rendered yet (e.g. due to
|
||||
@@ -471,25 +491,25 @@ export class InlineChatWidget {
|
||||
}
|
||||
|
||||
if (foundText !== undefined && currentCodeBlockIndex === codeBlockIndex) {
|
||||
return this._modelService.createModel(foundText, null, undefined, true);
|
||||
return this.#modelService.createModel(foundText, null, undefined, true);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get responseContent(): string | undefined {
|
||||
const requests = this._chatWidget.viewModel?.model.getRequests();
|
||||
const requests = this.#chatWidget.viewModel?.model.getRequests();
|
||||
return requests?.at(-1)?.response?.response.toString();
|
||||
}
|
||||
|
||||
|
||||
getChatModel(): IChatModel | undefined {
|
||||
return this._chatWidget.viewModel?.model;
|
||||
return this.#chatWidget.viewModel?.model;
|
||||
}
|
||||
|
||||
setChatModel(chatModel: IChatModel) {
|
||||
chatModel.inputModel.setState({ inputText: '', selections: [] });
|
||||
this._chatWidget.setModel(chatModel);
|
||||
this.#chatWidget.setModel(chatModel);
|
||||
}
|
||||
|
||||
updateInfo(message: string): void {
|
||||
@@ -528,8 +548,8 @@ export class InlineChatWidget {
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._chatWidget.attachmentModel.clear(true);
|
||||
this._chatWidget.saveState();
|
||||
this.#chatWidget.attachmentModel.clear(true);
|
||||
this.#chatWidget.saveState();
|
||||
|
||||
reset(this._elements.statusLabel);
|
||||
this._elements.statusLabel.classList.toggle('hidden', true);
|
||||
@@ -542,7 +562,7 @@ export class InlineChatWidget {
|
||||
}
|
||||
|
||||
focus() {
|
||||
this._chatWidget.focusInput();
|
||||
this.#chatWidget.focusInput();
|
||||
}
|
||||
|
||||
hasFocus() {
|
||||
|
||||
@@ -28,7 +28,7 @@ import { EditorBasedInlineChatWidget } from './inlineChatWidget.js';
|
||||
|
||||
export class InlineChatZoneWidget extends ZoneWidget {
|
||||
|
||||
private static readonly _options: IOptions = {
|
||||
static readonly #options: IOptions = {
|
||||
showFrame: true,
|
||||
frameWidth: 1,
|
||||
// frameColor: 'var(--vscode-inlineChat-border)',
|
||||
@@ -43,30 +43,34 @@ export class InlineChatZoneWidget extends ZoneWidget {
|
||||
|
||||
readonly widget: EditorBasedInlineChatWidget;
|
||||
|
||||
private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>;
|
||||
private _dimension?: Dimension;
|
||||
readonly #ctxCursorPosition: IContextKey<'above' | 'below' | ''>;
|
||||
#dimension?: Dimension;
|
||||
private notebookEditor?: INotebookEditor;
|
||||
|
||||
readonly #logService: ILogService;
|
||||
|
||||
constructor(
|
||||
location: IChatWidgetLocationOptions,
|
||||
options: IChatWidgetViewOptions | undefined,
|
||||
editors: { editor: ICodeEditor; notebookEditor?: INotebookEditor },
|
||||
/** @deprecated should go away with inline2 */
|
||||
clearDelegate: () => Promise<void>,
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
@ILogService private _logService: ILogService,
|
||||
@IInstantiationService instaService: IInstantiationService,
|
||||
@ILogService logService: ILogService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
) {
|
||||
super(editors.editor, InlineChatZoneWidget._options);
|
||||
super(editors.editor, InlineChatZoneWidget.#options);
|
||||
this.notebookEditor = editors.notebookEditor;
|
||||
|
||||
this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService);
|
||||
this.#logService = logService;
|
||||
|
||||
this.#ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService);
|
||||
|
||||
this._disposables.add(toDisposable(() => {
|
||||
this._ctxCursorPosition.reset();
|
||||
this.#ctxCursorPosition.reset();
|
||||
}));
|
||||
|
||||
this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, {
|
||||
this.widget = instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, {
|
||||
statusMenuId: {
|
||||
menu: MENU_INLINE_CHAT_WIDGET_STATUS,
|
||||
options: {
|
||||
@@ -105,14 +109,14 @@ export class InlineChatZoneWidget extends ZoneWidget {
|
||||
let revealFn: (() => void) | undefined;
|
||||
this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => {
|
||||
if (this.position) {
|
||||
revealFn = this._createZoneAndScrollRestoreFn(this.position);
|
||||
revealFn = this.#createZoneAndScrollRestoreFn(this.position);
|
||||
}
|
||||
}));
|
||||
this._disposables.add(this.widget.onDidChangeHeight(() => {
|
||||
if (this.position && !this._usesResizeHeight) {
|
||||
// only relayout when visible
|
||||
revealFn ??= this._createZoneAndScrollRestoreFn(this.position);
|
||||
const height = this._computeHeight();
|
||||
revealFn ??= this.#createZoneAndScrollRestoreFn(this.position);
|
||||
const height = this.#computeHeight();
|
||||
this._relayout(height.linesValue);
|
||||
revealFn?.();
|
||||
revealFn = undefined;
|
||||
@@ -136,13 +140,13 @@ export class InlineChatZoneWidget extends ZoneWidget {
|
||||
// todo@jrieken listen ONLY when showing
|
||||
const updateCursorIsAboveContextKey = () => {
|
||||
if (!this.position || !this.editor.hasModel()) {
|
||||
this._ctxCursorPosition.reset();
|
||||
this.#ctxCursorPosition.reset();
|
||||
} else if (this.position.lineNumber === this.editor.getPosition().lineNumber) {
|
||||
this._ctxCursorPosition.set('above');
|
||||
this.#ctxCursorPosition.set('above');
|
||||
} else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) {
|
||||
this._ctxCursorPosition.set('below');
|
||||
this.#ctxCursorPosition.set('below');
|
||||
} else {
|
||||
this._ctxCursorPosition.reset();
|
||||
this.#ctxCursorPosition.reset();
|
||||
}
|
||||
};
|
||||
this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey()));
|
||||
@@ -159,17 +163,17 @@ export class InlineChatZoneWidget extends ZoneWidget {
|
||||
|
||||
protected override _doLayout(heightInPixel: number): void {
|
||||
|
||||
this._updatePadding();
|
||||
this.#updatePadding();
|
||||
|
||||
const info = this.editor.getLayoutInfo();
|
||||
const width = info.contentWidth - info.verticalScrollbarWidth;
|
||||
// width = Math.min(850, width);
|
||||
|
||||
this._dimension = new Dimension(width, heightInPixel);
|
||||
this.widget.layout(this._dimension);
|
||||
this.#dimension = new Dimension(width, heightInPixel);
|
||||
this.widget.layout(this.#dimension);
|
||||
}
|
||||
|
||||
private _computeHeight(): { linesValue: number; pixelsValue: number } {
|
||||
#computeHeight(): { linesValue: number; pixelsValue: number } {
|
||||
const chatContentHeight = this.widget.contentHeight;
|
||||
const editorHeight = this.notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height;
|
||||
|
||||
@@ -192,25 +196,25 @@ export class InlineChatZoneWidget extends ZoneWidget {
|
||||
}
|
||||
|
||||
protected override _onWidth(_widthInPixel: number): void {
|
||||
if (this._dimension) {
|
||||
this._doLayout(this._dimension.height);
|
||||
if (this.#dimension) {
|
||||
this._doLayout(this.#dimension.height);
|
||||
}
|
||||
}
|
||||
|
||||
override show(position: Position): void {
|
||||
assertType(this.container);
|
||||
|
||||
this._updatePadding();
|
||||
this.#updatePadding();
|
||||
|
||||
const revealZone = this._createZoneAndScrollRestoreFn(position);
|
||||
super.show(position, this._computeHeight().linesValue);
|
||||
const revealZone = this.#createZoneAndScrollRestoreFn(position);
|
||||
super.show(position, this.#computeHeight().linesValue);
|
||||
this.widget.chatWidget.setVisible(true);
|
||||
this.widget.focus();
|
||||
|
||||
revealZone();
|
||||
}
|
||||
|
||||
private _updatePadding() {
|
||||
#updatePadding() {
|
||||
assertType(this.container);
|
||||
|
||||
const info = this.editor.getLayoutInfo();
|
||||
@@ -226,12 +230,12 @@ export class InlineChatZoneWidget extends ZoneWidget {
|
||||
}
|
||||
|
||||
override updatePositionAndHeight(position: Position): void {
|
||||
const revealZone = this._createZoneAndScrollRestoreFn(position);
|
||||
super.updatePositionAndHeight(position, !this._usesResizeHeight ? this._computeHeight().linesValue : undefined);
|
||||
const revealZone = this.#createZoneAndScrollRestoreFn(position);
|
||||
super.updatePositionAndHeight(position, !this._usesResizeHeight ? this.#computeHeight().linesValue : undefined);
|
||||
revealZone();
|
||||
}
|
||||
|
||||
private _createZoneAndScrollRestoreFn(position: Position): () => void {
|
||||
#createZoneAndScrollRestoreFn(position: Position): () => void {
|
||||
|
||||
const scrollState = StableEditorBottomScrollState.capture(this.editor);
|
||||
|
||||
@@ -242,7 +246,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
|
||||
|
||||
const scrollTop = this.editor.getScrollTop();
|
||||
const lineTop = this.editor.getTopForLineNumber(lineNumber);
|
||||
const zoneTop = lineTop - this._computeHeight().pixelsValue;
|
||||
const zoneTop = lineTop - this.#computeHeight().pixelsValue;
|
||||
const editorHeight = this.editor.getLayoutInfo().height;
|
||||
const lineBottom = this.editor.getBottomForLineNumber(lineNumber);
|
||||
|
||||
@@ -257,7 +261,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
|
||||
}
|
||||
|
||||
if (newScrollTop < scrollTop || forceScrollTop) {
|
||||
this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop });
|
||||
this.#logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop });
|
||||
this.editor.setScrollTop(newScrollTop, ScrollType.Immediate);
|
||||
}
|
||||
};
|
||||
@@ -269,7 +273,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
|
||||
|
||||
override hide(): void {
|
||||
const scrollState = StableEditorBottomScrollState.capture(this.editor);
|
||||
this._ctxCursorPosition.reset();
|
||||
this.#ctxCursorPosition.reset();
|
||||
this.widget.chatWidget.setVisible(false);
|
||||
super.hide();
|
||||
aria.status(localize('inlineChatClosed', 'Closed inline chat widget'));
|
||||
|
||||
Reference in New Issue
Block a user