Skip to content

Commit 9f6e415

Browse files
authored
add more tips, make all of them contextual (microsoft#294237)
fixes microsoft#290019
1 parent 0991a42 commit 9f6e415

6 files changed

Lines changed: 460 additions & 47 deletions

File tree

src/vs/workbench/contrib/chat/browser/actions/chatActions.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,24 @@ export function registerChatActions() {
809809
}
810810
});
811811

812+
registerAction2(class ShowContextUsageAction extends Action2 {
813+
constructor() {
814+
super({
815+
id: 'workbench.action.chat.showContextUsage',
816+
title: localize2('interactiveSession.showContextUsage.label', "Show Context Window Usage"),
817+
category: CHAT_CATEGORY,
818+
f1: true,
819+
precondition: ChatContextKeys.enabled,
820+
});
821+
}
822+
823+
async run(accessor: ServicesAccessor): Promise<void> {
824+
const widgetService = accessor.get(IChatWidgetService);
825+
const widget = widgetService.lastFocusedWidget ?? (await widgetService.revealWidget());
826+
widget?.input.showContextUsageDetails();
827+
}
828+
});
829+
812830
const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.completionsAdvancedSetting}.authProvider`, defaultChat.provider.enterprise.id));
813831
registerAction2(class extends Action2 {
814832
constructor() {

src/vs/workbench/contrib/chat/browser/chatTipService.ts

Lines changed: 198 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { PromptsType } from '../common/promptSyntax/promptTypes.js';
1919
import { CancellationToken } from '../../../../base/common/cancellation.js';
2020
import { localize } from '../../../../nls.js';
2121
import { ILogService } from '../../../../platform/log/common/log.js';
22+
import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js';
2223

2324
export const IChatTipService = createDecorator<IChatTipService>('chatTipService');
2425

@@ -85,6 +86,21 @@ export interface ITipDefinition {
8586
* Matches against both mode kind (e.g. 'agent') and mode name (e.g. 'Plan').
8687
*/
8788
readonly excludeWhenModesUsed?: string[];
89+
/**
90+
* Tool IDs that, if ever invoked in this workspace, make this tip ineligible.
91+
* The tip won't be shown if the tool it describes has already been used.
92+
*/
93+
readonly excludeWhenToolsInvoked?: string[];
94+
/**
95+
* If set, exclude this tip when prompt files of the specified type exist in the workspace.
96+
*/
97+
readonly excludeWhenPromptFilesExist?: {
98+
readonly promptType: PromptsType;
99+
/** Also check for this specific agent instruction file type. */
100+
readonly agentFileType?: AgentFileType;
101+
/** If true, exclude the tip until the async file check completes. Default: false. */
102+
readonly excludeUntilChecked?: boolean;
103+
};
88104
}
89105

90106
/**
@@ -108,10 +124,12 @@ const TIP_CATALOG: ITipDefinition[] = [
108124
{
109125
id: 'tip.attachFiles',
110126
message: localize('tip.attachFiles', "Tip: Attach files or folders with # to give Copilot more context."),
127+
excludeWhenCommandsExecuted: ['workbench.action.chat.attachContext', 'workbench.action.chat.attachFile', 'workbench.action.chat.attachFolder', 'workbench.action.chat.attachSelection'],
111128
},
112129
{
113130
id: 'tip.codeActions',
114131
message: localize('tip.codeActions', "Tip: Select code and right-click for Copilot actions in the context menu."),
132+
excludeWhenCommandsExecuted: ['inlineChat.start'],
115133
},
116134
{
117135
id: 'tip.undoChanges',
@@ -126,7 +144,78 @@ const TIP_CATALOG: ITipDefinition[] = [
126144
id: 'tip.customInstructions',
127145
message: localize('tip.customInstructions', "Tip: [Generate workspace instructions](command:workbench.action.chat.generateInstructions) so Copilot always has the context it needs when starting a task."),
128146
enabledCommands: ['workbench.action.chat.generateInstructions'],
129-
}
147+
excludeWhenPromptFilesExist: { promptType: PromptsType.instructions, agentFileType: AgentFileType.copilotInstructionsMd, excludeUntilChecked: true },
148+
},
149+
{
150+
id: 'tip.customAgent',
151+
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."),
152+
when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
153+
enabledCommands: ['workbench.command.new.agent'],
154+
excludeWhenCommandsExecuted: ['workbench.command.new.agent'],
155+
excludeWhenPromptFilesExist: { promptType: PromptsType.agent, excludeUntilChecked: true },
156+
},
157+
{
158+
id: 'tip.skill',
159+
message: localize('tip.skill', "Tip: [Create a skill](command:workbench.command.new.skill) so agents can perform domain-specific tasks with reusable prompts and tools."),
160+
when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
161+
enabledCommands: ['workbench.command.new.skill'],
162+
excludeWhenCommandsExecuted: ['workbench.command.new.skill'],
163+
excludeWhenPromptFilesExist: { promptType: PromptsType.skill, excludeUntilChecked: true },
164+
},
165+
{
166+
id: 'tip.messageQueueing',
167+
message: localize('tip.messageQueueing', "Tip: You can send follow-up messages while the agent is working. They'll be queued and processed in order."),
168+
when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
169+
excludeWhenCommandsExecuted: ['workbench.action.chat.queueMessage', 'workbench.action.chat.steerWithMessage'],
170+
},
171+
{
172+
id: 'tip.yoloMode',
173+
message: localize('tip.yoloMode', "Tip: Enable [auto-approve mode](command:workbench.action.openSettings?%5B%22chat.tools.global.autoApprove%22%5D) to let the agent run tools without manual confirmation."),
174+
when: ContextKeyExpr.and(
175+
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
176+
ContextKeyExpr.notEquals('config.chat.tools.global.autoApprove', true),
177+
),
178+
enabledCommands: ['workbench.action.openSettings'],
179+
},
180+
{
181+
id: 'tip.mermaid',
182+
message: localize('tip.mermaid', "Tip: Ask the agent to visualize architectures and flows; it can render Mermaid diagrams directly in chat."),
183+
when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
184+
excludeWhenToolsInvoked: ['renderMermaidDiagram'],
185+
},
186+
{
187+
id: 'tip.githubRepo',
188+
message: localize('tip.githubRepo', "Tip: Mention a GitHub repository (e.g. @owner/repo) in your prompt so the agent can query code and issues across that repo."),
189+
when: ContextKeyExpr.and(
190+
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
191+
ContextKeyExpr.notEquals('gitOpenRepositoryCount', '0'),
192+
),
193+
excludeWhenToolsInvoked: ['github-pull-request_doSearch', 'github-pull-request_issue_fetch', 'github-pull-request_formSearchQuery'],
194+
},
195+
{
196+
id: 'tip.subagents',
197+
message: localize('tip.subagents', "Tip: Ask the agent to implement a plan in parallel; it can delegate work across subagents for faster results."),
198+
when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
199+
excludeWhenToolsInvoked: ['runSubagent'],
200+
},
201+
{
202+
id: 'tip.contextUsage',
203+
message: localize('tip.contextUsage', "Tip: [View your context window usage](command:workbench.action.chat.showContextUsage) to see how many tokens are being spent and what's consuming them."),
204+
when: ContextKeyExpr.and(
205+
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
206+
ChatContextKeys.contextUsageHasBeenOpened.negate(),
207+
ChatContextKeys.chatSessionIsEmpty.negate(),
208+
),
209+
enabledCommands: ['workbench.action.chat.showContextUsage'],
210+
excludeWhenCommandsExecuted: ['workbench.action.chat.showContextUsage'],
211+
},
212+
{
213+
id: 'tip.sendToNewChat',
214+
message: localize('tip.sendToNewChat', "Tip: Use [Send to New Chat](command:workbench.action.chat.sendToNewChat) to start a fresh conversation with a clean context window."),
215+
when: ChatContextKeys.chatSessionIsEmpty.negate(),
216+
enabledCommands: ['workbench.action.chat.sendToNewChat'],
217+
excludeWhenCommandsExecuted: ['workbench.action.chat.sendToNewChat'],
218+
},
130219
];
131220

132221
/**
@@ -138,26 +227,38 @@ export class TipEligibilityTracker extends Disposable {
138227

139228
private static readonly _COMMANDS_STORAGE_KEY = 'chat.tips.executedCommands';
140229
private static readonly _MODES_STORAGE_KEY = 'chat.tips.usedModes';
230+
private static readonly _TOOLS_STORAGE_KEY = 'chat.tips.invokedTools';
141231

142232
private readonly _executedCommands: Set<string>;
143233
private readonly _usedModes: Set<string>;
234+
private readonly _invokedTools: Set<string>;
144235

145236
private readonly _pendingCommands: Set<string>;
146237
private readonly _pendingModes: Set<string>;
238+
private readonly _pendingTools: Set<string>;
147239

148240
private readonly _commandListener = this._register(new MutableDisposable());
241+
private readonly _toolListener = this._register(new MutableDisposable());
149242

150243
/**
151-
* Whether agent instruction files exist in the workspace.
152-
* Defaults to `true` (hide the tip) until the async check completes.
244+
* Tip IDs excluded because prompt files of the required type exist in the workspace.
245+
* Tips with `excludeUntilChecked` are pre-added and removed if no files are found.
153246
*/
154-
private _hasInstructionFiles = true;
247+
private readonly _excludedByFiles = new Set<string>();
248+
249+
/** Tips that have file-based exclusions, kept for re-checks. */
250+
private readonly _tipsWithFileExclusions: readonly ITipDefinition[];
251+
252+
/** Generation counter per tip ID to discard stale async file-check results. */
253+
private readonly _fileCheckGeneration = new Map<string, number>();
155254

156255
constructor(
157256
tips: readonly ITipDefinition[],
158257
@ICommandService commandService: ICommandService,
159258
@IStorageService private readonly _storageService: IStorageService,
160-
@IPromptsService promptsService: IPromptsService,
259+
@IPromptsService private readonly _promptsService: IPromptsService,
260+
@ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService,
261+
@ILogService private readonly _logService: ILogService,
161262
) {
162263
super();
163264

@@ -169,6 +270,9 @@ export class TipEligibilityTracker extends Disposable {
169270
const storedModes = this._storageService.get(TipEligibilityTracker._MODES_STORAGE_KEY, StorageScope.WORKSPACE);
170271
this._usedModes = new Set<string>(storedModes ? JSON.parse(storedModes) : []);
171272

273+
const storedTools = this._storageService.get(TipEligibilityTracker._TOOLS_STORAGE_KEY, StorageScope.WORKSPACE);
274+
this._invokedTools = new Set<string>(storedTools ? JSON.parse(storedTools) : []);
275+
172276
// --- Derive what still needs tracking ----------------------------------
173277

174278
this._pendingCommands = new Set<string>();
@@ -189,6 +293,15 @@ export class TipEligibilityTracker extends Disposable {
189293
}
190294
}
191295

296+
this._pendingTools = new Set<string>();
297+
for (const tip of tips) {
298+
for (const toolId of tip.excludeWhenToolsInvoked ?? []) {
299+
if (!this._invokedTools.has(toolId)) {
300+
this._pendingTools.add(toolId);
301+
}
302+
}
303+
}
304+
192305
// --- Set up command listener (auto-disposes when all seen) --------------
193306

194307
if (this._pendingCommands.size > 0) {
@@ -205,9 +318,40 @@ export class TipEligibilityTracker extends Disposable {
205318
});
206319
}
207320

208-
// --- Async file check --------------------------------------------------
321+
// --- Set up tool listener (auto-disposes when all seen) -----------------
322+
323+
if (this._pendingTools.size > 0) {
324+
this._toolListener.value = languageModelToolsService.onDidInvokeTool(e => {
325+
if (this._pendingTools.has(e.toolId)) {
326+
this._invokedTools.add(e.toolId);
327+
this._persistSet(TipEligibilityTracker._TOOLS_STORAGE_KEY, this._invokedTools);
328+
this._pendingTools.delete(e.toolId);
329+
330+
if (this._pendingTools.size === 0) {
331+
this._toolListener.clear();
332+
}
333+
}
334+
});
335+
}
336+
337+
// --- Async file checks -------------------------------------------------
338+
339+
this._tipsWithFileExclusions = tips.filter(t => t.excludeWhenPromptFilesExist);
340+
for (const tip of this._tipsWithFileExclusions) {
341+
if (tip.excludeWhenPromptFilesExist!.excludeUntilChecked) {
342+
this._excludedByFiles.add(tip.id);
343+
}
344+
this._checkForPromptFiles(tip);
345+
}
209346

210-
this._checkForInstructionFiles(promptsService);
347+
// Re-check agent file exclusions when custom agents change (covers late discovery)
348+
this._register(this._promptsService.onDidChangeCustomAgents(() => {
349+
for (const tip of this._tipsWithFileExclusions) {
350+
if (tip.excludeWhenPromptFilesExist!.promptType === PromptsType.agent) {
351+
this._checkForPromptFiles(tip);
352+
}
353+
}
354+
}));
211355
}
212356

213357
/**
@@ -245,33 +389,67 @@ export class TipEligibilityTracker extends Disposable {
245389
if (tip.excludeWhenCommandsExecuted) {
246390
for (const cmd of tip.excludeWhenCommandsExecuted) {
247391
if (this._executedCommands.has(cmd)) {
392+
this._logService.debug('#ChatTips: tip excluded because command was executed', tip.id, cmd);
248393
return true;
249394
}
250395
}
251396
}
252397
if (tip.excludeWhenModesUsed) {
253398
for (const mode of tip.excludeWhenModesUsed) {
254399
if (this._usedModes.has(mode)) {
400+
this._logService.debug('#ChatTips: tip excluded because mode was used', tip.id, mode);
401+
return true;
402+
}
403+
}
404+
}
405+
if (tip.excludeWhenToolsInvoked) {
406+
for (const toolId of tip.excludeWhenToolsInvoked) {
407+
if (this._invokedTools.has(toolId)) {
408+
this._logService.debug('#ChatTips: tip excluded because tool was invoked', tip.id, toolId);
255409
return true;
256410
}
257411
}
258412
}
259-
if (tip.id === 'tip.customInstructions' && this._hasInstructionFiles) {
413+
if (tip.excludeWhenPromptFilesExist && this._excludedByFiles.has(tip.id)) {
414+
this._logService.debug('#ChatTips: tip excluded because prompt files exist', tip.id);
260415
return true;
261416
}
262417
return false;
263418
}
264419

265-
private async _checkForInstructionFiles(promptsService: IPromptsService): Promise<void> {
420+
private async _checkForPromptFiles(tip: ITipDefinition): Promise<void> {
421+
const config = tip.excludeWhenPromptFilesExist!;
422+
const generation = (this._fileCheckGeneration.get(tip.id) ?? 0) + 1;
423+
this._fileCheckGeneration.set(tip.id, generation);
424+
266425
try {
267-
const [agentInstructions, instructionFiles] = await Promise.all([
268-
promptsService.listAgentInstructions(CancellationToken.None),
269-
promptsService.listPromptFiles(PromptsType.instructions, CancellationToken.None),
426+
const [promptFiles, agentInstructions] = await Promise.all([
427+
this._promptsService.listPromptFiles(config.promptType, CancellationToken.None),
428+
config.agentFileType ? this._promptsService.listAgentInstructions(CancellationToken.None) : Promise.resolve([]),
270429
]);
271-
const hasCopilotInstructions = agentInstructions.some(f => f.type === AgentFileType.copilotInstructionsMd);
272-
this._hasInstructionFiles = hasCopilotInstructions || instructionFiles.length > 0;
430+
431+
// Discard stale result if a newer check was started while we were awaiting
432+
if (this._fileCheckGeneration.get(tip.id) !== generation) {
433+
return;
434+
}
435+
436+
const hasPromptFiles = promptFiles.length > 0;
437+
const hasAgentFile = config.agentFileType
438+
? agentInstructions.some(f => f.type === config.agentFileType)
439+
: false;
440+
441+
if (hasPromptFiles || hasAgentFile) {
442+
this._excludedByFiles.add(tip.id);
443+
} else {
444+
this._excludedByFiles.delete(tip.id);
445+
}
273446
} catch {
274-
this._hasInstructionFiles = true;
447+
if (this._fileCheckGeneration.get(tip.id) !== generation) {
448+
return;
449+
}
450+
if (config.excludeUntilChecked) {
451+
this._excludedByFiles.add(tip.id);
452+
}
275453
}
276454
}
277455

@@ -413,7 +591,11 @@ export class ChatTipService extends Disposable implements IChatTipService {
413591
this._logService.debug('#ChatTips: tip is not eligible due to when clause', tip.id, tip.when.serialize());
414592
return false;
415593
}
416-
return !this._tracker.isExcluded(tip);
594+
if (this._tracker.isExcluded(tip)) {
595+
return false;
596+
}
597+
this._logService.debug('#ChatTips: tip is eligible', tip.id);
598+
return true;
417599
}
418600

419601
private _isCopilotEnabled(): boolean {

src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
16981698
}
16991699
}
17001700

1701+
/**
1702+
* Shows the context usage details popup and focuses it.
1703+
* @returns Whether the details were successfully shown.
1704+
*/
1705+
showContextUsageDetails(): boolean {
1706+
return this.contextUsageWidget?.showDetails() ?? false;
1707+
}
1708+
17011709
/**
17021710
* Updates the context usage widget based on the current model.
17031711
*/

0 commit comments

Comments
 (0)