Skip to content

Commit 6a3ec62

Browse files
authored
Minor cleanup for resolve customizations view (#308299)
1 parent a417408 commit 6a3ec62

11 files changed

Lines changed: 182 additions & 114 deletions

File tree

src/vs/platform/agentPlugins/common/pluginParsers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export interface IParsedHookCommand {
3434
readonly env?: Record<string, string>;
3535
/** Timeout in seconds. */
3636
readonly timeout?: number;
37+
/** URI of the file this hook was defined in. */
38+
readonly sourceUri?: URI;
3739
}
3840

3941
/** A group of hooks for a single lifecycle event. */

src/vs/workbench/contrib/chat/browser/chatDebug/chatCustomizationDiscoveryRenderer.ts

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ function createInlineFileLink(uri: URI, displayText: string, fileKind: FileKind,
7474
disposables.add(DOM.addDisposableListener(link, DOM.EventType.CLICK, (e) => {
7575
e.preventDefault();
7676
e.stopPropagation();
77-
openerService.open(uri);
77+
openerService.open(uri, { editorOptions: { preserveFocus: true } });
7878
}));
7979

8080
return link;
@@ -194,7 +194,7 @@ export function renderCustomizationDiscoveryContent(content: IChatDebugEventFile
194194
const relativeLabel = labelService.getUriLabel(file.uri, { relative: true });
195195
row.setAttribute('aria-label', relativeLabel);
196196
const uri = file.uri;
197-
rows.push({ element: row, activate: () => openerService.open(uri) });
197+
rows.push({ element: row, activate: () => openerService.open(uri, { editorOptions: { preserveFocus: true } }) });
198198
}
199199
}
200200
setupFileListNavigation(listEl, rows, disposables);
@@ -252,7 +252,7 @@ export function renderCustomizationDiscoveryContent(content: IChatDebugEventFile
252252
const relativeLabel = labelService.getUriLabel(file.uri, { relative: true });
253253
row.setAttribute('aria-label', relativeLabel);
254254
const uri = file.uri;
255-
rows.push({ element: row, activate: () => openerService.open(uri) });
255+
rows.push({ element: row, activate: () => openerService.open(uri, { editorOptions: { preserveFocus: true } }) });
256256
}
257257
}
258258
setupFileListNavigation(listEl, rows, disposables);
@@ -455,29 +455,68 @@ export function renderCustomizationSummaryContent(content: IChatDebugEventCustom
455455
listEl.setAttribute('aria-label', title);
456456

457457
const rows: { element: HTMLElement; activate: () => void }[] = [];
458-
for (const entry of entries) {
459-
const row = DOM.append(listEl, $('div.chat-debug-file-list-row'));
460-
DOM.append(row, $(`span.chat-debug-file-list-icon${ThemeIcon.asCSSSelector(icon)}`));
461-
462-
// Hide the reason for skills (e.g. "local") and hooks — it's noise in the UI.
463-
const showReason = entry.category !== 'skill' && entry.category !== 'hook' && entry.category !== 'custom-agent';
464-
465-
if (entry.uri) {
466-
row.appendChild(createInlineFileLink(
467-
entry.uri, entry.name, FileKind.FILE,
468-
openerService, modelService, languageService, hoverService, labelService, disposables,
469-
showReason ? entry.reason : undefined
470-
));
471-
const uri = entry.uri;
472-
rows.push({ element: row, activate: () => openerService.open(uri) });
473-
} else {
474-
DOM.append(row, $('span', undefined, entry.name));
458+
459+
// For hooks, group entries by lifecycle event (stored in reason).
460+
const isHookSection = entries.length > 0 && entries[0].category === 'hook';
461+
if (isHookSection) {
462+
// Collect entries by hook type, preserving insertion order.
463+
const groupedByType = new Map<string, IChatDebugCustomizationLogEntry[]>();
464+
for (const entry of entries) {
465+
const hookType = entry.reason ?? '';
466+
let group = groupedByType.get(hookType);
467+
if (!group) {
468+
group = [];
469+
groupedByType.set(hookType, group);
470+
}
471+
group.push(entry);
475472
}
476473

477-
if (showReason && entry.reason) {
478-
DOM.append(row, $('span.chat-debug-file-list-detail', undefined, ` — ${entry.reason}`));
474+
for (const [hookType, groupEntries] of groupedByType) {
475+
if (hookType) {
476+
DOM.append(listEl, $('div.chat-debug-file-list-group-header', undefined, hookType));
477+
}
478+
for (const entry of groupEntries) {
479+
const row = DOM.append(listEl, $('div.chat-debug-file-list-row'));
480+
DOM.append(row, $(`span.chat-debug-file-list-icon${ThemeIcon.asCSSSelector(icon)}`));
481+
482+
if (entry.uri) {
483+
row.appendChild(createInlineFileLink(
484+
entry.uri, entry.name, FileKind.FILE,
485+
openerService, modelService, languageService, hoverService, labelService, disposables,
486+
));
487+
const uri = entry.uri;
488+
rows.push({ element: row, activate: () => openerService.open(uri, { editorOptions: { preserveFocus: true } }) });
489+
} else {
490+
DOM.append(row, $('span', undefined, entry.name));
491+
}
492+
row.setAttribute('aria-label', entry.reason ? `${entry.name}${entry.reason}` : entry.name);
493+
}
494+
}
495+
} else {
496+
for (const entry of entries) {
497+
const row = DOM.append(listEl, $('div.chat-debug-file-list-row'));
498+
DOM.append(row, $(`span.chat-debug-file-list-icon${ThemeIcon.asCSSSelector(icon)}`));
499+
500+
// Hide the reason for skills (e.g. "local") and custom-agents — it's noise in the UI.
501+
const showReason = entry.category !== 'skill' && entry.category !== 'custom-agent';
502+
503+
if (entry.uri) {
504+
row.appendChild(createInlineFileLink(
505+
entry.uri, entry.name, FileKind.FILE,
506+
openerService, modelService, languageService, hoverService, labelService, disposables,
507+
showReason ? entry.reason : undefined
508+
));
509+
const uri = entry.uri;
510+
rows.push({ element: row, activate: () => openerService.open(uri, { editorOptions: { preserveFocus: true } }) });
511+
} else {
512+
DOM.append(row, $('span', undefined, entry.name));
513+
}
514+
515+
if (showReason && entry.reason) {
516+
DOM.append(row, $('span.chat-debug-file-list-detail', undefined, ` — ${entry.reason}`));
517+
}
518+
row.setAttribute('aria-label', entry.reason ? `${entry.name}${entry.reason}` : entry.name);
479519
}
480-
row.setAttribute('aria-label', entry.reason ? `${entry.name}${entry.reason}` : entry.name);
481520
}
482521
setupFileListNavigation(listEl, rows, disposables);
483522
}

src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugDetailPanel.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,12 @@ export class ChatDebugDetailPanel extends Disposable {
278278
layout(height: number): void {
279279
const headerHeight = this.headerElement?.offsetHeight ?? 0;
280280
const scrollableHeight = Math.max(0, height - headerHeight);
281+
// Preserve scroll position across layout changes (e.g. when opening
282+
// an editor causes the workbench to re-layout this panel).
283+
const scrollPos = this.scrollable.getScrollPosition();
281284
this.contentContainer.style.height = `${scrollableHeight}px`;
282285
this.scrollable.scanDomNode();
286+
this.scrollable.setScrollPosition({ scrollTop: scrollPos.scrollTop });
283287
this.sash.layout();
284288
}
285289

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

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,22 @@
55

66
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
77
import { Disposable } from '../../../../base/common/lifecycle.js';
8+
import { OS } from '../../../../base/common/platform.js';
89
import { generateUuid } from '../../../../base/common/uuid.js';
910
import { localize } from '../../../../nls.js';
1011
import { ILogService } from '../../../../platform/log/common/log.js';
1112
import { IWorkbenchContribution } from '../../../common/contributions.js';
1213
import { IChatDebugCustomizationLogEntry, IChatDebugEventFileListContent, IChatDebugResolvedEventContent, IChatDebugService } from '../common/chatDebugService.js';
1314
import { IChatAgentService } from '../common/participants/chatAgents.js';
1415
import { IChatService } from '../common/chatService/chatService.js';
15-
import { ChatRequestHooks } from '../common/promptSyntax/hookSchema.js';
16+
import { ChatRequestHooks, formatHookCommandLabel } from '../common/promptSyntax/hookSchema.js';
1617
import { HookType } from '../common/promptSyntax/hookTypes.js';
1718
import { PromptsType } from '../common/promptSyntax/promptTypes.js';
18-
import { IHookDiscoveryInfo, type InstructionsCollectionEvent, IPromptDiscoveryInfo, IPromptsService } from '../common/promptSyntax/service/promptsService.js';
19+
import { IHookDiscoveryInfo, type InstructionsCollectionDebugInfo, IPromptDiscoveryInfo, IPromptsService } from '../common/promptSyntax/service/promptsService.js';
20+
import { lastInstructionsCollectionResult } from '../common/promptSyntax/computeAutomaticInstructions.js';
1921

2022
interface ICustomizationEventData {
21-
readonly collectionEvent: InstructionsCollectionEvent;
23+
readonly debugInfo: InstructionsCollectionDebugInfo;
2224
readonly hooks: ChatRequestHooks | undefined;
2325
}
2426

@@ -123,35 +125,36 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
123125
}
124126

125127
// Log resolved customizations from the last instructions collection.
126-
const collectionEvent = this.promptsService.lastInstructionsCollectionEvent;
127-
if (!isFirstInvocation && collectionEvent) {
128-
// Also fetch the resolved hooks so they appear in the customization summary.
128+
const lastResult = lastInstructionsCollectionResult;
129+
if (!isFirstInvocation && lastResult) {
130+
const { telemetryEvent: collectionEvent, debugInfo } = lastResult;
131+
// Fetch the cached hook discovery info.
129132
let resolvedHooks: ChatRequestHooks | undefined;
130133
try {
131-
const hooksInfo = await this.promptsService.getHooks(CancellationToken.None);
132-
resolvedHooks = hooksInfo?.hooks;
134+
const hookDiscoveryInfo = await this.promptsService.getDiscoveryInfo(PromptsType.hook, CancellationToken.None) as IHookDiscoveryInfo;
135+
resolvedHooks = hookDiscoveryInfo.hooksInfo?.hooks;
133136
} catch (error) {
134137
logService.warn('Error while fetching hooks for customization debug event', error);
135138
}
136139

137140
const parts: string[] = [];
138141
if (collectionEvent.applyingInstructionsCount > 0) {
139-
parts.push(`${collectionEvent.applyingInstructionsCount} applying`);
142+
parts.push(localize('customizations.applying', '{0} applying', collectionEvent.applyingInstructionsCount));
140143
}
141144
if (collectionEvent.referencedInstructionsCount > 0) {
142-
parts.push(`${collectionEvent.referencedInstructionsCount} referenced`);
145+
parts.push(localize('customizations.referenced', '{0} referenced', collectionEvent.referencedInstructionsCount));
143146
}
144147
if (collectionEvent.agentInstructionsCount > 0) {
145-
parts.push(`${collectionEvent.agentInstructionsCount} agent`);
148+
parts.push(localize('customizations.agent', '{0} agent', collectionEvent.agentInstructionsCount));
146149
}
147150
if (collectionEvent.listedInstructionsCount > 0) {
148-
parts.push(`${collectionEvent.listedInstructionsCount} listed`);
151+
parts.push(localize('customizations.listed', '{0} listed', collectionEvent.listedInstructionsCount));
149152
}
150-
const durationStr = collectionEvent.durationInMillis.toFixed(1);
153+
const durationStr = debugInfo.durationInMillis.toFixed(1);
151154
const summary = parts.length > 0
152155
? localize('customizationsResolved.details', 'Resolved {0} customizations ({1}) in {2}ms', collectionEvent.totalInstructionsCount, parts.join(', '), durationStr)
153156
: localize('customizationsResolved.none', 'No customizations resolved');
154-
const detailSummaries = collectionEvent.debugDetails.map(e => {
157+
const detailSummaries = debugInfo.debugDetails.map(e => {
155158
const detail = e.reason ? `${e.name}${e.reason}` : e.name;
156159
return `[${e.category}] ${detail}`;
157160
});
@@ -160,7 +163,7 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
160163
: summary;
161164

162165
const customizationEventId = generateUuid();
163-
this._customizationEventDetails.set(customizationEventId, { collectionEvent, hooks: resolvedHooks });
166+
this._customizationEventDetails.set(customizationEventId, { debugInfo, hooks: resolvedHooks });
164167

165168
// Evict oldest entries when the map exceeds the cap.
166169
if (this._customizationEventDetails.size > PromptsDebugContribution.MAX_DISCOVERY_DETAILS) {
@@ -198,28 +201,28 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
198201
switch (discoveryInfo.type) {
199202
case PromptsType.prompt:
200203
return {
201-
name: localize('promptsService.loadSlashCommands', 'Load Slash Commands'),
204+
name: localize('promptsService.loadSlashCommands', 'Slash Commands Discovery'),
202205
details: loadedCount === 1
203206
? localize('promptsDebugContribution.resolvedSlashCommand', 'Resolved {0} slash command in {1}ms', loadedCount, durationInMillis)
204207
: localize('promptsDebugContribution.resolvedSlashCommands', 'Resolved {0} slash commands in {1}ms', loadedCount, durationInMillis)
205208
};
206209
case PromptsType.agent:
207210
return {
208-
name: localize('promptsService.loadAgents', 'Load Agents'),
211+
name: localize('promptsService.loadAgents', 'Agent Discovery'),
209212
details: loadedCount === 1
210213
? localize('promptsDebugContribution.resolvedAgent', 'Resolved {0} agent in {1}ms', loadedCount, durationInMillis)
211214
: localize('promptsDebugContribution.resolvedAgents', 'Resolved {0} agents in {1}ms', loadedCount, durationInMillis)
212215
};
213216
case PromptsType.skill:
214217
return {
215-
name: localize('promptsService.loadSkills', 'Load Skills'),
218+
name: localize('promptsService.loadSkills', 'Skill Discovery'),
216219
details: loadedCount === 1
217220
? localize('promptsDebugContribution.resolvedSkill', 'Resolved {0} skill in {1}ms', loadedCount, durationInMillis)
218221
: localize('promptsDebugContribution.resolvedSkills', 'Resolved {0} skills in {1}ms', loadedCount, durationInMillis)
219222
};
220223
case PromptsType.instructions:
221224
return {
222-
name: localize('promptsService.loadInstructions', 'Load Instructions'),
225+
name: localize('promptsService.loadInstructions', 'Instructions Discovery'),
223226
details: loadedCount === 1
224227
? localize('promptsDebugContribution.resolvedInstruction', 'Resolved {0} instruction in {1}ms', loadedCount, durationInMillis)
225228
: localize('promptsDebugContribution.resolvedInstructions', 'Resolved {0} instructions in {1}ms', loadedCount, durationInMillis)
@@ -235,7 +238,7 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
235238
? localize('promptsDebugContribution.resolvedHook', 'Resolved {0} hook in {1}ms', hookCount, durationInMillis)
236239
: localize('promptsDebugContribution.resolvedHooks', 'Resolved {0} hooks in {1}ms', hookCount, durationInMillis);
237240
return {
238-
name: localize('promptsService.loadHooks', 'Load Hooks'),
241+
name: localize('promptsService.loadHooks', 'Hook Discovery'),
239242
details
240243
};
241244
}
@@ -257,20 +260,21 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
257260
return undefined;
258261
}
259262

260-
const { collectionEvent, hooks } = data;
261-
const logs: IChatDebugCustomizationLogEntry[] = [...collectionEvent.debugDetails];
263+
const { debugInfo, hooks } = data;
264+
const logs: IChatDebugCustomizationLogEntry[] = [...debugInfo.debugDetails];
262265

263-
// Add hook entries from the resolved hooks.
266+
// Add hook entries from the resolved hooks — each command carries its sourceUri.
264267
if (hooks) {
265268
for (const hookType of Object.values(HookType)) {
266269
const commands = hooks[hookType];
267270
if (commands && commands.length > 0) {
268271
for (const cmd of commands) {
269-
const commandStr = cmd.command ?? cmd.osx ?? cmd.linux ?? cmd.windows ?? 'unknown';
272+
const commandLabel = formatHookCommandLabel(cmd, OS) || localize('hook.unknownCommand', '(unknown command)');
270273
logs.push({
271274
category: 'hook',
272-
name: hookType,
273-
reason: commandStr,
275+
name: commandLabel,
276+
reason: hookType,
277+
uri: cmd.sourceUri,
274278
});
275279
}
276280
}
@@ -280,7 +284,7 @@ export class PromptsDebugContribution extends Disposable implements IWorkbenchCo
280284
return {
281285
kind: 'customizationSummary',
282286
resolutionLogs: logs,
283-
durationInMillis: collectionEvent.durationInMillis,
287+
durationInMillis: debugInfo.durationInMillis,
284288
counts: {
285289
instructions: logs.filter(e => e.category === 'applying' || e.category === 'referenced').length,
286290
skills: logs.filter(e => e.category === 'skill').length,

0 commit comments

Comments
 (0)