diff --git a/src/vs/workbench/contrib/chat/browser/speechToText/dictationSession.ts b/src/vs/workbench/contrib/chat/browser/speechToText/dictationSession.ts index cba42089948de..1bfe708fe1cf3 100644 --- a/src/vs/workbench/contrib/chat/browser/speechToText/dictationSession.ts +++ b/src/vs/workbench/contrib/chat/browser/speechToText/dictationSession.ts @@ -362,9 +362,8 @@ export async function startDictation(service: IChatSpeechToTextService, editor: // prepared on first use (downloading/loading, which can take a while), show // a "Preparing…/Downloading… X%" placeholder instead so the user knows why // dictation has not started yet rather than staring at an idle editor. The - // placeholder must not appear during microphone acquisition. It remains - // visible until transcript text is inserted, and is restored to its - // previous value when the session ends. + // previous placeholder remains visible during microphone acquisition, then + // is restored when the session ends. const previousPlaceholder = editor.getOption(EditorOption.placeholder); const listeningPlaceholder = localize('chatStt.listening', "Listening…"); // The placeholder we last applied (listening or a preparing label), so we diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPlaceholderRotation.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPlaceholderRotation.ts index 43f99690f4e84..6564465e0a7cb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPlaceholderRotation.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPlaceholderRotation.ts @@ -63,17 +63,27 @@ export function installRotatingChatPlaceholder(editor: ICodeEditor, options?: IR const store = new DisposableStore(); const placeholderContribution = PlaceholderTextContribution.get(editor); - placeholderContribution?.setAnimateTransitions(true); store.add(toDisposable(() => PlaceholderTextContribution.get(editor)?.setAnimateTransitions(false))); const currentPlaceholder = editor.getOption(EditorOption.placeholder); + let expectedPlaceholder = currentPlaceholder; let index = placeholders.indexOf(currentPlaceholder); if (index === -1) { index = Math.floor(Math.random() * placeholders.length); } store.add(disposableWindowInterval(getWindow(editor.getDomNode()), () => { + if (editor.getOption(EditorOption.placeholder) !== expectedPlaceholder) { + return; + } + index = (index + 1) % placeholders.length; - editor.updateOptions({ placeholder: placeholders[index] }); + expectedPlaceholder = placeholders[index]; + placeholderContribution?.setAnimateTransitions(true); + try { + editor.updateOptions({ placeholder: expectedPlaceholder }); + } finally { + placeholderContribution?.setAnimateTransitions(false); + } }, intervalMs)); return store; diff --git a/src/vs/workbench/contrib/chat/test/browser/chatInputPlaceholderRotation.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatInputPlaceholderRotation.test.ts new file mode 100644 index 0000000000000..c8dabe89c0d0d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatInputPlaceholderRotation.test.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { timeout } from '../../../../../base/common/async.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { EditorOption } from '../../../../../editor/common/config/editorOptions.js'; +import { createTestCodeEditor } from '../../../../../editor/test/browser/testCodeEditor.js'; +import { createTextModel } from '../../../../../editor/test/common/testTextModel.js'; +import { installRotatingChatPlaceholder } from '../../browser/widget/input/chatInputPlaceholderRotation.js'; + +suite('ChatInputPlaceholderRotation', () => { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('does not overwrite a placeholder set by another feature', async () => { + const model = store.add(createTextModel('')); + const editor = store.add(createTestCodeEditor(model, { placeholder: 'First prompt' })); + store.add(installRotatingChatPlaceholder(editor, { + placeholders: ['First prompt', 'Second prompt'], + intervalMs: 5, + })); + + editor.updateOptions({ placeholder: 'Listening\u2026' }); + await timeout(20); + + assert.strictEqual(editor.getOption(EditorOption.placeholder), 'Listening\u2026'); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/dictationSession.test.ts b/src/vs/workbench/contrib/chat/test/browser/dictationSession.test.ts index b7b0976b50248..87a2c9854eea2 100644 --- a/src/vs/workbench/contrib/chat/test/browser/dictationSession.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/dictationSession.test.ts @@ -7,6 +7,7 @@ import assert from 'assert'; import { mainWindow } from '../../../../../base/browser/window.js'; import { Emitter } from '../../../../../base/common/event.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { EditorOption } from '../../../../../editor/common/config/editorOptions.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { createTestCodeEditor } from '../../../../../editor/test/browser/testCodeEditor.js'; import { createTextModel } from '../../../../../editor/test/common/testTextModel.js'; @@ -22,19 +23,25 @@ suite('DictationSession', () => { const transcript = 'hello world'; const onDidUpdateTranscript = store.add(new Emitter()); const onDidChangeState = store.add(new Emitter()); + const onDidChangePreparingModel = store.add(new Emitter()); let state = ChatSpeechToTextState.Idle; + let isPreparingModel = false; + let placeholderDuringStart: string | undefined; const service: IChatSpeechToTextService = { _serviceBrand: undefined, onDidUpdateTranscript: onDidUpdateTranscript.event, onDidChangeState: onDidChangeState.event, - onDidChangePreparingModel: store.add(new Emitter()).event, + onDidChangePreparingModel: onDidChangePreparingModel.event, onDidChangeModelDownloadProgress: store.add(new Emitter()).event, get state() { return state; }, get isConfigured() { return true; }, - get isPreparingModel() { return false; }, - get modelDownloadProgress() { return undefined; }, - get currentBackend() { return 'mai' as const; }, + get isPreparingModel() { return isPreparingModel; }, + get modelDownloadProgress() { return 0.5; }, + get currentBackend() { return 'local' as const; }, async start() { + placeholderDuringStart = editor.getOption(EditorOption.placeholder); + isPreparingModel = true; + onDidChangePreparingModel.fire(true); state = ChatSpeechToTextState.Recording; onDidChangeState.fire(state); }, @@ -47,13 +54,29 @@ suite('DictationSession', () => { logDictationAccuracy() { }, }; const model = store.add(createTextModel('')); - const editor = store.add(createTestCodeEditor(model)); + const editor = store.add(createTestCodeEditor(model, { placeholder: 'Ask a question' })); await startDictation(service, editor, mainWindow, new NullLogService()); + const downloadingPlaceholder = editor.getOption(EditorOption.placeholder); + isPreparingModel = false; + onDidChangePreparingModel.fire(false); + const listeningPlaceholder = editor.getOption(EditorOption.placeholder); onDidUpdateTranscript.fire({ text: transcript, finalizedText: '' }); editor.executeEdits('test', [{ range: new Range(1, 1, 1, transcript.length + 1), text: '' }]); await stopDictation(); - assert.strictEqual(editor.getValue(), ''); + assert.deepStrictEqual({ + placeholderDuringStart, + downloadingPlaceholder, + listeningPlaceholder, + placeholderAfterStop: editor.getOption(EditorOption.placeholder), + value: editor.getValue(), + }, { + placeholderDuringStart: 'Ask a question', + downloadingPlaceholder: 'Downloading speech-to-text model\u2026 50%', + listeningPlaceholder: 'Listening\u2026', + placeholderAfterStop: 'Ask a question', + value: '', + }); }); });