src/vs/workbench/contrib/chat/common/chatDebugServiceImpl.ts

629 LOC · 490 covered · 139 uncovered · 127 ranges · 264 concepts · 45 introducers · 117 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- chatDebugServiceImpl.ts ×16
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 { timeout } from '../../../../base/common/async.js';
7 > import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
8 > import { Emitter, Event } from '../../../../base/common/event.js';
9 > import { onUnexpectedError } from '../../../../base/common/errors.js';
10 > import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
11 > import { ResourceMap } from '../../../../base/common/map.js';
12 > import { extUri } from '../../../../base/common/resources.js';
13 > import { URI } from '../../../../base/common/uri.js';
14 > import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugLogProvider, IChatDebugResolvedEventContent, IChatDebugService } from './chatDebugService.js';
15 > import { isAgentHostTarget, localChatSessionType } from './chatSessionsService.js';
16 > import { getChatSessionType } from './model/chatUri.js';
17 > import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
18 > import { AgentHostAgentDebugLogMaxEventsSettingId } from './promptSyntax/promptTypes.js';
19 >
20 > /**
21 > * Per-session circular buffer for debug events.
22 > * Stores up to `capacity` events using a ring buffer.
23 > */
24 > class SessionEventBuffer {
25 > private readonly _buffer: (IChatDebugEvent | undefined)[];
26 > private _head = 0;
27 > private _size = 0;
28 >
29 > constructor(readonly capacity: number) {
30 > this._buffer = new Array(capacity); chatDebugServiceImpl.ts ×10
31 > }
33 > get size(): number {
34 return this._size;
35 }
37 > push(event: IChatDebugEvent): void {
38 > const idx = (this._head + this._size) % this.capacity; chatDebugServiceImpl.ts ×10
39 > this._buffer[idx] = event;
40 > if (this._size < this.capacity) {
41 > this._size++;
42 > } else {
43 > this._head = (this._head + 1) % this.capacity; chatDebugServiceImpl.ts ×1
44 > }
47 > /** Return events in insertion order. */
48 > toArray(): IChatDebugEvent[] {
49 > const result: IChatDebugEvent[] = []; chatDebugServiceImpl.ts ×1
50 > for (let i = 0; i < this._size; i++) {
51 > const event = this._buffer[(this._head + i) % this.capacity];
52 > if (event) {
53 > result.push(event);
54 > }
55 > }
56 > return result;
57 > }
59 > /** Remove events matching the predicate and compact in-place. */
60 > removeWhere(predicate: (event: IChatDebugEvent) => boolean): void {
61 let write = 0;
62 for (let i = 0; i < this._size; i++) {
63 const idx = (this._head + i) % this.capacity;
64 const event = this._buffer[idx];
65 if (event && predicate(event)) {
66 continue;
67 }
68 if (write !== i) {
69 const writeIdx = (this._head + write) % this.capacity;
70 this._buffer[writeIdx] = event;
71 }
72 write++;
73 }
74 for (let i = write; i < this._size; i++) {
75 this._buffer[(this._head + i) % this.capacity] = undefined;
76 }
77 this._size = write;
78 }
80 > clear(): void {
81 > this._buffer.fill(undefined); chatDebugServiceImpl.ts ×3
82 > this._head = 0;
83 > this._size = 0;
84 > }
86 >
87 > export class ChatDebugServiceImpl extends Disposable implements IChatDebugService {
88 > declare readonly _serviceBrand: undefined;
89 >
90 > static readonly MAX_EVENTS_PER_SESSION = 10_000;
91 > static readonly MAX_SESSIONS = 5;
92 >
93 > /** Per-session event buffers. Ordered from oldest to newest session (LRU). */
94 > private readonly _sessionBuffers = new ResourceMap<SessionEventBuffer>();
95 > /** Ordered list of session URIs for LRU eviction. */
96 > private readonly _sessionOrder: URI[] = [];
97 > /** Per-session tracking of seen event IDs to deduplicate events
98 > * that share the same ID (e.g. subagentInvocation + userMessage
99 > * emitted from the same span). Stores id → event kind so we can
100 > * keep the richer event kind on collision. */
101 > private readonly _seenEventIds = new ResourceMap<Map<string, IChatDebugEvent['kind']>>();
102 >
103 > private readonly _onDidAddEvent = this._register(new Emitter<IChatDebugEvent>());
104 > readonly onDidAddEvent: Event<IChatDebugEvent> = this._onDidAddEvent.event;
105 >
106 > private readonly _onDidClearProviderEvents = this._register(new Emitter<URI>());
107 > readonly onDidClearProviderEvents: Event<URI> = this._onDidClearProviderEvents.event;
108 >
109 > private readonly _onDidEndSession = this._register(new Emitter<URI>());
110 > readonly onDidEndSession: Event<URI> = this._onDidEndSession.event;
111 >
112 > private readonly _onDidChangeAvailableSessionResources = this._register(new Emitter<void>());
113 > readonly onDidChangeAvailableSessionResources: Event<void> = this._onDidChangeAvailableSessionResources.event;
114 >
115 > private readonly _providers = new Set<IChatDebugLogProvider>();
116 > private readonly _invocationCts = new ResourceMap<CancellationTokenSource>();
117 >
118 > /**
119 > * Sessions whose provider events should be cleared before the next batch of
120 > * provider events is applied. The clear is deferred until the first new
121 > * provider event actually arrives so that a provider which transiently
122 > * returns nothing (e.g. an Agent Host `events.jsonl` mid-rewrite) does not
123 > * wipe the events currently shown.
124 > */
125 > private readonly _pendingProviderClear = new ResourceMap<boolean>();
126 >
127 > /** Events that were returned by providers (not internally logged). */
128 > private readonly _providerEvents = new WeakSet<IChatDebugEvent>();
129 >
130 > /** Session URIs created via import. */
131 > private readonly _importedSessions = new ResourceMap<boolean>();
132 >
133 > /** Session URIs reported by providers as available on disk (historical sessions). */
134 > private readonly _availableSessionResources: URI[] = [];
135 > private readonly _availableSessionResourceSet = new Set<string>();
136 >
137 > /** Titles for historical sessions discovered from disk. */
138 > private readonly _historicalSessionTitles = new ResourceMap<string>();
139 >
140 > /** Human-readable titles for imported sessions. */
141 > private readonly _importedSessionTitles = new ResourceMap<string>();
142 >
143 > activeSessionResource: URI | undefined;
144 >
145 > constructor(
146 > @IConfigurationService private readonly _configurationService: IConfigurationService, chatDebugServiceImpl.ts ×25
147 > ) {
148 > super();
149 > }
151 > /** Priority for deduplicating events with the same ID: lower = richer. */
152 > private static readonly _eventKindPriority: Record<string, number> = {
153 > subagentInvocation: 0,
154 > modelTurn: 1,
155 > toolCall: 2,
156 > agentResponse: 3,
157 > userMessage: 4,
158 > generic: 5,
159 > };
160 >
161 > /** Session types eligible for debug logging and provider invocation. */
162 > private static readonly _debugEligibleSessionTypes = new Set([
163 > localChatSessionType, // local sessions
164 > 'copilotcli', // Copilot CLI background sessions
165 > 'agent-host-copilotcli', // local Agent Host Copilot CLI sessions
166 > 'claude-code', // Claude Code CLI sessions
167 > ]);
169 > private _isDebugEligibleSession(sessionResource: URI): boolean {
170 > const sessionType = getChatSessionType(sessionResource); chatDebugServiceImpl.ts ×2
171 > return ChatDebugServiceImpl._debugEligibleSessionTypes.has(sessionType)
172 > // Remote Agent Host Copilot CLI sessions use a dynamic chatDebugServiceImpl.ts ×1
173 > // `remote-<authority>-copilotcli` scheme; see copilotCliEventsUri.ts.
174 > || (sessionType.startsWith('remote-') && sessionType.endsWith('-copilotcli'))
175 > || this._importedSessions.has(sessionResource);
178 > /**
179 > * The in-memory event capacity for a session. Agent host (Copilot CLI)
180 > * sessions honor a dedicated, configurable cap so their (potentially large)
181 > * on-disk logs can be surfaced without changing the local-session default;
182 > * all other sessions use {@link ChatDebugServiceImpl.MAX_EVENTS_PER_SESSION}.
183 > */
184 > private _capacityForSession(sessionResource: URI): number {
185 > if (!isAgentHostTarget(getChatSessionType(sessionResource))) { chatDebugServiceImpl.ts ×10
186 > return ChatDebugServiceImpl.MAX_EVENTS_PER_SESSION;
187 > }
188 const configured = this._configurationService.getValue<number>(AgentHostAgentDebugLogMaxEventsSettingId);
189 > if (typeof configured === 'number' && Number.isFinite(configured) && configured >= 1) { chatDebugServiceImpl.ts ×10
190 return Math.floor(configured);
191 }
192 return ChatDebugServiceImpl.MAX_EVENTS_PER_SESSION;
195 > log(sessionResource: URI, name: string, details?: string, level: ChatDebugLogLevel = ChatDebugLogLevel.Info, options?: { id?: string; category?: string; parentEventId?: string }): void {
196 > if (!this._isDebugEligibleSession(sessionResource)) { chatDebugServiceImpl.ts ×2
198 > }
199 > this.addEvent({ chatDebugServiceImpl.ts ×1
200 > kind: 'generic',
201 > id: options?.id,
202 > sessionResource, chatDebugServiceImpl.ts ×2
203 > created: new Date(),
204 > name,
205 > details,
206 > level,
207 > category: options?.category,
208 > parentEventId: options?.parentEventId,
209 > });
210 > }
212 > addEvent(event: IChatDebugEvent): void {
213 > // Resolve the session's buffer (if any) once, and its capacity. New chatDebugServiceImpl.ts ×10
214 > // events during streaming target an existing buffer, so we reuse its
215 > // capacity and avoid re-reading configuration on the hot path.
216 > let buffer = this._sessionBuffers.get(event.sessionResource);
217 > const capacity = buffer?.capacity ?? this._capacityForSession(event.sessionResource);
218 >
219 > // Deduplicate events that share the same ID. The extension may emit
220 > // both a subagentInvocation and a userMessage from the same span;
221 > // keep the richer kind and discard the duplicate.
222 > if (event.id) {
223 > let seen = this._seenEventIds.get(event.sessionResource); chatDebugServiceImpl.ts ×3
224 > if (!seen) {
225 > seen = new Map();
226 > this._seenEventIds.set(event.sessionResource, seen);
227 > }
228 > const existingKind = seen.get(event.id);
229 > if (existingKind !== undefined) {
230 > const priority = ChatDebugServiceImpl._eventKindPriority; chatDebugServiceImpl.ts ×2
231 > if ((priority[event.kind] ?? 5) >= (priority[existingKind] ?? 5)) {
232 > return; // existing is richer or equal; skip this event chatDebugServiceImpl.ts ×1
233 > }
234 > // New event is richer — we can't remove the old one from chatDebugServiceImpl.ts ×2
235 > // the ring buffer, but the duplicate will be filtered out
236 > // in getEvents(). Update the tracked kind.
237 > }
238 > seen.set(event.id, event.kind); chatDebugServiceImpl.ts ×3
239 > // Cap the dedup map to prevent unbounded growth in long sessions.
240 > if (seen.size > capacity) {
241 // Delete the oldest entry (first key in insertion order).
242 const firstKey = seen.keys().next().value;
243 if (firstKey !== undefined) {
244 seen.delete(firstKey);
245 }
246 }
249 > if (!buffer) {
250 > // Evict least-recently-used session if we are at the session cap.
251 > if (this._sessionOrder.length >= ChatDebugServiceImpl.MAX_SESSIONS) {
252 > const evicted = this._sessionOrder.shift()!; chatDebugServiceImpl.ts ×3
253 > this._evictSession(evicted);
254 > }
255 > buffer = new SessionEventBuffer(capacity); chatDebugServiceImpl.ts ×10
256 > this._sessionBuffers.set(event.sessionResource, buffer);
257 > this._sessionOrder.push(event.sessionResource);
258 > } else {
259 > // Move to end of LRU order so actively-used sessions are not evicted. chatDebugServiceImpl.ts ×2
260 > // Fast-path: during streaming/backfill all events target the same
261 > // session which is already at the tail — skip the linear scan.
262 > const last = this._sessionOrder.length - 1;
263 > if (last < 0 || !extUri.isEqual(this._sessionOrder[last], event.sessionResource)) {
264 > const idx = this._sessionOrder.findIndex(u => extUri.isEqual(u, event.sessionResource)); chatDebugServiceImpl.ts ×1
265 > if (idx !== -1 && idx !== last) {
266 > this._sessionOrder.splice(idx, 1);
267 > this._sessionOrder.push(event.sessionResource);
268 > }
269 > }
271 > buffer.push(event); chatDebugServiceImpl.ts ×10
272 > this._onDidAddEvent.fire(event);
273 > }
275 > addProviderEvent(event: IChatDebugEvent): void {
276 > // If a re-invocation is pending for this session, clear the previously chatDebugServiceImpl.ts ×6
277 > // loaded provider events now that fresh data has actually arrived. This
278 > // is deferred (rather than done up front in invokeProviders) so that a
279 > // provider which returns nothing this cycle keeps the current events.
280 > if (this._pendingProviderClear.has(event.sessionResource)) {
281 > this._pendingProviderClear.delete(event.sessionResource);
282 > this._clearProviderEvents(event.sessionResource);
283 > }
284 > this._providerEvents.add(event);
285 > this.addEvent(event);
286 > }
288 > getEvents(sessionResource?: URI): readonly IChatDebugEvent[] {
289 > if (sessionResource) { chatDebugServiceImpl.ts ×2
290 > const buffer = this._sessionBuffers.get(sessionResource); chatDebugServiceImpl.ts ×1
291 > if (!buffer) {
292 > return []; chatDebugServiceImpl.ts ×1
293 > }
294 > let result = buffer.toArray(); chatDebugServiceImpl.ts ×6
295 > // Sort only when the buffer is not in chronological order,
296 > // which can happen when events arrive out of order (e.g.
297 > // tail-first backfill). When events arrive in
298 > // order (the common case) the check is O(n) with no sort.
299 > if (!this._isSorted(result)) {
300 result.sort((a, b) => a.created.getTime() - b.created.getTime());
301 }
302 > // Deduplicate: when multiple events share the same ID (e.g. chatDebugServiceImpl.ts ×6
303 > // subagentInvocation + userMessage from the same span), keep
304 > // the one with the richest kind.
305 > result = this._deduplicateEvents(result);
306 > return result;
307 > }
309 > // Cross-session query: merge all buffers and sort to interleave.
310 > const result: IChatDebugEvent[] = [];
311 > for (const buffer of this._sessionBuffers.values()) {
312 > result.push(...buffer.toArray()); chatDebugServiceImpl.ts ×1
313 > }
314 > result.sort((a, b) => a.created.getTime() - b.created.getTime()); chatDebugServiceImpl.ts ×2
315 > return result;
318 > private _isSorted(events: IChatDebugEvent[]): boolean {
319 > for (let i = 1; i < events.length; i++) { chatDebugServiceImpl.ts ×6
320 > if (events[i].created.getTime() < events[i - 1].created.getTime()) { chatDebugServiceImpl.ts ×2
321 return false;
322 }
324 > return true; chatDebugServiceImpl.ts ×6
325 > }
327 > private _deduplicateEvents(events: IChatDebugEvent[]): IChatDebugEvent[] {
328 > const seen = new Map<string, number>(); // id → index in result chatDebugServiceImpl.ts ×6
329 > const priority = ChatDebugServiceImpl._eventKindPriority;
330 > const result: IChatDebugEvent[] = [];
331 > for (const event of events) {
332 > if (!event.id) {
333 > result.push(event); chatDebugServiceImpl.ts ×1
334 > continue;
335 > }
336 > const existingIdx = seen.get(event.id); chatDebugServiceImpl.ts ×1
337 > if (existingIdx === undefined) {
338 > seen.set(event.id, result.length);
339 > result.push(event);
340 > } else {
341 > const existing = result[existingIdx]; chatDebugServiceImpl.ts ×1
342 > if ((priority[event.kind] ?? 5) < (priority[existing.kind] ?? 5)) {
343 > result[existingIdx] = event;
344 > }
345 > }
347 > return result;
348 > }
350 > getSessionResources(): readonly URI[] {
351 > return [...this._sessionOrder]; chatDebugServiceImpl.ts ×1
352 > }
354 > clear(): void {
355 > this._sessionBuffers.clear();
356 > this._sessionOrder.length = 0;
357 > this._seenEventIds.clear();
358 > this._importedSessions.clear();
359 > this._importedSessionTitles.clear();
360 > this._availableSessionResources.length = 0;
361 > this._availableSessionResourceSet.clear();
362 > this._historicalSessionTitles.clear();
363 > }
364 >
365 > /** Remove all ancillary state for an evicted session. */
366 > private _evictSession(sessionResource: URI): void {
367 > this._sessionBuffers.delete(sessionResource); chatDebugServiceImpl.ts ×3
368 > this._seenEventIds.delete(sessionResource);
369 > this._importedSessions.delete(sessionResource);
370 > this._importedSessionTitles.delete(sessionResource);
371 > const cts = this._invocationCts.get(sessionResource);
372 > if (cts) {
373 cts.cancel();
374 cts.dispose();
375 this._invocationCts.delete(sessionResource);
376 }
379 > registerProvider(provider: IChatDebugLogProvider): IDisposable {
380 > this._providers.add(provider); chatDebugServiceImpl.ts ×2
381 >
382 > // Invoke the new provider for all sessions that already have active
383 > // pipelines. This handles the case where invokeProviders() was called
384 > // before this provider was registered (e.g. extension activated late).
385 > for (const [sessionResource, cts] of this._invocationCts) {
386 > if (!cts.token.isCancellationRequested) { chatDebugServiceImpl.ts ×1
387 > this._invokeProvider(provider, sessionResource, cts.token).catch(onUnexpectedError);
388 > }
389 > }
391 > return toDisposable(() => {
392 > this._providers.delete(provider);
393 > });
394 > }
396 > hasInvokedProviders(sessionResource: URI): boolean {
397 return this._invocationCts.has(sessionResource);
398 }
400 > async invokeProviders(sessionResource: URI): Promise<void> {
402 > if (!this._isDebugEligibleSession(sessionResource)) {
404 > }
405 > // Cancel only the previous invocation for THIS session, not others. chatDebugServiceImpl.ts ×5
406 > // Each session has its own pipeline so events from multiple sessions
407 > // can be streamed concurrently.
408 > const existingCts = this._invocationCts.get(sessionResource);
409 > if (existingCts) {
410 > existingCts.cancel(); chatDebugServiceImpl.ts ×1
411 > existingCts.dispose();
412 > }
414 > // Mark provider events for this session to be cleared before the next
415 > // batch is applied. The clear is deferred to addProviderEvent so that a
416 > // provider returning nothing this cycle preserves the current events;
417 > // see _pendingProviderClear.
418 > this._pendingProviderClear.set(sessionResource, true);
419 >
420 > const cts = new CancellationTokenSource();
421 > this._invocationCts.set(sessionResource, cts);
422 >
423 > try {
424 > const promises = [...this._providers].map(provider =>
425 > this._invokeProvider(provider, sessionResource, cts.token)
426 > );
427 > await Promise.allSettled(promises);
428 > } catch (err) {
429 onUnexpectedError(err);
430 }
431 > // Note: do NOT dispose the CTS here - the token is used by the chatDebugServiceImpl.ts ×2
432 > // extension-side progress pipeline which stays alive for streaming.
433 > // It will be cancelled+disposed when re-invoking the same session
434 > // or when the service is disposed.
435 > }
437 > private async _invokeProvider(provider: IChatDebugLogProvider, sessionResource: URI, token: CancellationToken): Promise<void> {
439 > const events = await provider.provideChatDebugLog(sessionResource, token);
440 > if (events) { chatDebugServiceImpl.ts ×1
441 > // Yield to the event loop periodically so the UI stays chatDebugServiceImpl.ts ×2
442 > // responsive when a provider returns a large batch of events
443 > // (e.g. importing a multi-MB log file).
444 > const BATCH_SIZE = 500;
445 > for (let i = 0; i < events.length; i++) {
446 > if (token.isCancellationRequested) { chatDebugServiceImpl.ts ×6
447 break;
448 }
449 > this.addProviderEvent({ chatDebugServiceImpl.ts ×6
450 > ...events[i],
451 > sessionResource: events[i].sessionResource ?? sessionResource,
452 > });
453 > if (i > 0 && i % BATCH_SIZE === 0) {
454 await timeout(0);
455 }
458 > } catch (err) { chatDebugServiceImpl.ts ×5
459 > onUnexpectedError(err); chatDebugServiceImpl.ts ×1
460 > }
463 > endSession(sessionResource: URI): void {
464 > const cts = this._invocationCts.get(sessionResource); chatDebugServiceImpl.ts ×2
465 > if (cts) {
466 > cts.cancel(); chatDebugServiceImpl.ts ×1
467 > cts.dispose();
468 > this._invocationCts.delete(sessionResource);
469 > }
470 > this._onDidEndSession.fire(sessionResource); chatDebugServiceImpl.ts ×2
471 > }
473 > private _clearProviderEvents(sessionResource: URI): void {
474 > const buffer = this._sessionBuffers.get(sessionResource); chatDebugServiceImpl.ts ×6
475 > if (buffer) {
476 > // Provider events are typically the vast majority (90%+). chatDebugServiceImpl.ts ×3
477 > // Instead of iterating to remove them, extract the few core
478 > // events, clear the buffer, and re-add them.
479 > const coreEvents = buffer.toArray().filter(e => !this._providerEvents.has(e));
480 > buffer.clear();
481 > for (const e of coreEvents) {
482 buffer.push(e);
483 }
485 > // Reset dedup tracking so re-invoked provider events are accepted chatDebugServiceImpl.ts ×6
486 > this._seenEventIds.delete(sessionResource);
487 > this._onDidClearProviderEvents.fire(sessionResource);
488 > }
490 > async resolveEvent(eventId: string): Promise<IChatDebugResolvedEventContent | undefined> {
491 > for (const provider of this._providers) { chatDebugServiceImpl.ts ×2
492 > if (provider.resolveChatDebugLogEvent) { chatDebugServiceImpl.ts ×3
493 > try {
494 > const resolved = await provider.resolveChatDebugLogEvent(eventId, CancellationToken.None);
495 > if (resolved !== undefined) {
496 > return resolved; chatDebugServiceImpl.ts ×1
497 > }
498 > } catch (err) { chatDebugServiceImpl.ts ×3
499 onUnexpectedError(err);
500 }
502 > }
503 > return undefined; chatDebugServiceImpl.ts ×1
506 > isCoreEvent(event: IChatDebugEvent): boolean {
507 return !this._providerEvents.has(event);
508 }
510 > setImportedSessionTitle(sessionResource: URI, title: string): void {
511 this._importedSessionTitles.set(sessionResource, title);
512 }
514 > getImportedSessionTitle(sessionResource: URI): string | undefined {
515 return this._importedSessionTitles.get(sessionResource);
516 }
518 > addAvailableSessionResources(resources: readonly { uri: URI; title?: string }[]): void {
519 let added = false;
520 for (const { uri, title } of resources) {
521 const key = uri.toString();
522 if (!this._availableSessionResourceSet.has(key)) {
523 this._availableSessionResourceSet.add(key);
524 this._availableSessionResources.push(uri);
525 added = true;
526 }
527 if (title) {
528 this._historicalSessionTitles.set(uri, title);
529 }
530 }
531 if (added) {
532 this._onDidChangeAvailableSessionResources.fire();
533 }
534 }
536 > /** Lazy fetchers for available sessions from providers. Each is invoked at most once. */
537 > private readonly _availableSessionsFetchers = new Set<{ readonly fetcher: (token: CancellationToken) => Promise<{ uri: URI; title?: string }[]>; started: boolean }>();
538 > private _availableSessionsRequested = false;
540 > getAvailableSessionResources(): readonly URI[] {
541 // Trigger lazy fetch when both a fetcher is registered and this getter is called.
542 this._availableSessionsRequested = true;
543 this._tryFetchAvailableSessions();
544
545 const known = new Set(this._sessionOrder.map(u => u.toString()));
546 const result = [...this._sessionOrder];
547 for (const uri of this._availableSessionResources) {
548 if (!known.has(uri.toString())) {
549 known.add(uri.toString());
550 result.push(uri);
551 }
552 }
553 return result;
554 }
556 > registerAvailableSessionsFetcher(fetcher: (token: CancellationToken) => Promise<{ uri: URI; title?: string }[]>): IDisposable {
557 const entry = { fetcher, started: false };
558 this._availableSessionsFetchers.add(entry);
559 // If the UI already requested sessions before the fetcher was registered, fetch now.
560 this._tryFetchAvailableSessions();
561 return toDisposable(() => this._availableSessionsFetchers.delete(entry));
562 }
564 > private _tryFetchAvailableSessions(): void {
565 if (!this._availableSessionsRequested) {
566 return;
567 }
568 for (const entry of this._availableSessionsFetchers) {
569 if (entry.started) {
570 continue;
571 }
572 entry.started = true;
573 // Fire-and-forget: don't block the caller.
574 entry.fetcher(CancellationToken.None).then(entries => {
575 if (entries.length > 0) {
576 this.addAvailableSessionResources(entries);
577 }
578 }).catch(onUnexpectedError);
579 }
580 }
582 > getHistoricalSessionTitle(sessionResource: URI): string | undefined {
583 return this._historicalSessionTitles.get(sessionResource);
584 }
586 > async exportLog(sessionResource: URI): Promise<Uint8Array | undefined> {
587 for (const provider of this._providers) {
588 if (provider.provideChatDebugLogExport) {
589 try {
590 const data = await provider.provideChatDebugLogExport(sessionResource, CancellationToken.None);
591 if (data !== undefined) {
592 return data;
593 }
594 } catch (err) {
595 onUnexpectedError(err);
596 }
597 }
598 }
599 return undefined;
600 }
602 > async importLog(data: Uint8Array): Promise<URI | undefined> {
603 for (const provider of this._providers) {
604 if (provider.resolveChatDebugLogImport) {
605 try {
606 const sessionUri = await provider.resolveChatDebugLogImport(data, CancellationToken.None);
607 if (sessionUri !== undefined) {
608 this._importedSessions.set(sessionUri, true);
609 return sessionUri;
610 }
611 } catch (err) {
612 onUnexpectedError(err);
613 }
614 }
615 }
616 return undefined;
617 }
619 > override dispose(): void {
620 > for (const cts of this._invocationCts.values()) { chatDebugServiceImpl.ts ×25
621 > cts.cancel(); chatDebugServiceImpl.ts ×1
622 > cts.dispose();
623 > }
624 > this._invocationCts.clear(); chatDebugServiceImpl.ts ×25
625 > this.clear();
626 > this._providers.clear();
627 > super.dispose();
628 > }