diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index b6ea7312385629..7469b4377cae64 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -85,6 +85,7 @@ export const GENERATE_PROMPT_COMMAND_ID = 'workbench.action.chat.generatePrompt' export const GENERATE_SKILL_COMMAND_ID = 'workbench.action.chat.generateSkill'; export const GENERATE_AGENT_COMMAND_ID = 'workbench.action.chat.generateAgent'; export const GENERATE_HOOK_COMMAND_ID = 'workbench.action.chat.generateHook'; +export const INSERT_FORK_CONVERSATION_COMMAND_ID = 'workbench.action.chat.insertForkConversationCommand'; const defaultChat = { manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', @@ -1370,6 +1371,28 @@ export function registerChatActions() { } }); + registerAction2(class InsertForkConversationSlashCommandAction extends Action2 { + constructor() { + super({ + id: INSERT_FORK_CONVERSATION_COMMAND_ID, + title: localize2('insertForkConversationSlashCommand', "Insert Fork Command"), + shortTitle: localize2('insertForkConversationSlashCommand.short', "Insert /fork"), + category: CHAT_CATEGORY, + icon: Codicon.repoForked, + f1: true, + precondition: ChatContextKeys.enabled + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand('workbench.action.chat.open', { + query: '/fork ', + isPartialQuery: true, + }); + } + }); + registerAction2(class OpenChatFeatureSettingsAction extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts index d130efd72758d1..b29cc69ac81da2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts @@ -18,6 +18,7 @@ import { GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID, GENERATE_PROMPT_COMMAND_ID, GENERATE_SKILL_COMMAND_ID, + INSERT_FORK_CONVERSATION_COMMAND_ID, } from './actions/chatActions.js'; /** @@ -287,6 +288,26 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), excludeWhenCommandsExecuted: ['workbench.action.chat.queueMessage', 'workbench.action.chat.steerWithMessage'], }, + { + id: 'tip.forkConversation', + buildMessage(ctx) { + const kb = formatKeybinding(ctx, INSERT_FORK_CONVERSATION_COMMAND_ID); + return new MarkdownString( + localize( + 'tip.forkConversation', + "Use [{0}](command:{1}){2} to branch the conversation. Explore a different approach without losing the original context.", + '/fork', + INSERT_FORK_CONVERSATION_COMMAND_ID, + kb + ) + ); + }, + excludeWhenCommandsExecuted: [ + INSERT_FORK_CONVERSATION_COMMAND_ID, + 'workbench.action.chat.forkConversation', + TipTrackingCommands.ForkConversationUsed, + ], + }, { id: 'tip.yoloMode', buildMessage() { diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 3bc000338552c2..57ee53a820f0e6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -54,6 +54,8 @@ export const CREATE_PROMPT_TRACKING_COMMAND = TipTrackingCommands.CreatePromptUs export const CREATE_AGENT_TRACKING_COMMAND = TipTrackingCommands.CreateAgentUsed; /** @deprecated Use TipTrackingCommands.CreateSkillUsed */ export const CREATE_SKILL_TRACKING_COMMAND = TipTrackingCommands.CreateSkillUsed; +/** @deprecated Use TipTrackingCommands.ForkConversationUsed */ +export const FORK_CONVERSATION_TRACKING_COMMAND = TipTrackingCommands.ForkConversationUsed; export const IChatTipService = createDecorator('chatTipService'); @@ -216,9 +218,9 @@ export class ChatTipService extends Disposable implements IChatTipService { this._tracker.recordCommandExecuted(TipTrackingCommands.AttachFilesReferenceUsed); } - const createCommandTrackingId = this._getCreateSlashCommandTrackingId(message); - if (createCommandTrackingId) { - this._tracker.recordCommandExecuted(createCommandTrackingId); + const slashCommandTrackingId = this._getSlashCommandTrackingId(message); + if (slashCommandTrackingId) { + this._tracker.recordCommandExecuted(slashCommandTrackingId); } })); @@ -267,20 +269,20 @@ export class ChatTipService extends Disposable implements IChatTipService { }); } - private _getCreateSlashCommandTrackingId(message: IParsedChatRequest): string | undefined { + private _getSlashCommandTrackingId(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); + return this._toSlashCommandTrackingId(slashCommand); } } const trimmed = message.text.trimStart(); - const match = /^\/(create-(?:instructions|prompt|agent|skill))(?:\s|$)/.exec(trimmed); - return match ? this._toCreateSlashCommandTrackingId(match[1]) : undefined; + const match = /^\/(create-(?:instructions|prompt|agent|skill)|fork)(?:\s|$)/.exec(trimmed); + return match ? this._toSlashCommandTrackingId(match[1]) : undefined; } - private _toCreateSlashCommandTrackingId(command: string): string | undefined { + private _toSlashCommandTrackingId(command: string): string | undefined { switch (command) { case 'create-instructions': return CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND; @@ -290,6 +292,8 @@ export class ChatTipService extends Disposable implements IChatTipService { return CREATE_AGENT_TRACKING_COMMAND; case 'create-skill': return CREATE_SKILL_TRACKING_COMMAND; + case 'fork': + return FORK_CONVERSATION_TRACKING_COMMAND; default: return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts b/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts index 1b7a995f8c86bc..f7ccacdd94ccc9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts @@ -45,4 +45,6 @@ export const TipTrackingCommands = { CreateAgentUsed: 'chat.tips.createAgent.commandUsed', /** Tracked when user executes /create-skill. */ CreateSkillUsed: 'chat.tips.createSkill.commandUsed', + /** Tracked when user executes /fork. */ + ForkConversationUsed: 'chat.tips.forkConversation.commandUsed', } as const; diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts index ba43aa820ee2c8..f6e5316b0bb0bf 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -17,7 +17,7 @@ import { MockContextKeyService } from '../../../../../platform/keybinding/test/c import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { ChatTipService, CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND, CREATE_AGENT_TRACKING_COMMAND, CREATE_PROMPT_TRACKING_COMMAND, CREATE_SKILL_TRACKING_COMMAND, IChatTip, ITipDefinition, TipEligibilityTracker } from '../../browser/chatTipService.js'; +import { ChatTipService, CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND, CREATE_AGENT_TRACKING_COMMAND, CREATE_PROMPT_TRACKING_COMMAND, CREATE_SKILL_TRACKING_COMMAND, FORK_CONVERSATION_TRACKING_COMMAND, IChatTip, ITipDefinition, TipEligibilityTracker } from '../../browser/chatTipService.js'; import { AgentFileType, IPromptPath, IPromptsService, IResolvedAgentFile, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { URI } from '../../../../../base/common/uri.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; @@ -189,6 +189,32 @@ suite('ChatTipService', () => { assert.ok(!executedCommands.includes(CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND)); assert.ok(!executedCommands.includes(CREATE_AGENT_TRACKING_COMMAND)); assert.ok(!executedCommands.includes(CREATE_SKILL_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(FORK_CONVERSATION_TRACKING_COMMAND)); + }); + + test('records fork tip usage for submitted /fork command', () => { + const submitRequestEmitter = testDisposables.add(new Emitter<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>()); + instantiationService.stub(IChatService, { + onDidSubmitRequest: submitRequestEmitter.event, + getSession: () => undefined, + } as Partial as IChatService); + + createService(); + + submitRequestEmitter.fire({ + chatSessionResource: URI.parse('chat:session-fork'), + message: { + text: '/fork', + parts: [], + }, + }); + + const executedCommands = JSON.parse(storageService.get('chat.tips.executedCommands', StorageScope.APPLICATION) ?? '[]') as string[]; + assert.ok(executedCommands.includes(FORK_CONVERSATION_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(CREATE_PROMPT_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(CREATE_AGENT_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(CREATE_SKILL_TRACKING_COMMAND)); }); test('returns Auto switch tip when current model is gpt-4.1', () => {