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
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
import { derived, IObservable, observableValue, ISettableObservable } from '../../../../base/common/observable.js';
import { joinPath } from '../../../../base/common/resources.js';
import { URI } from '../../../../base/common/uri.js';
import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter, applyStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { IChatPromptSlashCommand, IPromptsService, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { CustomizationCreatorService } from '../../../../workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.js';
Expand Down Expand Up @@ -44,6 +45,7 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization
constructor(
@ISessionsManagementService private readonly sessionsService: ISessionsManagementService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IPromptsService private readonly promptsService: IPromptsService,
@IPathService pathService: IPathService,
) {
const userHome = pathService.userHome({ preferLocal: true });
Expand Down Expand Up @@ -135,4 +137,12 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization
const creator = this.instantiationService.createInstance(CustomizationCreatorService);
await creator.createWithAI(type);
}

async getFilteredPromptSlashCommands(token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]> {
const allCommands = await this.promptsService.getPromptSlashCommands(token);
return allCommands.filter(cmd => {
const filter = this.getStorageSourceFilter(cmd.promptPath.type);
return applyStorageSourceFilter([cmd.promptPath], filter).length > 0;
Comment thread
joshspicer marked this conversation as resolved.
});
}
}
8 changes: 7 additions & 1 deletion src/vs/sessions/contrib/chat/browser/newChatViewPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
}

