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
23 changes: 23 additions & 0 deletions src/vs/workbench/contrib/chat/browser/actions/chatActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '',
Expand Down Expand Up @@ -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<void> {
const commandService = accessor.get(ICommandService);
await commandService.executeCommand('workbench.action.chat.open', {
query: '/fork ',
isPartialQuery: true,
});
}
});

registerAction2(class OpenChatFeatureSettingsAction extends Action2 {
constructor() {
super({
Expand Down
21 changes: 21 additions & 0 deletions src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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() {
Expand Down
20 changes: 12 additions & 8 deletions src/vs/workbench/contrib/chat/browser/chatTipService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IChatTipService>('chatTipService');

Expand Down Expand Up @@ -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);
}
}));

Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<IChatService> 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', () => {
Expand Down
Loading