Path: blob/main/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { addDisposableListener } from '../../../base/browser/dom.js';6import { CachedFunction } from '../../../base/common/cache.js';7import { getStructuralKey } from '../../../base/common/equals.js';8import { Event, IValueWithChangeEvent } from '../../../base/common/event.js';9import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';10import { FileAccess } from '../../../base/common/network.js';11import { derived, observableFromEvent, ValueWithChangeEventFromObservable } from '../../../base/common/observable.js';12import { localize } from '../../../nls.js';13import { IAccessibilityService } from '../../accessibility/common/accessibility.js';14import { IConfigurationService } from '../../configuration/common/configuration.js';15import { createDecorator } from '../../instantiation/common/instantiation.js';16import { observableConfigValue } from '../../observable/common/platformObservableUtils.js';17import { ITelemetryService } from '../../telemetry/common/telemetry.js';1819export const IAccessibilitySignalService = createDecorator<IAccessibilitySignalService>('accessibilitySignalService');2021export interface IAccessibilitySignalService {22readonly _serviceBrand: undefined;23playSignal(signal: AccessibilitySignal, options?: IAccessbilitySignalOptions): Promise<void>;24playSignals(signals: (AccessibilitySignal | { signal: AccessibilitySignal; source: string })[]): Promise<void>;25playSignalLoop(signal: AccessibilitySignal, milliseconds: number): IDisposable;2627getEnabledState(signal: AccessibilitySignal, userGesture: boolean, modality?: AccessibilityModality | undefined): IValueWithChangeEvent<boolean>;28getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality, mode: 'line' | 'positional'): number;29/**30* Avoid this method and prefer `.playSignal`!31* Only use it when you want to play the sound regardless of enablement, e.g. in the settings quick pick.32*/33playSound(signal: Sound, allowManyInParallel: boolean, token: typeof AcknowledgeDocCommentsToken): Promise<void>;3435/** @deprecated Use getEnabledState(...).onChange */36isSoundEnabled(signal: AccessibilitySignal): boolean;37/** @deprecated Use getEnabledState(...).value */38isAnnouncementEnabled(signal: AccessibilitySignal): boolean;39/** @deprecated Use getEnabledState(...).onChange */40onSoundEnabledChanged(signal: AccessibilitySignal): Event<void>;41}4243/** Make sure you understand the doc comments of the method you want to call when using this token! */44export const AcknowledgeDocCommentsToken = Symbol('AcknowledgeDocCommentsToken');4546export type AccessibilityModality = 'sound' | 'announcement';4748export interface IAccessbilitySignalOptions {49allowManyInParallel?: boolean;5051modality?: AccessibilityModality;5253/**54* The source that triggered the signal (e.g. "diffEditor.cursorPositionChanged").55*/56source?: string;5758/**59* For actions like save or format, depending on the60* configured value, we will only61* play the sound if the user triggered the action.62*/63userGesture?: boolean;6465/**66* The custom message to alert with.67* This will override the default announcement message.68*/69customAlertMessage?: string;70}7172export class AccessibilitySignalService extends Disposable implements IAccessibilitySignalService {73readonly _serviceBrand: undefined;74private readonly sounds: Map<string, HTMLAudioElement>;75private readonly screenReaderAttached;76private readonly sentTelemetry;7778constructor(79@IConfigurationService private readonly configurationService: IConfigurationService,80@IAccessibilityService private readonly accessibilityService: IAccessibilityService,81@ITelemetryService private readonly telemetryService: ITelemetryService,82) {83super();84this.sounds = new Map();85this.screenReaderAttached = observableFromEvent(this,86this.accessibilityService.onDidChangeScreenReaderOptimized,87() => /** @description accessibilityService.onDidChangeScreenReaderOptimized */ this.accessibilityService.isScreenReaderOptimized()88);89this.sentTelemetry = new Set<string>();90this.playingSounds = new Set<Sound>();91this._signalConfigValue = new CachedFunction((signal: AccessibilitySignal) => observableConfigValue<{92sound: EnabledState;93announcement: EnabledState;94}>(signal.settingsKey, { sound: 'off', announcement: 'off' }, this.configurationService));95this._signalEnabledState = new CachedFunction(96{ getCacheKey: getStructuralKey },97(arg: { signal: AccessibilitySignal; userGesture: boolean; modality?: AccessibilityModality | undefined }) => {98return derived(reader => {99/** @description sound enabled */100const setting = this._signalConfigValue.get(arg.signal).read(reader);101102if (arg.modality === 'sound' || arg.modality === undefined) {103if (arg.signal.managesOwnEnablement || checkEnabledState(setting.sound, () => this.screenReaderAttached.read(reader), arg.userGesture)) {104return true;105}106}107if (arg.modality === 'announcement' || arg.modality === undefined) {108if (checkEnabledState(setting.announcement, () => this.screenReaderAttached.read(reader), arg.userGesture)) {109return true;110}111}112return false;113}).recomputeInitiallyAndOnChange(this._store);114}115);116}117118public getEnabledState(signal: AccessibilitySignal, userGesture: boolean, modality?: AccessibilityModality | undefined): IValueWithChangeEvent<boolean> {119return new ValueWithChangeEventFromObservable(this._signalEnabledState.get({ signal, userGesture, modality }));120}121122public async playSignal(signal: AccessibilitySignal, options: IAccessbilitySignalOptions = {}): Promise<void> {123const shouldPlayAnnouncement = options.modality === 'announcement' || options.modality === undefined;124const announcementMessage = options.customAlertMessage ?? signal.announcementMessage;125if (shouldPlayAnnouncement && this.isAnnouncementEnabled(signal, options.userGesture) && announcementMessage) {126this.accessibilityService.status(announcementMessage);127}128129const shouldPlaySound = options.modality === 'sound' || options.modality === undefined;130if (shouldPlaySound && this.isSoundEnabled(signal, options.userGesture)) {131this.sendSignalTelemetry(signal, options.source);132await this.playSound(signal.sound.getSound(), options.allowManyInParallel);133}134}135136public async playSignals(signals: (AccessibilitySignal | { signal: AccessibilitySignal; source: string })[]): Promise<void> {137for (const signal of signals) {138this.sendSignalTelemetry('signal' in signal ? signal.signal : signal, 'source' in signal ? signal.source : undefined);139}140const signalArray = signals.map(s => 'signal' in s ? s.signal : s);141const announcements = signalArray.filter(signal => this.isAnnouncementEnabled(signal)).map(s => s.announcementMessage);142if (announcements.length) {143this.accessibilityService.status(announcements.join(', '));144}145146// Some sounds are reused. Don't play the same sound twice.147const sounds = new Set(signalArray.filter(signal => this.isSoundEnabled(signal)).map(signal => signal.sound.getSound()));148await Promise.all(Array.from(sounds).map(sound => this.playSound(sound, true)));149150}151152153private sendSignalTelemetry(signal: AccessibilitySignal, source: string | undefined): void {154const isScreenReaderOptimized = this.accessibilityService.isScreenReaderOptimized();155const key = signal.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : '');156// Only send once per user session157if (this.sentTelemetry.has(key) || this.getVolumeInPercent() === 0) {158return;159}160this.sentTelemetry.add(key);161162this.telemetryService.publicLog2<{163signal: string;164source: string;165isScreenReaderOptimized: boolean;166}, {167owner: 'hediet';168169signal: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The signal that was played.' };170source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source that triggered the signal (e.g. "diffEditorNavigation").' };171isScreenReaderOptimized: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is using a screen reader' };172173comment: 'This data is collected to understand how signals are used and if more signals should be added.';174}>('signal.played', {175signal: signal.name,176source: source ?? '',177isScreenReaderOptimized,178});179}180181private getVolumeInPercent(): number {182const volume = this.configurationService.getValue<number>('accessibility.signalOptions.volume');183if (typeof volume !== 'number') {184return 50;185}186187return Math.max(Math.min(volume, 100), 0);188}189190private readonly playingSounds;191192public async playSound(sound: Sound, allowManyInParallel = false): Promise<void> {193if (!allowManyInParallel && this.playingSounds.has(sound)) {194return;195}196this.playingSounds.add(sound);197const url = FileAccess.asBrowserUri(`vs/platform/accessibilitySignal/browser/media/${sound.fileName}`).toString(true);198199try {200const sound = this.sounds.get(url);201if (sound) {202sound.volume = this.getVolumeInPercent() / 100;203sound.currentTime = 0;204await sound.play();205} else {206const playedSound = await playAudio(url, this.getVolumeInPercent() / 100);207this.sounds.set(url, playedSound);208}209} catch (e) {210if (!e.message.includes('play() can only be initiated by a user gesture')) {211// tracking this issue in #178642, no need to spam the console212console.error('Error while playing sound', e);213}214} finally {215this.playingSounds.delete(sound);216}217}218219public playSignalLoop(signal: AccessibilitySignal, milliseconds: number): IDisposable {220let playing = true;221const playSound = () => {222if (playing) {223this.playSignal(signal, { allowManyInParallel: true }).finally(() => {224setTimeout(() => {225if (playing) {226playSound();227}228}, milliseconds);229});230}231};232playSound();233return toDisposable(() => playing = false);234}235236private readonly _signalConfigValue;237238private readonly _signalEnabledState;239240public isAnnouncementEnabled(signal: AccessibilitySignal, userGesture?: boolean): boolean {241if (!signal.announcementMessage) {242return false;243}244return this._signalEnabledState.get({ signal, userGesture: !!userGesture, modality: 'announcement' }).get();245}246247public isSoundEnabled(signal: AccessibilitySignal, userGesture?: boolean): boolean {248return this._signalEnabledState.get({ signal, userGesture: !!userGesture, modality: 'sound' }).get();249}250251public onSoundEnabledChanged(signal: AccessibilitySignal): Event<void> {252return this.getEnabledState(signal, false).onDidChange;253}254255public getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality, mode: 'line' | 'positional'): number {256if (!this.configurationService.getValue('accessibility.signalOptions.debouncePositionChanges')) {257return 0;258}259let value: { sound: number; announcement: number };260if (signal.name === AccessibilitySignal.errorAtPosition.name && mode === 'positional') {261value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.errorAtPosition');262} else if (signal.name === AccessibilitySignal.warningAtPosition.name && mode === 'positional') {263value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.warningAtPosition');264} else {265value = this.configurationService.getValue('accessibility.signalOptions.experimental.delays.general');266}267return modality === 'sound' ? value.sound : value.announcement;268}269}270271type EnabledState = 'on' | 'off' | 'auto' | 'userGesture' | 'always' | 'never';272function checkEnabledState(state: EnabledState, getScreenReaderAttached: () => boolean, isTriggeredByUserGesture: boolean): boolean {273return state === 'on' || state === 'always' || (state === 'auto' && getScreenReaderAttached()) || state === 'userGesture' && isTriggeredByUserGesture;274}275276/**277* Play the given audio url.278* @volume value between 0 and 1279*/280async function playAudio(url: string, volume: number): Promise<HTMLAudioElement> {281const disposables = new DisposableStore();282try {283return await doPlayAudio(url, volume, disposables);284} finally {285disposables.dispose();286}287}288289function doPlayAudio(url: string, volume: number, disposables: DisposableStore): Promise<HTMLAudioElement> {290return new Promise<HTMLAudioElement>((resolve, reject) => {291const audio = new Audio(url);292audio.volume = volume;293disposables.add(addDisposableListener(audio, 'ended', () => {294resolve(audio);295}));296disposables.add(addDisposableListener(audio, 'error', (e) => {297// When the error event fires, ended might not be called298reject(e.error);299}));300audio.play().catch(e => {301// When play fails, the error event is not fired.302reject(e);303});304});305}306307/**308* Corresponds to the audio files in ./media.309*/310export class Sound {311private static register(options: { fileName: string }): Sound {312const sound = new Sound(options.fileName);313return sound;314}315316public static readonly error = Sound.register({ fileName: 'error.mp3' });317public static readonly warning = Sound.register({ fileName: 'warning.mp3' });318public static readonly success = Sound.register({ fileName: 'success.mp3' });319public static readonly foldedArea = Sound.register({ fileName: 'foldedAreas.mp3' });320public static readonly break = Sound.register({ fileName: 'break.mp3' });321public static readonly quickFixes = Sound.register({ fileName: 'quickFixes.mp3' });322public static readonly taskCompleted = Sound.register({ fileName: 'taskCompleted.mp3' });323public static readonly taskFailed = Sound.register({ fileName: 'taskFailed.mp3' });324public static readonly terminalBell = Sound.register({ fileName: 'terminalBell.mp3' });325public static readonly diffLineInserted = Sound.register({ fileName: 'diffLineInserted.mp3' });326public static readonly diffLineDeleted = Sound.register({ fileName: 'diffLineDeleted.mp3' });327public static readonly diffLineModified = Sound.register({ fileName: 'diffLineModified.mp3' });328public static readonly requestSent = Sound.register({ fileName: 'requestSent.mp3' });329public static readonly responseReceived1 = Sound.register({ fileName: 'responseReceived1.mp3' });330public static readonly responseReceived2 = Sound.register({ fileName: 'responseReceived2.mp3' });331public static readonly responseReceived3 = Sound.register({ fileName: 'responseReceived3.mp3' });332public static readonly responseReceived4 = Sound.register({ fileName: 'responseReceived4.mp3' });333public static readonly clear = Sound.register({ fileName: 'clear.mp3' });334public static readonly save = Sound.register({ fileName: 'save.mp3' });335public static readonly format = Sound.register({ fileName: 'format.mp3' });336public static readonly voiceRecordingStarted = Sound.register({ fileName: 'voiceRecordingStarted.mp3' });337public static readonly voiceRecordingStopped = Sound.register({ fileName: 'voiceRecordingStopped.mp3' });338public static readonly progress = Sound.register({ fileName: 'progress.mp3' });339public static readonly chatEditModifiedFile = Sound.register({ fileName: 'chatEditModifiedFile.mp3' });340public static readonly editsKept = Sound.register({ fileName: 'editsKept.mp3' });341public static readonly editsUndone = Sound.register({ fileName: 'editsUndone.mp3' });342public static readonly nextEditSuggestion = Sound.register({ fileName: 'nextEditSuggestion.mp3' });343public static readonly terminalCommandSucceeded = Sound.register({ fileName: 'terminalCommandSucceeded.mp3' });344public static readonly chatUserActionRequired = Sound.register({ fileName: 'chatUserActionRequired.mp3' });345public static readonly codeActionTriggered = Sound.register({ fileName: 'codeActionTriggered.mp3' });346public static readonly codeActionApplied = Sound.register({ fileName: 'codeActionApplied.mp3' });347348private constructor(public readonly fileName: string) { }349}350351export class SoundSource {352constructor(353public readonly randomOneOf: Sound[]354) { }355356public getSound(deterministic = false): Sound {357if (deterministic || this.randomOneOf.length === 1) {358return this.randomOneOf[0];359} else {360const index = Math.floor(Math.random() * this.randomOneOf.length);361return this.randomOneOf[index];362}363}364}365366export class AccessibilitySignal {367private constructor(368public readonly sound: SoundSource,369public readonly name: string,370public readonly legacySoundSettingsKey: string | undefined,371public readonly settingsKey: string,372public readonly legacyAnnouncementSettingsKey: string | undefined,373public readonly announcementMessage: string | undefined,374public readonly managesOwnEnablement: boolean = false375) { }376377private static _signals = new Set<AccessibilitySignal>();378private static register(options: {379name: string;380sound: Sound | {381/**382* Gaming and other apps often play a sound variant when the same event happens again383* for an improved experience. This option enables playing a random sound.384*/385randomOneOf: Sound[];386};387legacySoundSettingsKey?: string;388settingsKey: string;389legacyAnnouncementSettingsKey?: string;390announcementMessage?: string;391delaySettingsKey?: string;392managesOwnEnablement?: boolean;393}): AccessibilitySignal {394const soundSource = new SoundSource('randomOneOf' in options.sound ? options.sound.randomOneOf : [options.sound]);395const signal = new AccessibilitySignal(396soundSource,397options.name,398options.legacySoundSettingsKey,399options.settingsKey,400options.legacyAnnouncementSettingsKey,401options.announcementMessage,402options.managesOwnEnablement403);404AccessibilitySignal._signals.add(signal);405return signal;406}407408public static get allAccessibilitySignals() {409return [...this._signals];410}411412public static readonly errorAtPosition = AccessibilitySignal.register({413name: localize('accessibilitySignals.positionHasError.name', 'Error at Position'),414sound: Sound.error,415announcementMessage: localize('accessibility.signals.positionHasError', 'Error'),416settingsKey: 'accessibility.signals.positionHasError',417delaySettingsKey: 'accessibility.signalOptions.delays.errorAtPosition'418});419public static readonly warningAtPosition = AccessibilitySignal.register({420name: localize('accessibilitySignals.positionHasWarning.name', 'Warning at Position'),421sound: Sound.warning,422announcementMessage: localize('accessibility.signals.positionHasWarning', 'Warning'),423settingsKey: 'accessibility.signals.positionHasWarning',424delaySettingsKey: 'accessibility.signalOptions.delays.warningAtPosition'425});426427public static readonly errorOnLine = AccessibilitySignal.register({428name: localize('accessibilitySignals.lineHasError.name', 'Error on Line'),429sound: Sound.error,430legacySoundSettingsKey: 'audioCues.lineHasError',431legacyAnnouncementSettingsKey: 'accessibility.alert.error',432announcementMessage: localize('accessibility.signals.lineHasError', 'Error on Line'),433settingsKey: 'accessibility.signals.lineHasError',434});435436public static readonly warningOnLine = AccessibilitySignal.register({437name: localize('accessibilitySignals.lineHasWarning.name', 'Warning on Line'),438sound: Sound.warning,439legacySoundSettingsKey: 'audioCues.lineHasWarning',440legacyAnnouncementSettingsKey: 'accessibility.alert.warning',441announcementMessage: localize('accessibility.signals.lineHasWarning', 'Warning on Line'),442settingsKey: 'accessibility.signals.lineHasWarning',443});444public static readonly foldedArea = AccessibilitySignal.register({445name: localize('accessibilitySignals.lineHasFoldedArea.name', 'Folded Area on Line'),446sound: Sound.foldedArea,447legacySoundSettingsKey: 'audioCues.lineHasFoldedArea',448legacyAnnouncementSettingsKey: 'accessibility.alert.foldedArea',449announcementMessage: localize('accessibility.signals.lineHasFoldedArea', 'Folded'),450settingsKey: 'accessibility.signals.lineHasFoldedArea',451});452public static readonly break = AccessibilitySignal.register({453name: localize('accessibilitySignals.lineHasBreakpoint.name', 'Breakpoint on Line'),454sound: Sound.break,455legacySoundSettingsKey: 'audioCues.lineHasBreakpoint',456legacyAnnouncementSettingsKey: 'accessibility.alert.breakpoint',457announcementMessage: localize('accessibility.signals.lineHasBreakpoint', 'Breakpoint'),458settingsKey: 'accessibility.signals.lineHasBreakpoint',459});460public static readonly inlineSuggestion = AccessibilitySignal.register({461name: localize('accessibilitySignals.lineHasInlineSuggestion.name', 'Inline Suggestion on Line'),462sound: Sound.quickFixes,463legacySoundSettingsKey: 'audioCues.lineHasInlineSuggestion',464settingsKey: 'accessibility.signals.lineHasInlineSuggestion',465});466public static readonly nextEditSuggestion = AccessibilitySignal.register({467name: localize('accessibilitySignals.nextEditSuggestion.name', 'Next Edit Suggestion on Line'),468sound: Sound.nextEditSuggestion,469legacySoundSettingsKey: 'audioCues.nextEditSuggestion',470settingsKey: 'accessibility.signals.nextEditSuggestion',471announcementMessage: localize('accessibility.signals.nextEditSuggestion', 'Next Edit Suggestion'),472});473public static readonly terminalQuickFix = AccessibilitySignal.register({474name: localize('accessibilitySignals.terminalQuickFix.name', 'Terminal Quick Fix'),475sound: Sound.quickFixes,476legacySoundSettingsKey: 'audioCues.terminalQuickFix',477legacyAnnouncementSettingsKey: 'accessibility.alert.terminalQuickFix',478announcementMessage: localize('accessibility.signals.terminalQuickFix', 'Quick Fix'),479settingsKey: 'accessibility.signals.terminalQuickFix',480});481482public static readonly onDebugBreak = AccessibilitySignal.register({483name: localize('accessibilitySignals.onDebugBreak.name', 'Debugger Stopped on Breakpoint'),484sound: Sound.break,485legacySoundSettingsKey: 'audioCues.onDebugBreak',486legacyAnnouncementSettingsKey: 'accessibility.alert.onDebugBreak',487announcementMessage: localize('accessibility.signals.onDebugBreak', 'Breakpoint'),488settingsKey: 'accessibility.signals.onDebugBreak',489});490491public static readonly noInlayHints = AccessibilitySignal.register({492name: localize('accessibilitySignals.noInlayHints', 'No Inlay Hints on Line'),493sound: Sound.error,494legacySoundSettingsKey: 'audioCues.noInlayHints',495legacyAnnouncementSettingsKey: 'accessibility.alert.noInlayHints',496announcementMessage: localize('accessibility.signals.noInlayHints', 'No Inlay Hints'),497settingsKey: 'accessibility.signals.noInlayHints',498});499500public static readonly taskCompleted = AccessibilitySignal.register({501name: localize('accessibilitySignals.taskCompleted', 'Task Completed'),502sound: Sound.taskCompleted,503legacySoundSettingsKey: 'audioCues.taskCompleted',504legacyAnnouncementSettingsKey: 'accessibility.alert.taskCompleted',505announcementMessage: localize('accessibility.signals.taskCompleted', 'Task Completed'),506settingsKey: 'accessibility.signals.taskCompleted',507});508509public static readonly taskFailed = AccessibilitySignal.register({510name: localize('accessibilitySignals.taskFailed', 'Task Failed'),511sound: Sound.taskFailed,512legacySoundSettingsKey: 'audioCues.taskFailed',513legacyAnnouncementSettingsKey: 'accessibility.alert.taskFailed',514announcementMessage: localize('accessibility.signals.taskFailed', 'Task Failed'),515settingsKey: 'accessibility.signals.taskFailed',516});517518public static readonly terminalCommandFailed = AccessibilitySignal.register({519name: localize('accessibilitySignals.terminalCommandFailed', 'Terminal Command Failed'),520sound: Sound.error,521legacySoundSettingsKey: 'audioCues.terminalCommandFailed',522legacyAnnouncementSettingsKey: 'accessibility.alert.terminalCommandFailed',523announcementMessage: localize('accessibility.signals.terminalCommandFailed', 'Command Failed'),524settingsKey: 'accessibility.signals.terminalCommandFailed',525});526527public static readonly terminalCommandSucceeded = AccessibilitySignal.register({528name: localize('accessibilitySignals.terminalCommandSucceeded', 'Terminal Command Succeeded'),529sound: Sound.terminalCommandSucceeded,530announcementMessage: localize('accessibility.signals.terminalCommandSucceeded', 'Command Succeeded'),531settingsKey: 'accessibility.signals.terminalCommandSucceeded',532});533534public static readonly terminalBell = AccessibilitySignal.register({535name: localize('accessibilitySignals.terminalBell', 'Terminal Bell'),536sound: Sound.terminalBell,537legacySoundSettingsKey: 'audioCues.terminalBell',538legacyAnnouncementSettingsKey: 'accessibility.alert.terminalBell',539announcementMessage: localize('accessibility.signals.terminalBell', 'Terminal Bell'),540settingsKey: 'accessibility.signals.terminalBell',541});542543public static readonly notebookCellCompleted = AccessibilitySignal.register({544name: localize('accessibilitySignals.notebookCellCompleted', 'Notebook Cell Completed'),545sound: Sound.taskCompleted,546legacySoundSettingsKey: 'audioCues.notebookCellCompleted',547legacyAnnouncementSettingsKey: 'accessibility.alert.notebookCellCompleted',548announcementMessage: localize('accessibility.signals.notebookCellCompleted', 'Notebook Cell Completed'),549settingsKey: 'accessibility.signals.notebookCellCompleted',550});551552public static readonly notebookCellFailed = AccessibilitySignal.register({553name: localize('accessibilitySignals.notebookCellFailed', 'Notebook Cell Failed'),554sound: Sound.taskFailed,555legacySoundSettingsKey: 'audioCues.notebookCellFailed',556legacyAnnouncementSettingsKey: 'accessibility.alert.notebookCellFailed',557announcementMessage: localize('accessibility.signals.notebookCellFailed', 'Notebook Cell Failed'),558settingsKey: 'accessibility.signals.notebookCellFailed',559});560561public static readonly diffLineInserted = AccessibilitySignal.register({562name: localize('accessibilitySignals.diffLineInserted', 'Diff Line Inserted'),563sound: Sound.diffLineInserted,564legacySoundSettingsKey: 'audioCues.diffLineInserted',565settingsKey: 'accessibility.signals.diffLineInserted',566});567568public static readonly diffLineDeleted = AccessibilitySignal.register({569name: localize('accessibilitySignals.diffLineDeleted', 'Diff Line Deleted'),570sound: Sound.diffLineDeleted,571legacySoundSettingsKey: 'audioCues.diffLineDeleted',572settingsKey: 'accessibility.signals.diffLineDeleted',573});574575public static readonly diffLineModified = AccessibilitySignal.register({576name: localize('accessibilitySignals.diffLineModified', 'Diff Line Modified'),577sound: Sound.diffLineModified,578legacySoundSettingsKey: 'audioCues.diffLineModified',579settingsKey: 'accessibility.signals.diffLineModified',580});581582public static readonly chatEditModifiedFile = AccessibilitySignal.register({583name: localize('accessibilitySignals.chatEditModifiedFile', 'Chat Edit Modified File'),584sound: Sound.chatEditModifiedFile,585announcementMessage: localize('accessibility.signals.chatEditModifiedFile', 'File Modified from Chat Edits'),586settingsKey: 'accessibility.signals.chatEditModifiedFile',587});588589public static readonly chatRequestSent = AccessibilitySignal.register({590name: localize('accessibilitySignals.chatRequestSent', 'Chat Request Sent'),591sound: Sound.requestSent,592legacySoundSettingsKey: 'audioCues.chatRequestSent',593legacyAnnouncementSettingsKey: 'accessibility.alert.chatRequestSent',594announcementMessage: localize('accessibility.signals.chatRequestSent', 'Chat Request Sent'),595settingsKey: 'accessibility.signals.chatRequestSent',596});597598public static readonly chatResponseReceived = AccessibilitySignal.register({599name: localize('accessibilitySignals.chatResponseReceived', 'Chat Response Received'),600legacySoundSettingsKey: 'audioCues.chatResponseReceived',601sound: {602randomOneOf: [603Sound.responseReceived1,604Sound.responseReceived2,605Sound.responseReceived3,606Sound.responseReceived4607]608},609settingsKey: 'accessibility.signals.chatResponseReceived'610});611612public static readonly codeActionTriggered = AccessibilitySignal.register({613name: localize('accessibilitySignals.codeActionRequestTriggered', 'Code Action Request Triggered'),614sound: Sound.codeActionTriggered,615legacySoundSettingsKey: 'audioCues.codeActionRequestTriggered',616legacyAnnouncementSettingsKey: 'accessibility.alert.codeActionRequestTriggered',617announcementMessage: localize('accessibility.signals.codeActionRequestTriggered', 'Code Action Request Triggered'),618settingsKey: 'accessibility.signals.codeActionTriggered',619});620621public static readonly codeActionApplied = AccessibilitySignal.register({622name: localize('accessibilitySignals.codeActionApplied', 'Code Action Applied'),623legacySoundSettingsKey: 'audioCues.codeActionApplied',624sound: Sound.codeActionApplied,625settingsKey: 'accessibility.signals.codeActionApplied'626});627628629public static readonly progress = AccessibilitySignal.register({630name: localize('accessibilitySignals.progress', 'Progress'),631sound: Sound.progress,632legacySoundSettingsKey: 'audioCues.chatResponsePending',633legacyAnnouncementSettingsKey: 'accessibility.alert.progress',634announcementMessage: localize('accessibility.signals.progress', 'Progress'),635settingsKey: 'accessibility.signals.progress'636});637638public static readonly clear = AccessibilitySignal.register({639name: localize('accessibilitySignals.clear', 'Clear'),640sound: Sound.clear,641legacySoundSettingsKey: 'audioCues.clear',642legacyAnnouncementSettingsKey: 'accessibility.alert.clear',643announcementMessage: localize('accessibility.signals.clear', 'Clear'),644settingsKey: 'accessibility.signals.clear'645});646647public static readonly save = AccessibilitySignal.register({648name: localize('accessibilitySignals.save', 'Save'),649sound: Sound.save,650legacySoundSettingsKey: 'audioCues.save',651legacyAnnouncementSettingsKey: 'accessibility.alert.save',652announcementMessage: localize('accessibility.signals.save', 'Save'),653settingsKey: 'accessibility.signals.save'654});655656public static readonly format = AccessibilitySignal.register({657name: localize('accessibilitySignals.format', 'Format'),658sound: Sound.format,659legacySoundSettingsKey: 'audioCues.format',660legacyAnnouncementSettingsKey: 'accessibility.alert.format',661announcementMessage: localize('accessibility.signals.format', 'Format'),662settingsKey: 'accessibility.signals.format'663});664665public static readonly voiceRecordingStarted = AccessibilitySignal.register({666name: localize('accessibilitySignals.voiceRecordingStarted', 'Voice Recording Started'),667sound: Sound.voiceRecordingStarted,668legacySoundSettingsKey: 'audioCues.voiceRecordingStarted',669settingsKey: 'accessibility.signals.voiceRecordingStarted'670});671672public static readonly voiceRecordingStopped = AccessibilitySignal.register({673name: localize('accessibilitySignals.voiceRecordingStopped', 'Voice Recording Stopped'),674sound: Sound.voiceRecordingStopped,675legacySoundSettingsKey: 'audioCues.voiceRecordingStopped',676settingsKey: 'accessibility.signals.voiceRecordingStopped'677});678679public static readonly editsKept = AccessibilitySignal.register({680name: localize('accessibilitySignals.editsKept', 'Edits Kept'),681sound: Sound.editsKept,682announcementMessage: localize('accessibility.signals.editsKept', 'Edits Kept'),683settingsKey: 'accessibility.signals.editsKept',684});685686public static readonly editsUndone = AccessibilitySignal.register({687name: localize('accessibilitySignals.editsUndone', 'Undo Edits'),688sound: Sound.editsUndone,689announcementMessage: localize('accessibility.signals.editsUndone', 'Edits Undone'),690settingsKey: 'accessibility.signals.editsUndone',691});692693public static readonly chatUserActionRequired = AccessibilitySignal.register({694name: localize('accessibilitySignals.chatUserActionRequired', 'Chat User Action Required'),695sound: Sound.chatUserActionRequired,696announcementMessage: localize('accessibility.signals.chatUserActionRequired', 'Chat User Action Required'),697settingsKey: 'accessibility.signals.chatUserActionRequired',698managesOwnEnablement: true699});700}701702703