mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-09 16:24:45 +01:00
tighten tip when conditions, spilt out /create commands (#297555)
This commit is contained in:
@@ -25,6 +25,7 @@ import { IChatService } from '../common/chatService/chatService.js';
|
||||
import { CreateSlashCommandsUsageTracker } from './createSlashCommandsUsageTracker.js';
|
||||
import { ChatEntitlement, IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, IParsedChatRequest } from '../common/requestParser/chatParserTypes.js';
|
||||
|
||||
type ChatTipEvent = {
|
||||
tipId: string;
|
||||
@@ -40,6 +41,12 @@ type ChatTipClassification = {
|
||||
comment: 'Tracks user interactions with chat tips to understand which tips resonate and which are dismissed.';
|
||||
};
|
||||
|
||||
const ATTACH_FILES_REFERENCE_TRACKING_COMMAND = 'chat.tips.attachFiles.referenceUsed';
|
||||
const CREATE_INSTRUCTION_TRACKING_COMMAND = 'chat.tips.createInstruction.commandUsed';
|
||||
const CREATE_PROMPT_TRACKING_COMMAND = 'chat.tips.createPrompt.commandUsed';
|
||||
const CREATE_AGENT_TRACKING_COMMAND = 'chat.tips.createAgent.commandUsed';
|
||||
const CREATE_SKILL_TRACKING_COMMAND = 'chat.tips.createSkill.commandUsed';
|
||||
|
||||
export const IChatTipService = createDecorator<IChatTipService>('chatTipService');
|
||||
|
||||
export interface IChatTip {
|
||||
@@ -182,26 +189,55 @@ const TIP_CATALOG: ITipDefinition[] = [
|
||||
onlyWhenModelIds: ['gpt-4.1'],
|
||||
},
|
||||
{
|
||||
id: 'tip.createSlashCommands',
|
||||
id: 'tip.createInstruction',
|
||||
message: localize(
|
||||
'tip.createSlashCommands',
|
||||
"Tip: Use [/create-instruction](command:workbench.action.chat.generateInstruction), [/create-prompt](command:workbench.action.chat.generatePrompt), [/create-agent](command:workbench.action.chat.generateAgent), or [/create-skill](command:workbench.action.chat.generateSkill) to generate reusable agent customization files."
|
||||
'tip.createInstruction',
|
||||
"Tip: Use [/create-instruction](command:workbench.action.chat.generateInstruction) to generate an on-demand instruction file with the agent."
|
||||
),
|
||||
when: ContextKeyExpr.and(
|
||||
ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType),
|
||||
ChatContextKeys.hasUsedCreateSlashCommands.negate(),
|
||||
),
|
||||
enabledCommands: [
|
||||
'workbench.action.chat.generateInstruction',
|
||||
'workbench.action.chat.generatePrompt',
|
||||
'workbench.action.chat.generateAgent',
|
||||
'workbench.action.chat.generateSkill',
|
||||
],
|
||||
when: ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType),
|
||||
enabledCommands: ['workbench.action.chat.generateInstruction'],
|
||||
excludeWhenCommandsExecuted: [
|
||||
'workbench.action.chat.generateInstruction',
|
||||
CREATE_INSTRUCTION_TRACKING_COMMAND,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tip.createPrompt',
|
||||
message: localize(
|
||||
'tip.createPrompt',
|
||||
"Tip: Use [/create-prompt](command:workbench.action.chat.generatePrompt) to generate a reusable prompt file with the agent."
|
||||
),
|
||||
when: ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType),
|
||||
enabledCommands: ['workbench.action.chat.generatePrompt'],
|
||||
excludeWhenCommandsExecuted: [
|
||||
'workbench.action.chat.generatePrompt',
|
||||
CREATE_PROMPT_TRACKING_COMMAND,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tip.createAgent',
|
||||
message: localize(
|
||||
'tip.createAgent',
|
||||
"Tip: Use [/create-agent](command:workbench.action.chat.generateAgent) to scaffold a custom agent for your workflow."
|
||||
),
|
||||
when: ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType),
|
||||
enabledCommands: ['workbench.action.chat.generateAgent'],
|
||||
excludeWhenCommandsExecuted: [
|
||||
'workbench.action.chat.generateAgent',
|
||||
CREATE_AGENT_TRACKING_COMMAND,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tip.createSkill',
|
||||
message: localize(
|
||||
'tip.createSkill',
|
||||
"Tip: Use [/create-skill](command:workbench.action.chat.generateSkill) to create a skill the agent can load when relevant."
|
||||
),
|
||||
when: ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType),
|
||||
enabledCommands: ['workbench.action.chat.generateSkill'],
|
||||
excludeWhenCommandsExecuted: [
|
||||
'workbench.action.chat.generateSkill',
|
||||
CREATE_SKILL_TRACKING_COMMAND,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -222,7 +258,7 @@ const TIP_CATALOG: ITipDefinition[] = [
|
||||
{
|
||||
id: 'tip.attachFiles',
|
||||
message: localize('tip.attachFiles', "Tip: Reference files or folders with # to give the agent more context about the task."),
|
||||
excludeWhenCommandsExecuted: ['workbench.action.chat.attachContext', 'workbench.action.chat.attachFile', 'workbench.action.chat.attachFolder', 'workbench.action.chat.attachSelection'],
|
||||
excludeWhenCommandsExecuted: ['workbench.action.chat.attachContext', 'workbench.action.chat.attachFile', 'workbench.action.chat.attachFolder', 'workbench.action.chat.attachSelection', ATTACH_FILES_REFERENCE_TRACKING_COMMAND],
|
||||
},
|
||||
{
|
||||
id: 'tip.codeActions',
|
||||
@@ -241,29 +277,6 @@ const TIP_CATALOG: ITipDefinition[] = [
|
||||
),
|
||||
excludeWhenCommandsExecuted: ['workbench.action.chat.restoreCheckpoint'],
|
||||
},
|
||||
{
|
||||
id: 'tip.customInstructions',
|
||||
message: localize('tip.customInstructions', "Tip: [Generate workspace instructions](command:workbench.action.chat.generateInstructions) apply coding conventions across all agent sessions."),
|
||||
enabledCommands: ['workbench.action.chat.generateInstructions'],
|
||||
excludeWhenCommandsExecuted: ['workbench.action.chat.generateInstructions'],
|
||||
excludeWhenPromptFilesExist: { promptType: PromptsType.instructions, agentFileType: AgentFileType.copilotInstructionsMd, excludeUntilChecked: true },
|
||||
},
|
||||
{
|
||||
id: 'tip.customAgent',
|
||||
message: localize('tip.customAgent', "Tip: [Create a custom agent](command:workbench.command.new.agent) to define reusable personas with tailored instructions and tools for your workflow."),
|
||||
when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
|
||||
enabledCommands: ['workbench.command.new.agent'],
|
||||
excludeWhenCommandsExecuted: ['workbench.command.new.agent'],
|
||||
excludeWhenPromptFilesExist: { promptType: PromptsType.agent, excludeUntilChecked: true },
|
||||
},
|
||||
{
|
||||
id: 'tip.skill',
|
||||
message: localize('tip.skill', "Tip: [Create a skill](command:workbench.command.new.skill) to teach the agent specialized workflows, loaded only when relevant."),
|
||||
when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
|
||||
enabledCommands: ['workbench.command.new.skill'],
|
||||
excludeWhenCommandsExecuted: ['workbench.command.new.skill'],
|
||||
excludeWhenPromptFilesExist: { promptType: PromptsType.skill, excludeUntilChecked: true },
|
||||
},
|
||||
{
|
||||
id: 'tip.messageQueueing',
|
||||
message: localize('tip.messageQueueing', "Tip: Steer the agent mid-task by sending follow-up messages. They queue and apply in order."),
|
||||
@@ -324,7 +337,6 @@ export class TipEligibilityTracker extends Disposable {
|
||||
private static readonly _COMMANDS_STORAGE_KEY = 'chat.tips.executedCommands';
|
||||
private static readonly _MODES_STORAGE_KEY = 'chat.tips.usedModes';
|
||||
private static readonly _TOOLS_STORAGE_KEY = 'chat.tips.invokedTools';
|
||||
private static readonly _INSTRUCTION_FILES_EVER_DETECTED_KEY = 'chat.tips.instructionFilesEverDetected';
|
||||
|
||||
private readonly _executedCommands: Set<string>;
|
||||
private readonly _usedModes: Set<string>;
|
||||
@@ -349,7 +361,6 @@ export class TipEligibilityTracker extends Disposable {
|
||||
/** Generation counter per tip ID to discard stale async file-check results. */
|
||||
private readonly _fileCheckGeneration = new Map<string, number>();
|
||||
private readonly _fileChecksInFlight = new Map<string, Promise<void>>();
|
||||
private _instructionFilesEverDetected: boolean;
|
||||
|
||||
constructor(
|
||||
tips: readonly ITipDefinition[],
|
||||
@@ -372,8 +383,6 @@ export class TipEligibilityTracker extends Disposable {
|
||||
const storedTools = this._readApplicationWithProfileFallback(TipEligibilityTracker._TOOLS_STORAGE_KEY);
|
||||
this._invokedTools = new Set<string>(storedTools ? JSON.parse(storedTools) : []);
|
||||
|
||||
this._instructionFilesEverDetected = this._storageService.getBoolean(TipEligibilityTracker._INSTRUCTION_FILES_EVER_DETECTED_KEY, StorageScope.APPLICATION, false);
|
||||
|
||||
// --- Derive what still needs tracking ----------------------------------
|
||||
|
||||
this._pendingCommands = new Set<string>();
|
||||
@@ -407,15 +416,7 @@ export class TipEligibilityTracker extends Disposable {
|
||||
|
||||
if (this._pendingCommands.size > 0) {
|
||||
this._commandListener.value = commandService.onDidExecuteCommand(e => {
|
||||
if (this._pendingCommands.has(e.commandId)) {
|
||||
this._executedCommands.add(e.commandId);
|
||||
this._persistSet(TipEligibilityTracker._COMMANDS_STORAGE_KEY, this._executedCommands);
|
||||
this._pendingCommands.delete(e.commandId);
|
||||
|
||||
if (this._pendingCommands.size === 0) {
|
||||
this._commandListener.clear();
|
||||
}
|
||||
}
|
||||
this.recordCommandExecuted(e.commandId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -441,11 +442,6 @@ export class TipEligibilityTracker extends Disposable {
|
||||
|
||||
this._tipsWithFileExclusions = tips.filter(t => t.excludeWhenPromptFilesExist);
|
||||
for (const tip of this._tipsWithFileExclusions) {
|
||||
if (this._instructionFilesEverDetected && tip.id === 'tip.customInstructions') {
|
||||
this._excludedByFiles.add(tip.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tip.excludeWhenPromptFilesExist!.excludeUntilChecked) {
|
||||
this._excludedByFiles.add(tip.id);
|
||||
}
|
||||
@@ -462,6 +458,20 @@ export class TipEligibilityTracker extends Disposable {
|
||||
}));
|
||||
}
|
||||
|
||||
recordCommandExecuted(commandId: string): void {
|
||||
if (!this._pendingCommands.has(commandId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._executedCommands.add(commandId);
|
||||
this._persistSet(TipEligibilityTracker._COMMANDS_STORAGE_KEY, this._executedCommands);
|
||||
this._pendingCommands.delete(commandId);
|
||||
|
||||
if (this._pendingCommands.size === 0) {
|
||||
this._commandListener.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the current chat mode (kind + name) so future tip eligibility
|
||||
* checks can exclude mode-related tips. No-ops once all tracked modes
|
||||
@@ -531,11 +541,6 @@ export class TipEligibilityTracker extends Disposable {
|
||||
*/
|
||||
refreshPromptFileExclusions(): void {
|
||||
for (const tip of this._tipsWithFileExclusions) {
|
||||
if (this._instructionFilesEverDetected && tip.id === 'tip.customInstructions') {
|
||||
this._excludedByFiles.add(tip.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tip.excludeWhenPromptFilesExist!.excludeUntilChecked) {
|
||||
this._excludedByFiles.add(tip.id);
|
||||
}
|
||||
@@ -583,11 +588,6 @@ export class TipEligibilityTracker extends Disposable {
|
||||
: false;
|
||||
const hasPromptFilesOrAgentFile = hasPromptFiles || hasAgentFile;
|
||||
|
||||
if (tip.id === 'tip.customInstructions' && hasPromptFilesOrAgentFile) {
|
||||
this._instructionFilesEverDetected = true;
|
||||
this._storageService.store(TipEligibilityTracker._INSTRUCTION_FILES_EVER_DETECTED_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
|
||||
}
|
||||
|
||||
if (hasPromptFilesOrAgentFile) {
|
||||
this._excludedByFiles.add(tip.id);
|
||||
} else {
|
||||
@@ -684,6 +684,22 @@ export class ChatTipService extends Disposable implements IChatTipService {
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._chatService.onDidSubmitRequest(e => {
|
||||
const message = e.message ?? this._chatService.getSession(e.chatSessionResource)?.lastRequest?.message;
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._hasFileOrFolderReference(message)) {
|
||||
this._tracker.recordCommandExecuted(ATTACH_FILES_REFERENCE_TRACKING_COMMAND);
|
||||
}
|
||||
|
||||
const createCommandTrackingId = this._getCreateSlashCommandTrackingId(message);
|
||||
if (createCommandTrackingId) {
|
||||
this._tracker.recordCommandExecuted(createCommandTrackingId);
|
||||
}
|
||||
}));
|
||||
|
||||
// Track whether yolo mode was ever enabled
|
||||
this._yoloModeEverEnabled = this._storageService.getBoolean(ChatTipService._YOLO_EVER_ENABLED_KEY, StorageScope.APPLICATION, false);
|
||||
if (!this._yoloModeEverEnabled && this._configurationService.getValue<boolean>(ChatConfiguration.GlobalAutoApprove)) {
|
||||
@@ -718,6 +734,45 @@ export class ChatTipService extends Disposable implements IChatTipService {
|
||||
}
|
||||
}
|
||||
|
||||
private _hasFileOrFolderReference(message: IParsedChatRequest): boolean {
|
||||
return message.parts.some(part => {
|
||||
if (part.kind !== ChatRequestDynamicVariablePart.Kind) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dynamicPart = part as ChatRequestDynamicVariablePart;
|
||||
return dynamicPart.isFile === true || dynamicPart.isDirectory === true;
|
||||
});
|
||||
}
|
||||
|
||||
private _getCreateSlashCommandTrackingId(message: IParsedChatRequest): string | undefined {
|
||||
for (const part of message.parts) {
|
||||
if (part.kind === ChatRequestSlashCommandPart.Kind) {
|
||||
const slashCommand = (part as ChatRequestSlashCommandPart).slashCommand.command;
|
||||
return this._toCreateSlashCommandTrackingId(slashCommand);
|
||||
}
|
||||
}
|
||||
|
||||
const trimmed = message.text.trimStart();
|
||||
const match = /^\/(create-(?:instruction|prompt|agent|skill))(?:\s|$)/.exec(trimmed);
|
||||
return match ? this._toCreateSlashCommandTrackingId(match[1]) : undefined;
|
||||
}
|
||||
|
||||
private _toCreateSlashCommandTrackingId(command: string): string | undefined {
|
||||
switch (command) {
|
||||
case 'create-instruction':
|
||||
return CREATE_INSTRUCTION_TRACKING_COMMAND;
|
||||
case 'create-prompt':
|
||||
return CREATE_PROMPT_TRACKING_COMMAND;
|
||||
case 'create-agent':
|
||||
return CREATE_AGENT_TRACKING_COMMAND;
|
||||
case 'create-skill':
|
||||
return CREATE_SKILL_TRACKING_COMMAND;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
resetSession(): void {
|
||||
this._shownTip = undefined;
|
||||
this._tipRequestId = undefined;
|
||||
|
||||
@@ -21,13 +21,12 @@ export class CreateSlashCommandsUsageTracker extends Disposable {
|
||||
super();
|
||||
|
||||
this._register(this._chatService.onDidSubmitRequest(e => {
|
||||
const model = this._chatService.getSession(e.chatSessionResource);
|
||||
const lastRequest = model?.lastRequest;
|
||||
if (!lastRequest) {
|
||||
const message = e.message ?? this._chatService.getSession(e.chatSessionResource)?.lastRequest?.message;
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const part of lastRequest.message.parts) {
|
||||
for (const part of message.parts) {
|
||||
if (part.kind === ChatRequestSlashCommandPart.Kind) {
|
||||
const slash = part as ChatRequestSlashCommandPart;
|
||||
if (CreateSlashCommandsUsageTracker._isCreateSlashCommand(slash.slashCommand.command)) {
|
||||
@@ -38,7 +37,7 @@ export class CreateSlashCommandsUsageTracker extends Disposable {
|
||||
}
|
||||
|
||||
// Fallback when parsing doesn't produce a slash command part.
|
||||
const trimmed = lastRequest.message.text.trimStart();
|
||||
const trimmed = message.text.trimStart();
|
||||
const match = /^\/(create-(?:instruction|prompt|agent|skill))(?:\s|$)/.exec(trimmed);
|
||||
if (match && CreateSlashCommandsUsageTracker._isCreateSlashCommand(match[1])) {
|
||||
this._markUsed();
|
||||
|
||||
@@ -1345,7 +1345,7 @@ export interface IChatService {
|
||||
_serviceBrand: undefined;
|
||||
transferredSessionResource: URI | undefined;
|
||||
|
||||
readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }>;
|
||||
readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>;
|
||||
|
||||
readonly onDidCreateModel: Event<IChatModel>;
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ export class ChatService extends Disposable implements IChatService {
|
||||
return this._transferredSessionResource;
|
||||
}
|
||||
|
||||
private readonly _onDidSubmitRequest = this._register(new Emitter<{ readonly chatSessionResource: URI }>());
|
||||
private readonly _onDidSubmitRequest = this._register(new Emitter<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>());
|
||||
public readonly onDidSubmitRequest = this._onDidSubmitRequest.event;
|
||||
|
||||
public get onDidCreateModel() { return this._sessionModels.onDidCreateModel; }
|
||||
@@ -1228,7 +1228,7 @@ export class ChatService extends Disposable implements IChatService {
|
||||
if (options?.userSelectedModelId) {
|
||||
this.languageModelsService.addToRecentlyUsedList(options.userSelectedModelId);
|
||||
}
|
||||
this._onDidSubmitRequest.fire({ chatSessionResource: model.sessionResource });
|
||||
this._onDidSubmitRequest.fire({ chatSessionResource: model.sessionResource, message: parsedRequest });
|
||||
return {
|
||||
responseCreatedPromise: responseCreated.p,
|
||||
responseCompletePromise: rawResponsePromise,
|
||||
|
||||
@@ -28,7 +28,7 @@ import { TestChatEntitlementService } from '../../../../test/common/workbenchTes
|
||||
import { IChatService } from '../../common/chatService/chatService.js';
|
||||
import { MockChatService } from '../common/chatService/mockChatService.js';
|
||||
import { CreateSlashCommandsUsageTracker } from '../../browser/createSlashCommandsUsageTracker.js';
|
||||
import { ChatRequestSlashCommandPart } from '../../common/requestParser/chatParserTypes.js';
|
||||
import { ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, IParsedChatRequest } from '../../common/requestParser/chatParserTypes.js';
|
||||
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
|
||||
import { Range } from '../../../../../editor/common/core/range.js';
|
||||
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
|
||||
@@ -117,6 +117,62 @@ suite('ChatTipService', () => {
|
||||
assert.ok(tip.content.value.length > 0, 'Tip should have content');
|
||||
});
|
||||
|
||||
test('records # file reference usage for attach files tip eligibility', () => {
|
||||
const submitRequestEmitter = testDisposables.add(new Emitter<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>());
|
||||
instantiationService.stub(IChatService, {
|
||||
onDidSubmitRequest: submitRequestEmitter.event,
|
||||
getSession: () => undefined,
|
||||
} as Partial<IChatService> as IChatService);
|
||||
|
||||
createService();
|
||||
|
||||
submitRequestEmitter.fire({
|
||||
chatSessionResource: URI.parse('chat:session-attach-file'),
|
||||
message: {
|
||||
text: 'what does #file:README.md say',
|
||||
parts: [new ChatRequestDynamicVariablePart(
|
||||
new OffsetRange(10, 26),
|
||||
new Range(1, 11, 1, 27),
|
||||
'#file:README.md',
|
||||
'file',
|
||||
undefined,
|
||||
URI.file('/workspace/README.md'),
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
false,
|
||||
)],
|
||||
},
|
||||
});
|
||||
|
||||
const executedCommands = JSON.parse(storageService.get('chat.tips.executedCommands', StorageScope.APPLICATION) ?? '[]') as string[];
|
||||
assert.ok(executedCommands.includes('chat.tips.attachFiles.referenceUsed'));
|
||||
});
|
||||
|
||||
test('records only matching create tip usage for submitted create command', () => {
|
||||
const submitRequestEmitter = testDisposables.add(new Emitter<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>());
|
||||
instantiationService.stub(IChatService, {
|
||||
onDidSubmitRequest: submitRequestEmitter.event,
|
||||
getSession: () => undefined,
|
||||
} as Partial<IChatService> as IChatService);
|
||||
|
||||
createService();
|
||||
|
||||
submitRequestEmitter.fire({
|
||||
chatSessionResource: URI.parse('chat:session-create-prompt'),
|
||||
message: {
|
||||
text: '/create-prompt scaffold a reusable prompt',
|
||||
parts: [],
|
||||
},
|
||||
});
|
||||
|
||||
const executedCommands = JSON.parse(storageService.get('chat.tips.executedCommands', StorageScope.APPLICATION) ?? '[]') as string[];
|
||||
assert.ok(executedCommands.includes('chat.tips.createPrompt.commandUsed'));
|
||||
assert.ok(!executedCommands.includes('chat.tips.createInstruction.commandUsed'));
|
||||
assert.ok(!executedCommands.includes('chat.tips.createAgent.commandUsed'));
|
||||
assert.ok(!executedCommands.includes('chat.tips.createSkill.commandUsed'));
|
||||
});
|
||||
|
||||
test('returns Auto switch tip when current model is gpt-4.1', () => {
|
||||
const service = createService();
|
||||
contextKeyService.createKey(ChatContextKeys.chatModelId.key, 'gpt-4.1');
|
||||
@@ -846,53 +902,55 @@ suite('ChatTipService', () => {
|
||||
assert.strictEqual(tracker.isExcluded(tip), false, 'Should not be excluded when no skill files exist');
|
||||
});
|
||||
|
||||
test('shows tip.createSlashCommands when context key is false', () => {
|
||||
test('shows all create slash command tips in local chat sessions', () => {
|
||||
const service = createService();
|
||||
contextKeyService.createKey(ChatContextKeys.hasUsedCreateSlashCommands.key, false);
|
||||
contextKeyService.createKey(ChatContextKeys.chatSessionType.key, localChatSessionType);
|
||||
|
||||
// Dismiss tips until we find createSlashCommands or run out
|
||||
let found = false;
|
||||
const expectedCreateTips = new Set(['tip.createInstruction', 'tip.createPrompt', 'tip.createAgent', 'tip.createSkill']);
|
||||
const seenCreateTips = new Set<string>();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const tip = service.getWelcomeTip(contextKeyService);
|
||||
if (!tip) {
|
||||
break;
|
||||
}
|
||||
if (tip.id === 'tip.createSlashCommands') {
|
||||
found = true;
|
||||
break;
|
||||
if (expectedCreateTips.has(tip.id)) {
|
||||
seenCreateTips.add(tip.id);
|
||||
if (seenCreateTips.size === expectedCreateTips.size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
service.dismissTip();
|
||||
}
|
||||
|
||||
assert.ok(found, 'Should eventually show tip.createSlashCommands when context key is false');
|
||||
assert.deepStrictEqual([...seenCreateTips].sort(), [...expectedCreateTips].sort());
|
||||
});
|
||||
|
||||
test('does not show tip.createSlashCommands in non-local chat sessions', () => {
|
||||
test('does not show create slash command tips in non-local chat sessions', () => {
|
||||
const service = createService();
|
||||
contextKeyService.createKey(ChatContextKeys.hasUsedCreateSlashCommands.key, false);
|
||||
contextKeyService.createKey(ChatContextKeys.chatSessionType.key, 'cloud');
|
||||
const createTipIds = new Set(['tip.createInstruction', 'tip.createPrompt', 'tip.createAgent', 'tip.createSkill']);
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const tip = service.getWelcomeTip(contextKeyService);
|
||||
if (!tip) {
|
||||
break;
|
||||
}
|
||||
assert.notStrictEqual(tip.id, 'tip.createSlashCommands', 'Should not show tip.createSlashCommands in non-local sessions');
|
||||
assert.ok(!createTipIds.has(tip.id), 'Should not show create slash command tips in non-local sessions');
|
||||
service.dismissTip();
|
||||
}
|
||||
});
|
||||
|
||||
test('does not show tip.createSlashCommands when context key is true', () => {
|
||||
storageService.store('chat.tips.usedCreateSlashCommands', true, StorageScope.APPLICATION, StorageTarget.MACHINE);
|
||||
test('does not show create prompt tip when create prompt was already used', () => {
|
||||
storageService.store('chat.tips.executedCommands', JSON.stringify(['chat.tips.createPrompt.commandUsed']), StorageScope.APPLICATION, StorageTarget.MACHINE);
|
||||
const service = createService();
|
||||
contextKeyService.createKey(ChatContextKeys.chatSessionType.key, localChatSessionType);
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const tip = service.getWelcomeTip(contextKeyService);
|
||||
if (!tip) {
|
||||
break;
|
||||
}
|
||||
assert.notStrictEqual(tip.id, 'tip.createSlashCommands', 'Should not show tip.createSlashCommands when context key is true');
|
||||
assert.notStrictEqual(tip.id, 'tip.createPrompt', 'Should not show tip.createPrompt when create-prompt was used');
|
||||
service.dismissTip();
|
||||
}
|
||||
});
|
||||
@@ -1269,36 +1327,6 @@ suite('ChatTipService', () => {
|
||||
assert.strictEqual(tracker.isExcluded(tip), true, 'Should be excluded after refresh finds instruction files');
|
||||
});
|
||||
|
||||
test('keeps tip.customInstructions excluded after instruction files were detected once', async () => {
|
||||
const tip: ITipDefinition = {
|
||||
id: 'tip.customInstructions',
|
||||
message: 'test',
|
||||
excludeWhenPromptFilesExist: { promptType: PromptsType.instructions, agentFileType: AgentFileType.copilotInstructionsMd, excludeUntilChecked: true },
|
||||
};
|
||||
|
||||
const tracker1 = testDisposables.add(new TipEligibilityTracker(
|
||||
[tip],
|
||||
{ onDidExecuteCommand: Event.None, onWillExecuteCommand: Event.None } as Partial<ICommandService> as ICommandService,
|
||||
storageService,
|
||||
createMockPromptsService([], [{ uri: URI.file('/.github/instructions/coding.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions }]) as IPromptsService,
|
||||
createMockToolsService(),
|
||||
new NullLogService(),
|
||||
));
|
||||
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
assert.strictEqual(tracker1.isExcluded(tip), true, 'Should be excluded when instruction files exist');
|
||||
|
||||
const tracker2 = testDisposables.add(new TipEligibilityTracker(
|
||||
[tip],
|
||||
{ onDidExecuteCommand: Event.None, onWillExecuteCommand: Event.None } as Partial<ICommandService> as ICommandService,
|
||||
storageService,
|
||||
createMockPromptsService() as IPromptsService,
|
||||
createMockToolsService(),
|
||||
new NullLogService(),
|
||||
));
|
||||
|
||||
assert.strictEqual(tracker2.isExcluded(tip), true, 'Should remain excluded based on persisted detection signal');
|
||||
});
|
||||
});
|
||||
|
||||
suite('CreateSlashCommandsUsageTracker', () => {
|
||||
@@ -1306,13 +1334,13 @@ suite('CreateSlashCommandsUsageTracker', () => {
|
||||
|
||||
let storageService: InMemoryStorageService;
|
||||
let contextKeyService: MockContextKeyService;
|
||||
let submitRequestEmitter: Emitter<{ readonly chatSessionResource: URI }>;
|
||||
let submitRequestEmitter: Emitter<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>;
|
||||
let sessions: Map<string, { lastRequest: { message: { text: string; parts: readonly { kind: string }[] } } | undefined }>;
|
||||
|
||||
setup(() => {
|
||||
storageService = testDisposables.add(new InMemoryStorageService());
|
||||
contextKeyService = new MockContextKeyService();
|
||||
submitRequestEmitter = testDisposables.add(new Emitter<{ readonly chatSessionResource: URI }>());
|
||||
submitRequestEmitter = testDisposables.add(new Emitter<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>());
|
||||
sessions = new Map();
|
||||
});
|
||||
|
||||
@@ -1425,6 +1453,26 @@ suite('CreateSlashCommandsUsageTracker', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('detects create command from submitted message payload when session has no last request', () => {
|
||||
const sessionResource = URI.parse('chat:session-payload');
|
||||
const tracker = createTracker();
|
||||
tracker.syncContextKey(contextKeyService);
|
||||
|
||||
submitRequestEmitter.fire({
|
||||
chatSessionResource: sessionResource,
|
||||
message: {
|
||||
text: '/create-prompt payload-test',
|
||||
parts: [],
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
storageService.getBoolean('chat.tips.usedCreateSlashCommands', StorageScope.APPLICATION, false),
|
||||
true,
|
||||
'Storage should persist usage detected from submitted message payload',
|
||||
);
|
||||
});
|
||||
|
||||
test('does not mark used for non-create slash commands', () => {
|
||||
const sessionResource = URI.parse('chat:session4');
|
||||
const tracker = createTracker();
|
||||
|
||||
@@ -20,7 +20,7 @@ export class MockChatService implements IChatService {
|
||||
_serviceBrand: undefined;
|
||||
editingSessions = [];
|
||||
transferredSessionResource: URI | undefined;
|
||||
readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }> = Event.None;
|
||||
readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }> = Event.None;
|
||||
readonly onDidCreateModel: Event<IChatModel> = Event.None;
|
||||
|
||||
private sessions = new ResourceMap<IChatModel>();
|
||||
|
||||
Reference in New Issue
Block a user