Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatDebug/chatCustomizationDiscoveryRenderer.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import '../widget/chatContentParts/media/chatInlineAnchorWidget.css';
7
import * as DOM from '../../../../../base/browser/dom.js';
8
import { Button } from '../../../../../base/browser/ui/button/button.js';
9
import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
10
import { Codicon } from '../../../../../base/common/codicons.js';
11
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
12
import { ThemeIcon } from '../../../../../base/common/themables.js';
13
import { URI } from '../../../../../base/common/uri.js';
14
import { dirname } from '../../../../../base/common/resources.js';
15
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
16
import { getIconClasses } from '../../../../../editor/common/services/getIconClasses.js';
17
import { IModelService } from '../../../../../editor/common/services/model.js';
18
import { localize } from '../../../../../nls.js';
19
import { FileKind } from '../../../../../platform/files/common/files.js';
20
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
21
import { ILabelService } from '../../../../../platform/label/common/label.js';
22
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
23
import { IChatDebugCustomizationLogEntry, IChatDebugEventCustomizationSummaryContent, IChatDebugEventFileListContent } from '../../common/chatDebugService.js';
24
import { InlineAnchorWidget } from '../widget/chatContentParts/chatInlineAnchorWidget.js';
25
import { setupCollapsibleToggle } from './chatDebugCollapsible.js';
26
27
const $ = DOM.$;
28
29
/**
30
* Map a discovery type string to its corresponding settings key.
31
*/
32
function getSettingsKeyForDiscoveryType(discoveryType: string): string | undefined {
33
switch (discoveryType) {
34
case 'prompt': return 'chat.promptFilesLocations';
35
case 'instructions': return 'chat.instructionsFilesLocations';
36
case 'agent': return 'chat.agentFilesLocations';
37
case 'skill': return 'chat.agentSkillsLocations';
38
case 'hook': return 'chat.hookFilesLocations';
39
default: return undefined;
40
}
41
}
42
43
/**
44
* Get a display label for a file's location.
45
* Extension files show the extension ID,
46
* all other files show the relative (or tildified) parent folder path.
47
*/
48
function getFileLocationLabel(file: { uri: URI; storage?: string; extensionId?: string }, labelService: ILabelService, discoveryType?: string): string {
49
if (file.extensionId) {
50
return file.extensionId;
51
}
52
// Skills live inside individual skill folders (e.g. .github/skills/foo/SKILL.md),
53
// so group by the parent of the skill folder for a more useful label.
54
const parentDir = discoveryType === 'skill' ? dirname(dirname(file.uri)) : dirname(file.uri);
55
return labelService.getUriLabel(parentDir, { relative: true });
56
}
57
58
/**
59
* Create a file link element styled like the chat panel's InlineAnchorWidget.
60
*/
61
function createInlineFileLink(uri: URI, displayText: string, fileKind: FileKind, openerService: IOpenerService, modelService: IModelService, languageService: ILanguageService, hoverService: IHoverService, labelService: ILabelService, disposables: DisposableStore, hoverSuffix?: string): HTMLElement {
62
const link = $(`a.${InlineAnchorWidget.className}.show-file-icons`);
63
link.tabIndex = -1;
64
65
const iconEl = DOM.append(link, $('span.icon'));
66
const iconClasses = getIconClasses(modelService, languageService, uri, fileKind);
67
iconEl.classList.add(...iconClasses);
68
69
DOM.append(link, $('span.icon-label', undefined, displayText));
70
71
const relativeLabel = labelService.getUriLabel(uri, { relative: true });
72
const hoverText = hoverSuffix ? `${relativeLabel} ${hoverSuffix}` : relativeLabel;
73
disposables.add(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), link, hoverText));
74
disposables.add(DOM.addDisposableListener(link, DOM.EventType.CLICK, (e) => {
75
e.preventDefault();
76
e.stopPropagation();
77
openerService.open(uri, { editorOptions: { preserveFocus: true } });
78
}));
79
80
return link;
81
}
82
83
/**
84
* Set up roving tabindex with arrow-key navigation on a list of rows.
85
* The first row starts with tabIndex 0; the rest get -1.
86
* Up/Down arrow keys move focus, Home/End jump to first/last.
87
* Enter on a focused row activates the associated action.
88
*/
89
function setupFileListNavigation(listEl: HTMLElement, rows: { element: HTMLElement; activate: () => void }[], disposables: DisposableStore): void {
90
if (rows.length === 0) {
91
return;
92
}
93
94
for (let i = 0; i < rows.length; i++) {
95
rows[i].element.tabIndex = i === 0 ? 0 : -1;
96
rows[i].element.setAttribute('role', 'listitem');
97
}
98
99
disposables.add(DOM.addDisposableListener(listEl, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
100
const target = e.target as HTMLElement;
101
const index = rows.findIndex(r => r.element === target);
102
if (index === -1) {
103
return;
104
}
105
106
let nextIndex: number | undefined;
107
switch (e.key) {
108
case 'ArrowDown':
109
nextIndex = Math.min(index + 1, rows.length - 1);
110
break;
111
case 'ArrowUp':
112
nextIndex = Math.max(index - 1, 0);
113
break;
114
case 'Home':
115
nextIndex = 0;
116
break;
117
case 'End':
118
nextIndex = rows.length - 1;
119
break;
120
case 'Enter': {
121
rows[index].activate();
122
e.preventDefault();
123
return;
124
}
125
}
126
127
if (nextIndex !== undefined && nextIndex !== index) {
128
e.preventDefault();
129
rows[index].element.tabIndex = -1;
130
rows[nextIndex].element.tabIndex = 0;
131
rows[nextIndex].element.focus();
132
}
133
}));
134
}
135
136
/**
137
* Render a file list resolved content as a rich HTML element.
138
*/
139
export function renderCustomizationDiscoveryContent(content: IChatDebugEventFileListContent, openerService: IOpenerService, modelService: IModelService, languageService: ILanguageService, hoverService: IHoverService, labelService: ILabelService, scrollable?: { scanDomNode(): void }): { element: HTMLElement; disposables: DisposableStore } {
140
const disposables = new DisposableStore();
141
const container = $('div.chat-debug-file-list');
142
container.tabIndex = 0;
143
144
const capitalizedType = content.discoveryType.charAt(0).toUpperCase() + content.discoveryType.slice(1);
145
DOM.append(container, $('div.chat-debug-file-list-title', undefined, localize('chatDebug.discoveryResults', "{0} Discovery Results", capitalizedType)));
146
DOM.append(container, $('div.chat-debug-file-list-summary', undefined, localize('chatDebug.totalFiles', "Total files: {0}", content.files.length)));
147
148
// Loaded files - grouped by source location
149
const loaded = content.files.filter(f => f.status === 'loaded');
150
if (loaded.length > 0) {
151
const section = DOM.append(container, $('div.chat-debug-file-list-section'));
152
DOM.append(section, $('div.chat-debug-file-list-section-title', undefined,
153
localize('chatDebug.loadedFiles', "Loaded ({0})", loaded.length)));
154
155
// Group files by location label (extension ID or folder path)
156
const groups = new Map<string, typeof loaded>();
157
for (const file of loaded) {
158
const key = getFileLocationLabel(file, labelService, content.discoveryType);
159
let group = groups.get(key);
160
if (!group) {
161
group = [];
162
groups.set(key, group);
163
}
164
group.push(file);
165
}
166
167
const listEl = DOM.append(section, $('div.chat-debug-file-list-rows'));
168
listEl.setAttribute('role', 'list');
169
listEl.setAttribute('aria-label', localize('chatDebug.loadedFilesList', "Loaded files"));
170
171
const rows: { element: HTMLElement; activate: () => void }[] = [];
172
for (const [locationLabel, files] of groups) {
173
// Group header - show the source location
174
const groupHeader = DOM.append(listEl, $('div.chat-debug-file-list-group-header'));
175
const firstFile = files[0];
176
if (firstFile.extensionId) {
177
const link = DOM.append(groupHeader, $('a.chat-debug-file-list-group-label.chat-debug-file-list-badge-link'));
178
link.textContent = locationLabel;
179
link.tabIndex = -1;
180
disposables.add(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), link, localize('chatDebug.openExtension', "Open {0} in Extensions", firstFile.extensionId)));
181
disposables.add(DOM.addDisposableListener(link, DOM.EventType.CLICK, (e) => {
182
e.preventDefault();
183
e.stopPropagation();
184
openerService.open(URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([firstFile.extensionId]))}`), { allowCommands: true });
185
}));
186
} else {
187
DOM.append(groupHeader, $('span.chat-debug-file-list-group-label', undefined, locationLabel));
188
}
189
190
for (const file of files) {
191
const row = DOM.append(listEl, $('div.chat-debug-file-list-row'));
192
DOM.append(row, $(`span.chat-debug-file-list-icon${ThemeIcon.asCSSSelector(Codicon.check)}`));
193
row.appendChild(createInlineFileLink(file.uri, file.name ?? file.uri.path, FileKind.FILE, openerService, modelService, languageService, hoverService, labelService, disposables));
194
const relativeLabel = labelService.getUriLabel(file.uri, { relative: true });
195
row.setAttribute('aria-label', relativeLabel);
196
const uri = file.uri;
197
rows.push({ element: row, activate: () => openerService.open(uri, { editorOptions: { preserveFocus: true } }) });
198
}
199
}
200
setupFileListNavigation(listEl, rows, disposables);
201
}
202
203
// Skipped files - grouped by skip reason
204
const skipped = content.files.filter(f => f.status === 'skipped');
205
if (skipped.length > 0) {
206
const section = DOM.append(container, $('div.chat-debug-file-list-section'));
207
DOM.append(section, $('div.chat-debug-file-list-section-title', undefined,
208
localize('chatDebug.skippedFiles', "Skipped ({0})", skipped.length)));
209
210
// Group files by skip reason
211
const groups = new Map<string, typeof skipped>();
212
for (const file of skipped) {
213
const key = file.skipReason ?? localize('chatDebug.unknown', "unknown");
214
let group = groups.get(key);
215
if (!group) {
216
group = [];
217
groups.set(key, group);
218
}
219
group.push(file);
220
}
221
222
const listEl = DOM.append(section, $('div.chat-debug-file-list-rows'));
223
listEl.setAttribute('role', 'list');
224
listEl.setAttribute('aria-label', localize('chatDebug.skippedFilesList', "Skipped files"));
225
226
const rows: { element: HTMLElement; activate: () => void }[] = [];
227
for (const [reasonLabel, files] of groups) {
228
// Group header - show the skip reason
229
const groupHeader = DOM.append(listEl, $('div.chat-debug-file-list-group-header'));
230
DOM.append(groupHeader, $('span.chat-debug-file-list-group-label', undefined, reasonLabel));
231
232
for (const file of files) {
233
const row = DOM.append(listEl, $('div.chat-debug-file-list-row'));
234
DOM.append(row, $(`span.chat-debug-file-list-icon${ThemeIcon.asCSSSelector(Codicon.close)}`));
235
236
// Build per-file detail (error message / duplicate info)
237
let detail = '';
238
if (file.errorMessage) {
239
detail += file.errorMessage;
240
}
241
if (file.duplicateOf) {
242
if (detail) {
243
detail += ', ';
244
}
245
detail += localize('chatDebug.duplicateOf', "duplicate of {0}", file.duplicateOf.path);
246
}
247
248
row.appendChild(createInlineFileLink(file.uri, file.name ?? file.uri.path, FileKind.FILE, openerService, modelService, languageService, hoverService, labelService, disposables));
249
if (detail) {
250
DOM.append(row, $('span.chat-debug-file-list-detail', undefined, ` (${detail})`));
251
}
252
const relativeLabel = labelService.getUriLabel(file.uri, { relative: true });
253
row.setAttribute('aria-label', relativeLabel);
254
const uri = file.uri;
255
rows.push({ element: row, activate: () => openerService.open(uri, { editorOptions: { preserveFocus: true } }) });
256
}
257
}
258
setupFileListNavigation(listEl, rows, disposables);
259
}
260
261
// Source folders (paths attempted) - collapsible, initially collapsed
262
if (content.sourceFolders && content.sourceFolders.length > 0) {
263
const sectionEl = DOM.append(container, $('div.chat-debug-message-section'));
264
265
const header = DOM.append(sectionEl, $('div.chat-debug-message-section-header'));
266
267
const chevron = DOM.append(header, $('span.chat-debug-message-section-chevron'));
268
DOM.append(header, $('span.chat-debug-message-section-title', undefined,
269
localize('chatDebug.sourceFolders', "Sources ({0})", content.sourceFolders.length)));
270
271
// Settings gear button on the right side of the header
272
const settingsKey = getSettingsKeyForDiscoveryType(content.discoveryType);
273
if (settingsKey) {
274
const gearBtn = disposables.add(new Button(header, {
275
title: localize('chatDebug.openSettingsTooltip', "Configure locations"),
276
ariaLabel: localize('chatDebug.configureLocations', "Configure locations"),
277
hoverDelegate: getDefaultHoverDelegate('mouse'),
278
}));
279
gearBtn.icon = Codicon.settingsGear;
280
gearBtn.element.classList.add('chat-debug-settings-gear');
281
disposables.add(DOM.addDisposableListener(gearBtn.element, DOM.EventType.MOUSE_ENTER, () => {
282
header.classList.add('chat-debug-settings-gear-header-passthrough');
283
}));
284
disposables.add(DOM.addDisposableListener(gearBtn.element, DOM.EventType.MOUSE_LEAVE, () => {
285
header.classList.remove('chat-debug-settings-gear-header-passthrough');
286
}));
287
disposables.add(gearBtn.onDidClick((e) => {
288
if (e) {
289
DOM.EventHelper.stop(e, true);
290
}
291
openerService.open(URI.parse(`command:workbench.action.openSettings?${encodeURIComponent(JSON.stringify([`@id:${settingsKey}`]))}`), { allowCommands: true });
292
}));
293
}
294
295
const contentEl = DOM.append(sectionEl, $('div.chat-debug-source-folder-content'));
296
contentEl.tabIndex = 0;
297
contentEl.setAttribute('role', 'region');
298
contentEl.setAttribute('aria-label', localize('chatDebug.sourceFoldersContent', "Source folders"));
299
300
const capitalizedType = content.discoveryType.charAt(0).toUpperCase() + content.discoveryType.slice(1);
301
const sourcesCaption = capitalizedType.endsWith('s') ? capitalizedType : capitalizedType + 's';
302
DOM.append(contentEl, $('div.chat-debug-source-folder-note', undefined,
303
localize('chatDebug.sourcesNote', "{0} were discovered by checking the following sources in order:", sourcesCaption)));
304
for (let i = 0; i < content.sourceFolders.length; i++) {
305
const folder = content.sourceFolders[i];
306
const row = DOM.append(contentEl, $('div.chat-debug-source-folder-row'));
307
DOM.append(row, $('span.chat-debug-source-folder-index', undefined, `${i + 1}.`));
308
DOM.append(row, $('span.chat-debug-source-folder-label', undefined, folder.uri.path));
309
}
310
311
setupCollapsibleToggle(chevron, header, contentEl, disposables, /* initiallyCollapsed */ true, scrollable);
312
}
313
314
return { element: container, disposables };
315
}
316
317
/**
318
* Convert a file list content to plain text for clipboard / editor output.
319
*/
320
export function fileListToPlainText(content: IChatDebugEventFileListContent): string {
321
const lines: string[] = [];
322
const capitalizedType = content.discoveryType.charAt(0).toUpperCase() + content.discoveryType.slice(1);
323
lines.push(localize('chatDebug.plainText.discoveryResults', "{0} Discovery Results", capitalizedType));
324
lines.push(localize('chatDebug.plainText.totalFiles', "Total files: {0}", content.files.length));
325
lines.push('');
326
327
const loaded = content.files.filter(f => f.status === 'loaded');
328
const skipped = content.files.filter(f => f.status === 'skipped');
329
330
if (loaded.length > 0) {
331
lines.push(localize('chatDebug.plainText.loaded', "Loaded ({0})", loaded.length));
332
// Group by location
333
const groups = new Map<string, typeof loaded>();
334
for (const f of loaded) {
335
const parentDir = content.discoveryType === 'skill' ? dirname(dirname(f.uri)) : dirname(f.uri);
336
const key = f.extensionId ?? parentDir.path;
337
let group = groups.get(key);
338
if (!group) {
339
group = [];
340
groups.set(key, group);
341
}
342
group.push(f);
343
}
344
for (const [locationLabel, files] of groups) {
345
lines.push(` ${locationLabel}`);
346
for (const f of files) {
347
const label = f.name ?? f.uri.path;
348
lines.push(` \u2713 ${label}`);
349
}
350
}
351
lines.push('');
352
}
353
354
if (skipped.length > 0) {
355
lines.push(localize('chatDebug.plainText.skipped', "Skipped ({0})", skipped.length));
356
// Group by skip reason
357
const skippedGroups = new Map<string, typeof skipped>();
358
for (const f of skipped) {
359
const key = f.skipReason ?? localize('chatDebug.plainText.unknown', "unknown");
360
let group = skippedGroups.get(key);
361
if (!group) {
362
group = [];
363
skippedGroups.set(key, group);
364
}
365
group.push(f);
366
}
367
for (const [reasonLabel, files] of skippedGroups) {
368
lines.push(` ${reasonLabel}`);
369
for (const f of files) {
370
const label = f.name ?? f.uri.path;
371
let detail = ` \u2717 ${label}`;
372
if (f.errorMessage || f.duplicateOf) {
373
const parts: string[] = [];
374
if (f.errorMessage) {
375
parts.push(f.errorMessage);
376
}
377
if (f.duplicateOf) {
378
parts.push(localize('chatDebug.plainText.duplicateOf', "duplicate of {0}", f.duplicateOf.path));
379
}
380
detail += ` (${parts.join(', ')})`;
381
}
382
lines.push(detail);
383
}
384
}
385
}
386
387
if (content.sourceFolders && content.sourceFolders.length > 0) {
388
lines.push('');
389
lines.push(localize('chatDebug.plainText.sourceFolders', "Sources ({0})", content.sourceFolders.length));
390
for (const folder of content.sourceFolders) {
391
lines.push(` ${folder.uri.path}`);
392
}
393
}
394
395
return lines.join('\n');
396
}
397
398
/**
399
* Get a human-readable section title for a resolution log category.
400
*/
401
function getCategorySectionTitle(category: IChatDebugCustomizationLogEntry['category'], count: number): string {
402
switch (category) {
403
case 'applying': return localize('chatDebug.customization.instructions', "Instructions ({0})", count);
404
case 'referenced': return localize('chatDebug.customization.referenced', "Referenced ({0})", count);
405
case 'skill': return localize('chatDebug.customization.skill', "Skills ({0})", count);
406
case 'custom-agent': return localize('chatDebug.customization.customAgent', "Agents ({0})", count);
407
case 'hook': return localize('chatDebug.customization.hook', "Hooks ({0})", count);
408
case 'skipped': return localize('chatDebug.customization.skipped', "Skipped ({0})", count);
409
}
410
}
411
412
/**
413
* Render a customization summary showing per-file resolution logs
414
* from the instructions context computer.
415
*/
416
export function renderCustomizationSummaryContent(content: IChatDebugEventCustomizationSummaryContent, openerService: IOpenerService, modelService: IModelService, languageService: ILanguageService, hoverService: IHoverService, labelService: ILabelService, scrollable?: { scanDomNode(): void }): { element: HTMLElement; disposables: DisposableStore } {
417
const disposables = new DisposableStore();
418
const container = $('div.chat-debug-customization-summary');
419
container.tabIndex = 0;
420
421
// Title with counts and duration
422
const mainSection = DOM.append(container, $('div.chat-debug-file-list'));
423
DOM.append(mainSection, $('div.chat-debug-file-list-title', undefined,
424
localize('chatDebug.customizationTitle', "Customization Resolution Results")));
425
DOM.append(mainSection, $('div.chat-debug-file-list-summary', undefined,
426
localize('chatDebug.customizationSummary', "{0} instructions, {1} skills, {2} agents, {3} hooks, {4} skipped in {5}ms",
427
content.counts.instructions, content.counts.skills, content.counts.agents, content.counts.hooks, content.counts.skipped, content.durationInMillis.toFixed(1))));
428
429
// Group entries by display section: instructions (applying+referenced), skills, agents, skipped
430
// Instructions section merges applying + referenced
431
const instructionEntries = content.resolutionLogs.filter(e => e.category === 'applying' || e.category === 'referenced');
432
const skillEntries = content.resolutionLogs.filter(e => e.category === 'skill');
433
const agentEntries = content.resolutionLogs.filter(e => e.category === 'custom-agent');
434
const hookEntries = content.resolutionLogs.filter(e => e.category === 'hook');
435
const skippedEntries = content.resolutionLogs.filter(e => e.category === 'skipped');
436
437
const sections: { title: string; icon: ThemeIcon; entries: readonly IChatDebugCustomizationLogEntry[] }[] = [
438
{ title: getCategorySectionTitle('applying', instructionEntries.length), icon: Codicon.book, entries: instructionEntries },
439
{ title: getCategorySectionTitle('skill', skillEntries.length), icon: Codicon.lightbulb, entries: skillEntries },
440
{ title: getCategorySectionTitle('custom-agent', agentEntries.length), icon: Codicon.agent, entries: agentEntries },
441
{ title: getCategorySectionTitle('hook', hookEntries.length), icon: Codicon.zap, entries: hookEntries },
442
{ title: getCategorySectionTitle('skipped', skippedEntries.length), icon: Codicon.close, entries: skippedEntries },
443
];
444
445
for (const { title, icon, entries } of sections) {
446
if (entries.length === 0) {
447
continue;
448
}
449
450
const section = DOM.append(mainSection, $('div.chat-debug-file-list-section'));
451
DOM.append(section, $('div.chat-debug-file-list-section-title', undefined, title));
452
453
const listEl = DOM.append(section, $('div.chat-debug-file-list-rows'));
454
listEl.setAttribute('role', 'list');
455
listEl.setAttribute('aria-label', title);
456
457
const rows: { element: HTMLElement; activate: () => void }[] = [];
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);
472
}
473
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);
519
}
520
}
521
setupFileListNavigation(listEl, rows, disposables);
522
}
523
524
if (content.resolutionLogs.length === 0) {
525
DOM.append(mainSection, $('div.chat-debug-file-list-summary', undefined,
526
localize('chatDebug.noResolutionLogs', "No resolution logs")));
527
}
528
529
return { element: container, disposables };
530
}
531
532
/**
533
* Serialize a customization summary to plain text for clipboard / full-screen.
534
*/
535
export function customizationSummaryToPlainText(content: IChatDebugEventCustomizationSummaryContent): string {
536
const lines: string[] = [];
537
538
lines.push(localize('chatDebug.plainText.customizationTitle', "Customization Resolution Results"));
539
lines.push(localize('chatDebug.plainText.customizationSummary', "{0} instructions, {1} skills, {2} agents, {3} hooks, {4} skipped in {5}ms",
540
content.counts.instructions, content.counts.skills, content.counts.agents, content.counts.hooks, content.counts.skipped, content.durationInMillis.toFixed(1)));
541
lines.push('');
542
for (const entry of content.resolutionLogs) {
543
const detail = entry.reason ? `${entry.name} — ${entry.reason}` : entry.name;
544
lines.push(` [${entry.category}] ${detail}`);
545
}
546
547
return lines.join('\n');
548
}
549
550