Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.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 * as DOM from '../../../../../base/browser/dom.js';
7
import { Button } from '../../../../../base/browser/ui/button/button.js';
8
import { Codicon } from '../../../../../base/common/codicons.js';
9
import { Emitter } from '../../../../../base/common/event.js';
10
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
11
import { ThemeIcon } from '../../../../../base/common/themables.js';
12
import { URI } from '../../../../../base/common/uri.js';
13
import { isUUID } from '../../../../../base/common/uuid.js';
14
import { localize } from '../../../../../nls.js';
15
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
16
import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
17
import { IChatDebugService } from '../../common/chatDebugService.js';
18
import { IChatService } from '../../common/chatService/chatService.js';
19
import { AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING } from '../../common/promptSyntax/promptTypes.js';
20
import { getChatSessionType, isUntitledChatSession, LocalChatSessionUri } from '../../common/model/chatUri.js';
21
import { IChatWidgetService } from '../chat.js';
22
import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js';
23
import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';
24
25
const $ = DOM.$;
26
27
const PAGE_SIZE = 5;
28
29
export class ChatDebugHomeView extends Disposable {
30
31
private readonly _onNavigateToSession = this._register(new Emitter<URI>());
32
readonly onNavigateToSession = this._onNavigateToSession.event;
33
34
readonly container: HTMLElement;
35
private readonly scrollContent: HTMLElement;
36
private readonly renderDisposables = this._register(new DisposableStore());
37
38
/** Number of sessions currently visible (grows on "Show More"). */
39
private _visibleCount = PAGE_SIZE;
40
41
/** Session resource that the user last navigated to from the home view. */
42
private _lastOpenedSessionResource: URI | undefined;
43
44
/** Tracks the number of known sessions so we can detect new ones. */
45
private _lastKnownSessionCount = 0;
46
47
constructor(
48
parent: HTMLElement,
49
@IChatService private readonly chatService: IChatService,
50
@IChatDebugService private readonly chatDebugService: IChatDebugService,
51
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
52
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
53
@IConfigurationService private readonly configurationService: IConfigurationService,
54
@IPreferencesService private readonly preferencesService: IPreferencesService,
55
) {
56
super();
57
this.container = DOM.append(parent, $('.chat-debug-home'));
58
this.scrollContent = DOM.append(this.container, $('div.chat-debug-home-content'));
59
60
this._register(this.configurationService.onDidChangeConfiguration(e => {
61
if (e.affectsConfiguration(AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING)) {
62
this.render();
63
}
64
}));
65
66
// Re-render when a new session appears so it surfaces at the top.
67
this._register(this.chatDebugService.onDidAddEvent(e => {
68
const currentCount = this.chatDebugService.getSessionResources().length;
69
if (currentCount !== this._lastKnownSessionCount) {
70
this._lastKnownSessionCount = currentCount;
71
if (this.container.style.display !== 'none') {
72
this.render();
73
}
74
}
75
}));
76
77
// Re-render when historical sessions are discovered from disk.
78
this._register(this.chatDebugService.onDidChangeAvailableSessionResources(() => {
79
if (this.container.style.display !== 'none') {
80
this.render();
81
}
82
}));
83
}
84
85
show(): void {
86
this.container.style.display = '';
87
this.render();
88
}
89
90
hide(): void {
91
this.container.style.display = 'none';
92
}
93
94
render(): void {
95
const isFileLoggingEnabled = this.configurationService.getValue<boolean>(AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING);
96
this._lastKnownSessionCount = this.chatDebugService.getSessionResources().length;
97
98
const sessionResources = isFileLoggingEnabled
99
? this._getFilteredSessionResources(this.chatDebugService.getAvailableSessionResources())
100
: [];
101
this._renderWithSessions(sessionResources);
102
}
103
104
private _getFilteredSessionResources(resources: readonly URI[]): URI[] {
105
const cliSessionTypes = new Set(['copilotcli', 'claude-code']);
106
return [...resources]
107
.filter(r => !cliSessionTypes.has(getChatSessionType(r)) || !isUntitledChatSession(r));
108
}
109
110
private _renderWithSessions(sessionResources: URI[]): void {
111
DOM.clearNode(this.scrollContent);
112
this.renderDisposables.clear();
113
114
DOM.append(this.scrollContent, $('h2.chat-debug-home-title', undefined, localize('chatDebug.title', "Agent Debug Logs")));
115
116
const isEnabled = this.configurationService.getValue<boolean>(AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING);
117
if (!isEnabled) {
118
DOM.append(this.scrollContent, $('p.chat-debug-home-subtitle', undefined,
119
localize('chatDebug.disabled', "Enable to view debug logs and investigate chat issues with /troubleshoot.")
120
));
121
122
const enableButton = this.renderDisposables.add(new Button(this.scrollContent, { ...defaultButtonStyles, secondary: true }));
123
enableButton.element.style.width = 'auto';
124
enableButton.label = localize('chatDebug.openSetting', "Enable in Settings");
125
this.renderDisposables.add(enableButton.onDidClick(() => {
126
this.preferencesService.openSettings({ jsonEditor: false, query: `@id:${AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING}` });
127
}));
128
return;
129
}
130
131
// Determine the active session resource
132
const activeWidget = this.chatWidgetService.lastFocusedWidget;
133
const activeSessionResource = activeWidget?.viewModel?.sessionResource;
134
135
// Bubble active sessions to top
136
const bubbleToTop = (resource: URI | undefined) => {
137
if (!resource) {
138
return;
139
}
140
const idx = sessionResources.findIndex(r => r.toString() === resource.toString());
141
if (idx > 0) {
142
sessionResources.splice(idx, 1);
143
sessionResources.unshift(resource);
144
}
145
};
146
bubbleToTop(this._lastOpenedSessionResource);
147
bubbleToTop(activeSessionResource);
148
149
DOM.append(this.scrollContent, $('p.chat-debug-home-subtitle', undefined,
150
sessionResources.length > 0
151
? localize('chatDebug.homeSubtitle', "Select a chat session to debug")
152
: localize('chatDebug.noSessions', "Send a chat message to get started")
153
));
154
155
if (sessionResources.length > 0) {
156
const visibleSessions = sessionResources.slice(0, this._visibleCount);
157
158
const sessionList = DOM.append(this.scrollContent, $('.chat-debug-home-session-list'));
159
sessionList.setAttribute('role', 'list');
160
sessionList.setAttribute('aria-label', localize('chatDebug.sessionList', "Chat sessions"));
161
162
const items: HTMLButtonElement[] = [];
163
164
for (const sessionResource of visibleSessions) {
165
// Resolve title: agent sessions model (same as sidebar) → chat service → historical from JSONL → fallback
166
const agentSession = this.agentSessionsService.model.getSession(sessionResource);
167
const rawTitle = agentSession?.label ?? this.chatService.getSessionTitle(sessionResource);
168
const importedTitle = this.chatDebugService.getImportedSessionTitle(sessionResource);
169
const historicalTitle = this.chatDebugService.getHistoricalSessionTitle(sessionResource);
170
let sessionTitle: string;
171
if (rawTitle && !isUUID(rawTitle)) {
172
sessionTitle = rawTitle;
173
} else if (historicalTitle) {
174
sessionTitle = historicalTitle;
175
} else if (importedTitle) {
176
sessionTitle = localize('chatDebug.importedSession', "Imported: {0}", importedTitle);
177
} else if (LocalChatSessionUri.isLocalSession(sessionResource)) {
178
sessionTitle = localize('chatDebug.newSession', "New Chat");
179
} else if (getChatSessionType(sessionResource) === 'copilotcli') {
180
const pathId = sessionResource.path.replace(/^\//, '').split('-')[0];
181
const shortId = pathId || sessionResource.authority || sessionResource.toString();
182
sessionTitle = localize('chatDebug.copilotCliSessionWithId', "Copilot CLI: {0}", shortId);
183
} else if (getChatSessionType(sessionResource) === 'claude-code') {
184
const pathId = sessionResource.path.replace(/^\//, '').split('-')[0];
185
const shortId = pathId || sessionResource.authority || sessionResource.toString();
186
sessionTitle = localize('chatDebug.claudeCodeSessionWithId', "Claude Code: {0}", shortId);
187
} else {
188
sessionTitle = localize('chatDebug.newSession', "New Chat");
189
}
190
const isActive = activeSessionResource !== undefined && sessionResource.toString() === activeSessionResource.toString();
191
192
const item = DOM.append(sessionList, $<HTMLButtonElement>('button.chat-debug-home-session-item'));
193
item.setAttribute('role', 'listitem');
194
if (isActive) {
195
item.classList.add('chat-debug-home-session-item-active');
196
item.setAttribute('aria-current', 'true');
197
}
198
199
DOM.append(item, $(`span${ThemeIcon.asCSSSelector(Codicon.comment)}`));
200
201
const titleSpan = DOM.append(item, $('span.chat-debug-home-session-item-title'));
202
titleSpan.textContent = sessionTitle;
203
const ariaLabel = isActive
204
? localize('chatDebug.sessionItemActive', "{0} (active)", sessionTitle)
205
: sessionTitle;
206
item.setAttribute('aria-label', ariaLabel);
207
208
if (isActive) {
209
DOM.append(item, $('span.chat-debug-home-session-badge', undefined, localize('chatDebug.active', "Active")));
210
}
211
212
this.renderDisposables.add(DOM.addDisposableListener(item, DOM.EventType.CLICK, () => {
213
this._lastOpenedSessionResource = sessionResource;
214
this._onNavigateToSession.fire(sessionResource);
215
}));
216
items.push(item);
217
}
218
219
// "Show More" button when there are more sessions to display
220
if (sessionResources.length > this._visibleCount) {
221
const remaining = sessionResources.length - this._visibleCount;
222
const showMoreButton = this.renderDisposables.add(new Button(this.scrollContent, { ...defaultButtonStyles, secondary: true }));
223
showMoreButton.element.classList.add('chat-debug-home-show-more');
224
showMoreButton.label = localize('chatDebug.showMore', "Show More ({0})", remaining);
225
this.renderDisposables.add(showMoreButton.onDidClick(() => {
226
this._visibleCount += PAGE_SIZE;
227
this.render();
228
}));
229
}
230
231
// Arrow key navigation between session items
232
this.renderDisposables.add(DOM.addDisposableListener(sessionList, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
233
if (items.length === 0) {
234
return;
235
}
236
const focused = DOM.getActiveElement() as HTMLElement;
237
const idx = items.indexOf(focused as HTMLButtonElement);
238
if (idx === -1) {
239
return;
240
}
241
let nextIdx: number | undefined;
242
switch (e.key) {
243
case 'ArrowDown':
244
nextIdx = idx + 1 < items.length ? idx + 1 : idx;
245
break;
246
case 'ArrowUp':
247
nextIdx = idx - 1 >= 0 ? idx - 1 : idx;
248
break;
249
case 'Home':
250
nextIdx = 0;
251
break;
252
case 'End':
253
nextIdx = items.length - 1;
254
break;
255
}
256
if (nextIdx !== undefined) {
257
e.preventDefault();
258
items[nextIdx].focus();
259
}
260
}));
261
}
262
}
263
}
264
265