private async _send(options?: { openNewAfterSend?: boolean }): Promise<void> {
const query = this._editor.getModel()?.getValue().trim();
let query = this._editor.getModel()?.getValue().trim();
const session = this._newSession.value;
if (!query || !session || this._sending) {
return;
Expand All @@ -968,6 +968,12 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
return;
}

// Expand prompt/skill slash commands into a CLI-friendly reference
const expanded = this._slashCommandHandler?.tryExpandPromptSlashCommand(query);
if (expanded) {
query = expanded;
}

session.setQuery(query);
session.setAttachedContext(
this._contextAttachments.attachments.length > 0 ? [...this._contextAttachments.attachments] : undefined
Expand Down
87 changes: 82 additions & 5 deletions src/vs/sessions/contrib/chat/browser/slashCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { inputPlaceholderForeground } from '../../../../platform/theme/common/co
import { localize } from '../../../../nls.js';
import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../../../workbench/contrib/chat/common/widget/chatColors.js';
import { AICustomizationManagementCommands, AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js';
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { IChatPromptSlashCommand, IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';

/**
* Static command ID used by completion items to trigger immediate slash command execution,
Expand Down Expand Up @@ -57,30 +59,42 @@ export class SlashCommandHandler extends Disposable {
private static _slashDecosRegistered = false;

private readonly _slashCommands: ISessionsSlashCommandData[] = [];
private _cachedPromptCommands: readonly IChatPromptSlashCommand[] = [];

constructor(
private readonly _editor: CodeEditorWidget,
@ICommandService private readonly commandService: ICommandService,
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IThemeService private readonly themeService: IThemeService,
@IAICustomizationWorkspaceService private readonly aiCustomizationWorkspaceService: IAICustomizationWorkspaceService,
@IPromptsService private readonly promptsService: IPromptsService,
) {
super();
this._registerSlashCommands();
this._registerCompletions();
this._registerDecorations();
this._refreshPromptCommands();
this._register(this.promptsService.onDidChangeSlashCommands(() => this._refreshPromptCommands()));
}

clearInput(): void {
this._editor.getModel()?.setValue('');
}

private _refreshPromptCommands(): void {
this.aiCustomizationWorkspaceService.getFilteredPromptSlashCommands(CancellationToken.None).then(commands => {
this._cachedPromptCommands = commands;
this._updateDecorations();
}, () => { /* swallow errors from stale refresh */ });
}

/**
* Attempts to parse and execute a slash command from the input.
* Returns `true` if a command was handled.
*/
tryExecuteSlashCommand(query: string): boolean {
const match = query.match(/^\/(\w+)\s*(.*)/s);
const match = query.match(/^\/([\w\p{L}\d_\-\.:]+)\s*(.*)/su);
if (!match) {
return false;
}
Expand All @@ -95,6 +109,29 @@ export class SlashCommandHandler extends Disposable {
return true;
}

/**
* If the query starts with a prompt/skill slash command (e.g. `/my-prompt args`),
* expands it into a CLI-friendly markdown reference so the agent can locate the
* file. Returns `undefined` when the query is not a prompt slash command.
*/
tryExpandPromptSlashCommand(query: string): string | undefined {
const match = query.match(/^\/([\w\p{L}\d_\-\.:]+)\s*(.*)/su);
if (!match) {
return undefined;
}

const commandName = match[1];
const promptCommand = this._cachedPromptCommands.find(c => c.name === commandName);
if (!promptCommand) {
return undefined;
}

const args = match[2]?.trim() ?? '';
const uri = promptCommand.promptPath.uri;
const expanded = `Use the prompt file located at [${promptCommand.name}](${uri.toString()}).`;
return args ? `${expanded} ${args}` : expanded;
}
Comment thread
joshspicer marked this conversation as resolved.

private _registerSlashCommands(): void {
const openSection = (section: AICustomizationManagementSection) =>
() => this.commandService.executeCommand(AICustomizationManagementCommands.OpenEditor, section);
Expand Down Expand Up @@ -154,7 +191,7 @@ export class SlashCommandHandler extends Disposable {
private _updateDecorations(): void {
const model = this._editor.getModel();
const value = model?.getValue() ?? '';
const match = value.match(/^\/(\w+)\s?/);
const match = value.match(/^\/([\w\p{L}\d_\-\.:]+)\s?/u);

if (!match) {
this._editor.setDecorationsByType('sessions-chat', SlashCommandHandler._slashDecoType, []);
Expand All @@ -164,7 +201,8 @@ export class SlashCommandHandler extends Disposable {

const commandName = match[1];
const slashCommand = this._slashCommands.find(c => c.command === commandName);
if (!slashCommand) {
const promptCommand = this._cachedPromptCommands.find(c => c.name === commandName);
if (!slashCommand && !promptCommand) {
this._editor.setDecorationsByType('sessions-chat', SlashCommandHandler._slashDecoType, []);
this._editor.setDecorationsByType('sessions-chat', SlashCommandHandler._slashPlaceholderDecoType, []);
return;
Expand All @@ -179,13 +217,14 @@ export class SlashCommandHandler extends Disposable {

// Show the command description as a placeholder after the command
const restOfInput = value.slice(match[0].length).trim();
if (!restOfInput && slashCommand.detail) {
const detail = slashCommand?.detail ?? promptCommand?.description;
if (!restOfInput && detail) {
const placeholderCol = match[0].length + 1;
const placeholderDeco: IDecorationOptions[] = [{
range: { startLineNumber: 1, startColumn: placeholderCol, endLineNumber: 1, endColumn: model!.getLineMaxColumn(1) },
renderOptions: {
after: {
contentText: slashCommand.detail,
contentText: detail,
color: this._getPlaceholderColor(),
}
}
Expand Down Expand Up @@ -238,6 +277,44 @@ export class SlashCommandHandler extends Disposable {
};
}
}));

// Dynamic completions for individual prompt/skill files (filtered to match
// what the sessions customizations view shows).
this._register(this.languageFeaturesService.completionProvider.register({ scheme: uri.scheme, hasAccessToAllModels: true }, {
_debugDisplayName: 'sessionsPromptSlashCommands',
triggerCharacters: ['/'],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => {
const range = this._computeCompletionRanges(model, position, /\/[\p{L}0-9_.:-]*/gu);
if (!range) {
return null;
}

const textBefore = model.getValueInRange(new Range(1, 1, range.replace.startLineNumber, range.replace.startColumn));
if (textBefore.trim() !== '') {
return null;
}

const promptCommands = await this.aiCustomizationWorkspaceService.getFilteredPromptSlashCommands(token);
const userInvocable = promptCommands.filter(c => c.parsedPromptFile?.header?.userInvocable !== false);
if (userInvocable.length === 0) {
return null;
}

return {
suggestions: userInvocable.map((c, i): CompletionItem => {
const label = `/${c.name}`;
return {
label: { label, description: c.description },
insertText: `${label} `,
documentation: c.description,
range,
sortText: 'b'.repeat(i + 1),
kind: CompletionItemKind.Text,
};
})
};
}
}));
}

private _computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp): { insert: Range; replace: Range } | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

import { constObservable, derived, IObservable, observableFromEventOpts } from '../../../../../base/common/observable.js';
import { URI } from '../../../../../base/common/uri.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter } from '../../common/aiCustomizationWorkspaceService.js';
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
import { IChatPromptSlashCommand, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
import {
Expand All @@ -27,6 +28,7 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic
constructor(
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@ICommandService private readonly commandService: ICommandService,
@IPromptsService private readonly promptsService: IPromptsService,
) {
const workspaceFolders = observableFromEventOpts(
{ owner: this },
Expand Down Expand Up @@ -84,6 +86,10 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic
await this.commandService.executeCommand(commandId);
}
}

async getFilteredPromptSlashCommands(token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]> {
return this.promptsService.getPromptSlashCommands(token);
}
Comment thread
joshspicer marked this conversation as resolved.
}

registerSingleton(IAICustomizationWorkspaceService, AICustomizationWorkspaceService, InstantiationType.Delayed);
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken } from '../../../../base/common/cancellation.js';
import { IObservable } from '../../../../base/common/observable.js';
import { URI } from '../../../../base/common/uri.js';
import { isEqualOrParent } from '../../../../base/common/resources.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { PromptsType } from './promptSyntax/promptTypes.js';
import { PromptsStorage } from './promptSyntax/service/promptsService.js';
import { IChatPromptSlashCommand, PromptsStorage } from './promptSyntax/service/promptsService.js';

export const IAICustomizationWorkspaceService = createDecorator<IAICustomizationWorkspaceService>('aiCustomizationWorkspaceService');

Expand Down Expand Up @@ -121,4 +122,11 @@ export interface IAICustomizationWorkspaceService {
* session-derived (or workspace-derived) root.
*/
clearOverrideProjectRoot(): void;

/**
* Returns prompt/skill slash commands filtered through the workspace
* service's storage source policy, ensuring the results match the
* customizations visible in the AI Customization views.
*/
getFilteredPromptSlashCommands(token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]>;
}
Loading