Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/vs/sessions/AI_CUSTOMIZATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ The shared `applyStorageSourceFilter()` helper applies this filter to any `{uri,

Local harness: all types use `[local, user, extension, plugin, builtin]`. Items from the default chat extension (`productService.defaultChatAgent.chatExtensionId`) are grouped under "Built-in" via `groupKey` override in the list widget.

Voice customizations follow the same workspace/user split as Copilot instructions but are consumed directly by voice features rather than listed as standard prompt-file sections in the management editor. Voice Mode combines `~/.copilot/voice.md` with each trusted workspace's `.github/voice.md` and sends the result to the backend as `voice_instructions` on both session start and resume. Dictation separately combines `~/.copilot/dictation.md` with each trusted workspace's `.github/dictation.md` and appends the result to its language-model post-processing prompt for terminology and formatting guidance. Separate configure commands create or open either scope and are linked from their respective settings, microphone menus, and the management editor overview.

CLI harness (core):

| Type | sources |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js';
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
import { ChatAgentLocation } from '../../chat/common/constants.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { CONFIGURE_VOICE_INSTRUCTIONS_ACTION_ID } from '../../chat/browser/actions/configureVoiceInstructionsAction.js';

// --- Context Keys ---

Expand Down Expand Up @@ -361,7 +362,7 @@ registerAction2(class extends Action2 {
}
});

// --- Open Voice Mode Settings (surfaced via the mic button context menu, no toolbar gear) ---
// --- Open Voice Mode Settings ---

registerAction2(class extends Action2 {
constructor() {
Expand Down Expand Up @@ -556,7 +557,7 @@ configurationRegistry.registerConfiguration({
nls.localize('agents.voice.voice.maya', "Maya."),
nls.localize('agents.voice.voice.daniel', "Daniel."),
],
description: nls.localize('agents.voice.voice', "The voice used when the assistant reads responses aloud. Changing this while voice mode is connected takes effect immediately."),
markdownDescription: nls.localize('agents.voice.voice', "The voice used when the assistant reads responses aloud. Changing this while voice mode is connected takes effect immediately. Use [Voice Mode instructions](command:{0}) to customize Voice Mode behavior and terminology.", CONFIGURE_VOICE_INSTRUCTIONS_ACTION_ID),
default: 'maya_neutral',
scope: ConfigurationScope.APPLICATION,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { dirname, joinPath } from '../../../../../base/common/resources.js';
import { URI } from '../../../../../base/common/uri.js';
import { localize2, localize } from '../../../../../nls.js';
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { IWorkspaceTrustManagementService } from '../../../../../platform/workspace/common/workspaceTrust.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { IPathService } from '../../../../services/path/common/pathService.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { COPILOT_CONFIG_FOLDER, DICTATION_INSTRUCTIONS_FILENAME, GITHUB_CONFIG_FOLDER, VOICE_INSTRUCTIONS_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js';

export const CONFIGURE_VOICE_INSTRUCTIONS_ACTION_ID = 'workbench.action.chat.configureVoiceInstructions';
export const CONFIGURE_DICTATION_INSTRUCTIONS_ACTION_ID = 'workbench.action.chat.configureDictationInstructions';

interface IVoiceInstructionsQuickPickItem extends IQuickPickItem {
readonly resource: URI;
}

class ConfigureSpeechInstructionsAction extends Action2 {

constructor(id: string, title: ReturnType<typeof localize2>, private readonly fileName: string) {
super({
id,
title,
precondition: ChatContextKeys.enabled,
f1: false,
menu: {
id: MenuId.CommandPalette,
when: ChatContextKeys.enabled,
},
});
}

override async run(accessor: ServicesAccessor): Promise<void> {
const pathService = accessor.get(IPathService);
const workspaceContextService = accessor.get(IWorkspaceContextService);
const workspaceTrustService = accessor.get(IWorkspaceTrustManagementService);
const quickInputService = accessor.get(IQuickInputService);
const fileService = accessor.get(IFileService);
const editorService = accessor.get(IEditorService);

const userHome = await pathService.userHome();
const items: IVoiceInstructionsQuickPickItem[] = [{
label: localize('userSpeechInstructions', "User"),
description: `~/.copilot/${this.fileName}`,
detail: localize('userSpeechInstructionsDetail', "Applies across all workspaces."),
resource: joinPath(userHome, COPILOT_CONFIG_FOLDER, this.fileName),
}];

const workspaceFolders = workspaceContextService.getWorkspace().folders;
if (workspaceTrustService.isWorkspaceTrusted()) {
for (const folder of workspaceFolders) {
items.push({
label: workspaceFolders.length === 1
? localize('workspaceSpeechInstructions', "Workspace")
: localize('workspaceSpeechInstructionsNamed', "Workspace: {0}", folder.name),
description: `.github/${this.fileName}`,
detail: localize('workspaceSpeechInstructionsDetail', "Applies to this workspace and can be shared with your team."),
resource: joinPath(folder.uri, GITHUB_CONFIG_FOLDER, this.fileName),
});
}
}

const selected = await quickInputService.pick(items, {
placeHolder: workspaceFolders.length > 0 && !workspaceTrustService.isWorkspaceTrusted()
? localize('selectSpeechInstructionsTargetUntrusted', "Select where to configure {0}. Trust the workspace to configure workspace instructions.", this.fileName)
: localize('selectSpeechInstructionsTarget', "Select where to configure {0}", this.fileName),
matchOnDescription: true,
matchOnDetail: true,
});
if (!selected) {
return;
}

if (!await fileService.exists(selected.resource)) {
await fileService.createFolder(dirname(selected.resource));
await fileService.createFile(selected.resource);
}
await editorService.openEditor({ resource: selected.resource });
}
}

export function registerConfigureSpeechInstructionsActions(): void {
registerAction2(class extends ConfigureSpeechInstructionsAction {
constructor() {
super(
CONFIGURE_VOICE_INSTRUCTIONS_ACTION_ID,
localize2('configureVoiceInstructions', "Voice: Configure Voice Mode Instructions"),
VOICE_INSTRUCTIONS_FILENAME,
);
}
});
registerAction2(class extends ConfigureSpeechInstructionsAction {
constructor() {
super(
CONFIGURE_DICTATION_INSTRUCTIONS_ACTION_ID,
localize2('configureDictationInstructions', "Voice: Configure Dictation Instructions"),
DICTATION_INSTRUCTIONS_FILENAME,
);
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { IAICustomizationWelcomePageImplementation, IWelcomePageCallbacks }
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { IPromptMigrationInfo } from './promptMigration.js';
import { CONFIGURE_DICTATION_INSTRUCTIONS_ACTION_ID, CONFIGURE_VOICE_INSTRUCTIONS_ACTION_ID } from '../actions/configureVoiceInstructionsAction.js';

const $ = DOM.$;

Expand All @@ -31,6 +32,13 @@ interface IPromptLaunchersCategoryDescription {
readonly promptType?: PromptsType;
}

interface IStandaloneCustomizationDescription {
readonly label: string;
readonly icon: ThemeIcon;
readonly description: string;
readonly commandId: string;
}

export class PromptLaunchersAICustomizationWelcomePage extends Disposable implements IAICustomizationWelcomePageImplementation {

private readonly cardDisposables = this._register(new DisposableStore());
Expand Down Expand Up @@ -97,11 +105,26 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem
},
];

private readonly standaloneCustomizations: IStandaloneCustomizationDescription[] = [
{
label: localize('voiceModeInstructions', "Voice Mode Instructions"),
icon: Codicon.voiceMode,
description: localize('voiceModeInstructionsDesc', "Customize Voice Mode behavior and terminology with voice.md."),
commandId: CONFIGURE_VOICE_INSTRUCTIONS_ACTION_ID,
},
{
label: localize('dictationInstructions', "Dictation Instructions"),
icon: Codicon.mic,
description: localize('dictationInstructionsDesc', "Customize Dictation terminology and transcript formatting with dictation.md."),
commandId: CONFIGURE_DICTATION_INSTRUCTIONS_ACTION_ID,
},
];

constructor(
parent: HTMLElement,
private readonly welcomePageFeatures: IWelcomePageFeatures | undefined,
private readonly callbacks: IWelcomePageCallbacks,
_commandService: ICommandService,
private readonly commandService: ICommandService,
private readonly workspaceService: IAICustomizationWorkspaceService,
private readonly hoverService: IHoverService,
private harnessLabel: string,
Expand Down Expand Up @@ -293,6 +316,12 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem
}));
}

if (!this.workspaceService.isSessionsWindow) {
for (const customization of this.standaloneCustomizations) {
this.renderStandaloneCustomization(customization);
}
}

if (this.promptMigrationInfo) {
this.renderPromptMigrationCard();
}
Expand All @@ -301,6 +330,48 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem
this.scrollable.scanDomNode();
}

private renderStandaloneCustomization(customization: IStandaloneCustomizationDescription): void {
if (!this.cardsContainer) {
return;
}

const card = DOM.append(this.cardsContainer, $('.welcome-prompts-card'));
card.setAttribute('tabindex', '0');
card.setAttribute('role', 'button');
if (!this.firstCard) {
this.firstCard = card;
}

const cardHeader = DOM.append(card, $('.welcome-prompts-card-header'));
const iconEl = DOM.append(cardHeader, $('.welcome-prompts-card-icon'));
iconEl.classList.add(...ThemeIcon.asClassNameArray(customization.icon));
const labelEl = DOM.append(cardHeader, $('span.welcome-prompts-card-label'));
labelEl.textContent = customization.label;

const descEl = DOM.append(card, $('p.welcome-prompts-card-description'));
descEl.textContent = customization.description;

const footer = DOM.append(card, $('.welcome-prompts-card-footer'));
const configureButton = DOM.append(footer, $('button.welcome-prompts-card-action'));
configureButton.textContent = localize('configure', "Configure...");
configureButton.setAttribute('aria-label', localize('configureCategoryAriaLabel', "Configure {0}...", customization.label));

const configure = () => {
void this.commandService.executeCommand(customization.commandId);
};
this.cardDisposables.add(DOM.addDisposableListener(configureButton, 'click', e => {
e.stopPropagation();
configure();
}));
this.cardDisposables.add(DOM.addDisposableListener(card, 'click', configure));
this.cardDisposables.add(DOM.addDisposableListener(card, 'keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
configure();
}
}));
}

setPromptMigrationInfo(info: IPromptMigrationInfo | undefined): void {
const didChange = this.promptMigrationInfo?.totalPromptCount !== info?.totalPromptCount
|| this.promptMigrationInfo?.workspacePromptCount !== info?.workspacePromptCount
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import { registerChatExecuteActions } from './actions/chatExecuteActions.js';
import { ChatVoiceInputModeAction, ChatVoiceInputModeToggleListenAction, registerVoiceInputModeSimulateActions } from './voiceInputMode/voiceInputModeActionViewItem.js';
import './voiceInputMode/voiceInputMode.js';
import { registerChatSpeechToTextActions } from './actions/chatSpeechToTextActions.js';
import { CONFIGURE_DICTATION_INSTRUCTIONS_ACTION_ID, registerConfigureSpeechInstructionsActions } from './actions/configureVoiceInstructionsAction.js';
import { ChatSpeechToTextService, DictationSettingId, IChatSpeechToTextService } from './speechToText/chatSpeechToTextService.js';
import { registerChatFileTreeActions } from './actions/chatFileTreeActions.js';
import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js';
Expand Down Expand Up @@ -301,7 +302,7 @@ configurationRegistry.registerConfiguration({
},
'dictation.experimental.llmCleanup': {
type: 'boolean',
markdownDescription: nls.localize('dictation.experimental.llmCleanup', "Experimental: periodically refine finalized text while dictating, then pass the final transcript through a small language model to restore punctuation, capitalization, paragraphs, and lists. Requires Copilot to be enabled; the transcript is sent to the language model for cleanup. Falls back to the raw transcript when no model is available."),
markdownDescription: nls.localize('dictation.experimental.llmCleanup', "Experimental: periodically refine finalized text while dictating, then pass the final transcript through a small language model to restore punctuation, capitalization, paragraphs, and lists. Requires Copilot to be enabled; the transcript is sent to the language model for cleanup. Falls back to the raw transcript when no model is available. Use [dictation instructions](command:{0}) to customize terminology and formatting.", CONFIGURE_DICTATION_INSTRUCTIONS_ACTION_ID),
default: true,
tags: ['experimental']
},
Expand Down Expand Up @@ -2871,6 +2872,7 @@ registerAction2(ChatVoiceInputModeAction);
registerAction2(ChatVoiceInputModeToggleListenAction);
registerVoiceInputModeSimulateActions();
registerChatSpeechToTextActions();
registerConfigureSpeechInstructionsActions();
registerChatQueueActions();
registerQuickChatActions();
registerChatExportActions();
Expand Down
Loading
Loading