Show historical debug sessions in Agent Debug Panel (#309073)

* Show historical debug sessions in Agent Debug Logs

* Feedback update

* add test
This commit is contained in:
Vijay Upadya
2026-04-10 14:09:57 -07:00
committed by GitHub
parent aeca54ad55
commit a19305eca8
14 changed files with 420 additions and 30 deletions

View File

@@ -1305,6 +1305,31 @@ export class ChatDebugFileLoggerService extends Disposable implements IChatDebug
}
}
async listSessionIds(): Promise<string[]> {
const dir = this._getDebugLogsDir();
if (!dir) {
return [];
}
try {
const entries = await this._fileSystemService.readDirectory(dir);
const dirs = entries.filter(([, type]) => type === 2 /* FileType.Directory */);
// Stat each directory in parallel to sort by most recently modified.
const withMtime = await Promise.all(dirs.map(async ([name]) => {
try {
const stat = await this._fileSystemService.stat(URI.joinPath(dir, name));
return { name, mtime: stat.mtime };
} catch {
return { name, mtime: 0 };
}
}));
withMtime.sort((a, b) => b.mtime - a.mtime);
return withMtime.map(e => e.name);
} catch {
return [];
}
}
private async _cleanupOldLogs(): Promise<void> {
const dir = this._getDebugLogsDir();
if (!dir) {

View File

@@ -750,4 +750,70 @@ describe('ChatDebugFileLoggerService', () => {
expect(childHooks).toHaveLength(1);
expect(childHooks[0].name).toBe('PreToolUse');
});
describe('listSessionIds', () => {
it('returns empty when no sessions exist', async () => {
const ids = await service.listSessionIds();
expect(ids).toHaveLength(0);
});
it('lists session directories on disk', async () => {
await service.startSession('session-a');
otelService.fireSpan(makeToolCallSpan('session-a', 'read_file'));
await service.flush('session-a');
await service.startSession('session-b');
otelService.fireSpan(makeToolCallSpan('session-b', 'edit_file'));
await service.flush('session-b');
const ids = await service.listSessionIds();
expect(ids).toContain('session-a');
expect(ids).toContain('session-b');
});
it('returns sessions sorted by most recently modified first', async () => {
await service.startSession('older-session');
otelService.fireSpan(makeToolCallSpan('older-session', 'read_file'));
await service.flush('older-session');
// Small delay so mtime differs
await new Promise(resolve => setTimeout(resolve, 50));
await service.startSession('newer-session');
otelService.fireSpan(makeToolCallSpan('newer-session', 'edit_file'));
await service.flush('newer-session');
const ids = await service.listSessionIds();
expect(ids.indexOf('newer-session')).toBeLessThan(ids.indexOf('older-session'));
});
it('does not include non-directory entries', async () => {
// Create a session directory
await service.startSession('real-session');
otelService.fireSpan(makeToolCallSpan('real-session', 'read_file'));
await service.flush('real-session');
// Create a stray file in the debug-logs directory
const debugLogsDir = service.debugLogsDir!;
await fs.promises.writeFile(path.join(debugLogsDir.fsPath, 'stray-file.jsonl'), '{}');
const ids = await service.listSessionIds();
expect(ids).toContain('real-session');
expect(ids).not.toContain('stray-file.jsonl');
});
it('handles stat failures gracefully', async () => {
await service.startSession('good-session');
otelService.fireSpan(makeToolCallSpan('good-session', 'read_file'));
await service.flush('good-session');
// Create an empty directory that can be listed but stat should still work
const debugLogsDir = service.debugLogsDir!;
await fs.promises.mkdir(path.join(debugLogsDir.fsPath, 'empty-dir'));
const ids = await service.listSessionIds();
expect(ids).toContain('good-session');
expect(ids).toContain('empty-dir');
});
});
});

View File

@@ -753,17 +753,30 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
// log entries are routed to a dedicated child JSONL file.
// parentChatSessionId is only set on subagent requests
// (see CapturingToken setup in defaultIntentRequestHandler).
if (parentChatSessionId && chatSessionId) {
const childLabel = debugLogLabel ?? `runSubagent-${agentName}`;
if (chatSessionId) {
const fileLogger = this._instantiationService.invokeFunction(accessor =>
accessor.get(IChatDebugFileLoggerService));
fileLogger.startChildSession(
chatSessionId, parentChatSessionId, childLabel, parentTraceContext?.spanId);
// Also register the invoke_agent span's ID so that hook spans
// (whose parentSpanId is this span) are routed to the child session.
const invokeSpanId = span.getSpanContext()?.spanId;
if (invokeSpanId) {
fileLogger.registerSpanSession(invokeSpanId, chatSessionId);
// Register this session as a child of its parent so that debug
// log entries are routed to a dedicated child JSONL file.
// parentChatSessionId is only set on subagent requests
// (see CapturingToken setup in defaultIntentRequestHandler).
if (parentChatSessionId) {
const childLabel = debugLogLabel ?? `runSubagent-${agentName}`;
fileLogger.startChildSession(
chatSessionId, parentChatSessionId, childLabel, parentTraceContext?.spanId);
// Also register the invoke_agent span's ID so that hook spans
// (whose parentSpanId is this span) are routed to the child session.
const invokeSpanId = span.getSpanContext()?.spanId;
if (invokeSpanId) {
fileLogger.registerSpanSession(invokeSpanId, chatSessionId);
}
} else {
// For top-level agent invocations (not subagents), start a debug
// file logging session so entries are flushed to JSONL on disk.
// This is idempotent — calling startSession on an already-started
// session just promotes it if needed.
fileLogger.startSession(chatSessionId).catch(() => { /* best effort */ });
}
}

View File

@@ -93,6 +93,8 @@ export class OTelChatDebugLogProviderContribution extends Disposable implements
this._provideChatDebugLogExport(sessionResource, options, token),
resolveChatDebugLogImport: (data, token) =>
this._resolveChatDebugLogImport(data, token),
provideAvailableDebugSessionResources: (token) =>
this._getAvailableDebugSessionResources(token),
}));
} catch (e) {
this._logService.warn(`[OTelDebug] Failed to register debug log provider: ${e}`);
@@ -690,6 +692,71 @@ export class OTelChatDebugLogProviderContribution extends Disposable implements
return undefined;
}
}
private async _getAvailableDebugSessionResources(
token: vscode.CancellationToken,
): Promise<{ uri: vscode.Uri; title?: string }[]> {
// Sessions are returned sorted by most recent first from listSessionIds().
// We only read JSONL tails for a limited batch to keep startup fast.
// The home view shows PAGE_SIZE (5) at a time, so reading ~15 covers
// the visible page plus a buffer for filtered-out discovery-only sessions.
const MAX_TAIL_READS = 15;
try {
const sessionIds = await this._fileLogger.listSessionIds();
// Read tails only for the first batch (most recent sessions).
const toRead = sessionIds.slice(0, MAX_TAIL_READS);
const settled = await Promise.allSettled(toRead.map(async (id): Promise<{ uri: vscode.Uri; title?: string } | undefined> => {
if (token.isCancellationRequested) {
return undefined;
}
const encoded = Buffer.from(id).toString('base64url');
const uri = vscode.Uri.parse(`vscode-chat-session://local/${encoded}`);
let title: string | undefined;
let hasRealEvents = false;
try {
const entries = await this._fileLogger.readTailEntries(id, 50);
const userMsg = entries.find(e => e.type === 'user_message');
if (userMsg) {
hasRealEvents = true;
const content = userMsg.attrs.content as string | undefined;
if (content) {
title = content.length > 80 ? content.slice(0, 80) + '\u2026' : content;
}
}
if (!hasRealEvents) {
hasRealEvents = entries.some(e =>
e.type === 'tool_call' || e.type === 'llm_request' ||
e.type === 'agent_response' || e.type === 'subagent'
);
}
} catch {
// best effort
}
if (!hasRealEvents) {
return undefined;
}
if (!title) {
const shortId = id.length > 12 ? id.slice(0, 12) + '\u2026' : id;
title = `Session ${shortId}`;
}
return { uri, title };
}));
const results: { uri: vscode.Uri; title?: string }[] = [];
for (const entry of settled) {
if (entry.status === 'fulfilled' && entry.value) {
results.push(entry.value);
}
}
return results;
} catch (err) {
this._logService.error(`[OTelDebug] Failed to list available sessions: ${err}`);
return [];
}
}
}
// ── Helpers ──

View File

@@ -737,6 +737,17 @@ declare module 'vscode' {
data: Uint8Array,
token: CancellationToken
): ProviderResult<ChatDebugLogImportResult>;
/**
* Return session resource URIs that have debug log data available,
* including historical sessions persisted on disk.
*
* @param token A cancellation token.
* @returns Session URIs with available debug data and optional titles.
*/
provideAvailableDebugSessionResources?(
token: CancellationToken
): ProviderResult<{ uri: Uri; title?: string }[]>;
}
export namespace chat {

View File

@@ -139,6 +139,12 @@ export interface IChatDebugFileLoggerService {
* Uses a streaming parser to avoid loading the entire file into memory.
*/
streamEntries(sessionId: string, onEntry: (entry: IDebugLogEntry) => void): Promise<void>;
/**
* List session IDs that have debug log directories on disk.
* Returns both active and historical sessions found in the debug-logs/ directory.
*/
listSessionIds(): Promise<string[]>;
}
/**
@@ -191,4 +197,5 @@ export class NullChatDebugFileLoggerService implements IChatDebugFileLoggerServi
async readEntries(): Promise<IDebugLogEntry[]> { return []; }
async readTailEntries(): Promise<IDebugLogEntry[]> { return []; }
async streamEntries(): Promise<void> { }
async listSessionIds(): Promise<string[]> { return []; }
}

View File

@@ -75,6 +75,13 @@ export class MainThreadChatDebug extends Disposable implements MainThreadChatDeb
return uri;
}
}));
// Register a lazy fetcher so historical sessions are loaded from the
// extension only when the debug panel home page first needs them.
this._chatDebugService.registerAvailableSessionsFetcher(async (token) => {
const entries = await this._proxy.$getAvailableDebugSessionResources(handle, token);
return entries.map(e => ({ uri: URI.revive(e.uri), title: e.title }));
});
}
$unregisterChatDebugLogProvider(handle: number): void {

View File

@@ -1542,6 +1542,7 @@ export interface ExtHostChatDebugShape {
$resolveChatDebugLogEvent(handle: number, eventId: string, token: CancellationToken): Promise<IChatDebugResolvedEventContentDto | undefined>;
$exportChatDebugLog(handle: number, sessionResource: UriComponents, coreEvents: IChatDebugEventDto[], sessionTitle: string | undefined, token: CancellationToken): Promise<VSBuffer | undefined>;
$importChatDebugLog(handle: number, data: VSBuffer, token: CancellationToken): Promise<{ uri: UriComponents; sessionTitle?: string } | undefined>;
$getAvailableDebugSessionResources(handle: number, token: CancellationToken): Promise<{ uri: UriComponents; title?: string }[]>;
$onCoreDebugEvent(event: IChatDebugEventDto): void;
}

View File

@@ -422,6 +422,14 @@ export class ExtHostChatDebug extends Disposable implements ExtHostChatDebugShap
return { uri: result.uri, sessionTitle: result.sessionTitle };
}
async $getAvailableDebugSessionResources(_handle: number, token: CancellationToken): Promise<{ uri: UriComponents; title?: string }[]> {
if (!this._provider?.provideAvailableDebugSessionResources) {
return [];
}
const result = await this._provider.provideAvailableDebugSessionResources(token);
return result ?? [];
}
override dispose(): void {
for (const store of this._activeProgress.values()) {
store.dispose();

View File

@@ -19,10 +19,13 @@ import { IChatService } from '../../common/chatService/chatService.js';
import { AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING } from '../../common/promptSyntax/promptTypes.js';
import { getChatSessionType, isUntitledChatSession, LocalChatSessionUri } from '../../common/model/chatUri.js';
import { IChatWidgetService } from '../chat.js';
import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js';
import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';
const $ = DOM.$;
const PAGE_SIZE = 5;
export class ChatDebugHomeView extends Disposable {
private readonly _onNavigateToSession = this._register(new Emitter<URI>());
@@ -32,11 +35,21 @@ export class ChatDebugHomeView extends Disposable {
private readonly scrollContent: HTMLElement;
private readonly renderDisposables = this._register(new DisposableStore());
/** Number of sessions currently visible (grows on "Show More"). */
private _visibleCount = PAGE_SIZE;
/** Session resource that the user last navigated to from the home view. */
private _lastOpenedSessionResource: URI | undefined;
/** Tracks the number of known sessions so we can detect new ones. */
private _lastKnownSessionCount = 0;
constructor(
parent: HTMLElement,
@IChatService private readonly chatService: IChatService,
@IChatDebugService private readonly chatDebugService: IChatDebugService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IPreferencesService private readonly preferencesService: IPreferencesService,
) {
@@ -49,6 +62,24 @@ export class ChatDebugHomeView extends Disposable {
this.render();
}
}));
// Re-render when a new session appears so it surfaces at the top.
this._register(this.chatDebugService.onDidAddEvent(e => {
const currentCount = this.chatDebugService.getSessionResources().length;
if (currentCount !== this._lastKnownSessionCount) {
this._lastKnownSessionCount = currentCount;
if (this.container.style.display !== 'none') {
this.render();
}
}
}));
// Re-render when historical sessions are discovered from disk.
this._register(this.chatDebugService.onDidChangeAvailableSessionResources(() => {
if (this.container.style.display !== 'none') {
this.render();
}
}));
}
show(): void {
@@ -61,6 +92,22 @@ export class ChatDebugHomeView extends Disposable {
}
render(): void {
const isFileLoggingEnabled = this.configurationService.getValue<boolean>(AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING);
this._lastKnownSessionCount = this.chatDebugService.getSessionResources().length;
const sessionResources = isFileLoggingEnabled
? this._getFilteredSessionResources(this.chatDebugService.getAvailableSessionResources())
: [];
this._renderWithSessions(sessionResources);
}
private _getFilteredSessionResources(resources: readonly URI[]): URI[] {
const cliSessionTypes = new Set(['copilotcli', 'claude-code']);
return [...resources]
.filter(r => !cliSessionTypes.has(getChatSessionType(r)) || !isUntitledChatSession(r));
}
private _renderWithSessions(sessionResources: URI[]): void {
DOM.clearNode(this.scrollContent);
this.renderDisposables.clear();
@@ -85,24 +132,19 @@ export class ChatDebugHomeView extends Disposable {
const activeWidget = this.chatWidgetService.lastFocusedWidget;
const activeSessionResource = activeWidget?.viewModel?.sessionResource;
// List sessions that have debug event data.
// Use the debug service as the source of truth — it includes sessions
// whose chat models may have been archived (e.g. when a new chat was started).
const cliSessionTypes = new Set(['copilotcli', 'claude-code']);
const sessionResources = [...this.chatDebugService.getSessionResources()].reverse()
// Hide untitled bootstrap sessions for CLI session types (e.g. copilotcli, claude-code).
// These are transient sessions created during async session setup that only contain
// a single "Load Hooks" event and would confuse users.
.filter(r => !cliSessionTypes.has(getChatSessionType(r)) || !isUntitledChatSession(r));
// Sort: active session first
if (activeSessionResource) {
const activeIndex = sessionResources.findIndex(r => r.toString() === activeSessionResource.toString());
if (activeIndex > 0) {
sessionResources.splice(activeIndex, 1);
sessionResources.unshift(activeSessionResource);
// Bubble active sessions to top
const bubbleToTop = (resource: URI | undefined) => {
if (!resource) {
return;
}
}
const idx = sessionResources.findIndex(r => r.toString() === resource.toString());
if (idx > 0) {
sessionResources.splice(idx, 1);
sessionResources.unshift(resource);
}
};
bubbleToTop(this._lastOpenedSessionResource);
bubbleToTop(activeSessionResource);
DOM.append(this.scrollContent, $('p.chat-debug-home-subtitle', undefined,
sessionResources.length > 0
@@ -111,22 +153,29 @@ export class ChatDebugHomeView extends Disposable {
));
if (sessionResources.length > 0) {
const visibleSessions = sessionResources.slice(0, this._visibleCount);
const sessionList = DOM.append(this.scrollContent, $('.chat-debug-home-session-list'));
sessionList.setAttribute('role', 'list');
sessionList.setAttribute('aria-label', localize('chatDebug.sessionList', "Chat sessions"));
const items: HTMLButtonElement[] = [];
for (const sessionResource of sessionResources) {
const rawTitle = this.chatService.getSessionTitle(sessionResource);
for (const sessionResource of visibleSessions) {
// Resolve title: agent sessions model (same as sidebar) → chat service → historical from JSONL → fallback
const agentSession = this.agentSessionsService.model.getSession(sessionResource);
const rawTitle = agentSession?.label ?? this.chatService.getSessionTitle(sessionResource);
const importedTitle = this.chatDebugService.getImportedSessionTitle(sessionResource);
const historicalTitle = this.chatDebugService.getHistoricalSessionTitle(sessionResource);
let sessionTitle: string;
if (rawTitle && !isUUID(rawTitle)) {
sessionTitle = rawTitle;
} else if (LocalChatSessionUri.isLocalSession(sessionResource)) {
sessionTitle = localize('chatDebug.newSession', "New Chat");
} else if (historicalTitle) {
sessionTitle = historicalTitle;
} else if (importedTitle) {
sessionTitle = localize('chatDebug.importedSession', "Imported: {0}", importedTitle);
} else if (LocalChatSessionUri.isLocalSession(sessionResource)) {
sessionTitle = localize('chatDebug.newSession', "New Chat");
} else if (getChatSessionType(sessionResource) === 'copilotcli') {
const pathId = sessionResource.path.replace(/^\//, '').split('-')[0];
const shortId = pathId || sessionResource.authority || sessionResource.toString();
@@ -161,11 +210,24 @@ export class ChatDebugHomeView extends Disposable {
}
this.renderDisposables.add(DOM.addDisposableListener(item, DOM.EventType.CLICK, () => {
this._lastOpenedSessionResource = sessionResource;
this._onNavigateToSession.fire(sessionResource);
}));
items.push(item);
}
// "Show More" button when there are more sessions to display
if (sessionResources.length > this._visibleCount) {
const remaining = sessionResources.length - this._visibleCount;
const showMoreButton = this.renderDisposables.add(new Button(this.scrollContent, { ...defaultButtonStyles, secondary: true }));
showMoreButton.element.classList.add('chat-debug-home-show-more');
showMoreButton.label = localize('chatDebug.showMore', "Show More ({0})", remaining);
this.renderDisposables.add(showMoreButton.onDidClick(() => {
this._visibleCount += PAGE_SIZE;
this.render();
}));
}
// Arrow key navigation between session items
this.renderDisposables.add(DOM.addDisposableListener(sessionList, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
if (items.length === 0) {

View File

@@ -100,6 +100,12 @@
font-size: 11px;
font-weight: 500;
}
.chat-debug-home-show-more {
margin-top: 8px;
width: auto;
max-width: 400px;
align-self: center;
}
@keyframes chat-debug-shimmer {
0% { background-position: 120% 0; }

View File

@@ -236,6 +236,35 @@ export interface IChatDebugService extends IDisposable {
*/
getImportedSessionTitle(sessionResource: URI): string | undefined;
/**
* Fired when available session resources change (e.g. historical sessions discovered from disk).
*/
readonly onDidChangeAvailableSessionResources: Event<void>;
/**
* Store session resources that have debug log data available on disk.
* Called by the main thread after the extension reports historical sessions.
*/
addAvailableSessionResources(resources: readonly { uri: URI; title?: string }[]): void;
/**
* Get all session resources that have debug log data available,
* including historical sessions persisted on disk by the provider.
* Triggers a lazy fetch from the registered fetcher on first call.
*/
getAvailableSessionResources(): readonly URI[];
/**
* Register a callback that fetches available session resources from a provider.
* Called lazily when `getAvailableSessionResources()` is first invoked.
*/
registerAvailableSessionsFetcher(fetcher: (token: CancellationToken) => Promise<{ uri: URI; title?: string }[]>): void;
/**
* Get the stored title for a historical session discovered from disk.
*/
getHistoricalSessionTitle(sessionResource: URI): string | undefined;
}
/**

View File

@@ -103,6 +103,9 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic
private readonly _onDidClearProviderEvents = this._register(new Emitter<URI>());
readonly onDidClearProviderEvents: Event<URI> = this._onDidClearProviderEvents.event;
private readonly _onDidChangeAvailableSessionResources = this._register(new Emitter<void>());
readonly onDidChangeAvailableSessionResources: Event<void> = this._onDidChangeAvailableSessionResources.event;
private readonly _providers = new Set<IChatDebugLogProvider>();
private readonly _invocationCts = new ResourceMap<CancellationTokenSource>();
@@ -112,6 +115,13 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic
/** Session URIs created via import. */
private readonly _importedSessions = new ResourceMap<boolean>();
/** Session URIs reported by providers as available on disk (historical sessions). */
private readonly _availableSessionResources: URI[] = [];
private readonly _availableSessionResourceSet = new Set<string>();
/** Titles for historical sessions discovered from disk. */
private readonly _historicalSessionTitles = new ResourceMap<string>();
/** Human-readable titles for imported sessions. */
private readonly _importedSessionTitles = new ResourceMap<string>();
@@ -291,6 +301,9 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic
this._seenEventIds.clear();
this._importedSessions.clear();
this._importedSessionTitles.clear();
this._availableSessionResources.length = 0;
this._availableSessionResourceSet.clear();
this._historicalSessionTitles.clear();
}
/** Remove all ancillary state for an evicted session. */
@@ -444,6 +457,70 @@ export class ChatDebugServiceImpl extends Disposable implements IChatDebugServic
return this._importedSessionTitles.get(sessionResource);
}
addAvailableSessionResources(resources: readonly { uri: URI; title?: string }[]): void {
let added = false;
for (const { uri, title } of resources) {
const key = uri.toString();
if (!this._availableSessionResourceSet.has(key)) {
this._availableSessionResourceSet.add(key);
this._availableSessionResources.push(uri);
added = true;
}
if (title) {
this._historicalSessionTitles.set(uri, title);
}
}
if (added) {
this._onDidChangeAvailableSessionResources.fire();
}
}
/** Lazy fetcher for available sessions from the extension. */
private _availableSessionsFetcher: ((token: CancellationToken) => Promise<{ uri: URI; title?: string }[]>) | undefined;
private _availableSessionsFetchStarted = false;
private _availableSessionsRequested = false;
getAvailableSessionResources(): readonly URI[] {
// Trigger lazy fetch when both a fetcher is registered and this getter is called.
this._availableSessionsRequested = true;
this._tryFetchAvailableSessions();
const known = new Set(this._sessionOrder.map(u => u.toString()));
const result = [...this._sessionOrder];
for (const uri of this._availableSessionResources) {
if (!known.has(uri.toString())) {
known.add(uri.toString());
result.push(uri);
}
}
return result;
}
registerAvailableSessionsFetcher(fetcher: (token: CancellationToken) => Promise<{ uri: URI; title?: string }[]>): void {
this._availableSessionsFetcher = fetcher;
this._availableSessionsFetchStarted = false;
// If the UI already requested sessions before the fetcher was registered, fetch now.
this._tryFetchAvailableSessions();
}
private _tryFetchAvailableSessions(): void {
if (!this._availableSessionsFetcher || !this._availableSessionsRequested || this._availableSessionsFetchStarted) {
return;
}
this._availableSessionsFetchStarted = true;
// Fire-and-forget: don't block the caller.
const fetcher = this._availableSessionsFetcher;
fetcher(CancellationToken.None).then(entries => {
if (entries.length > 0) {
this.addAvailableSessionResources(entries);
}
}).catch(onUnexpectedError);
}
getHistoricalSessionTitle(sessionResource: URI): string | undefined {
return this._historicalSessionTitles.get(sessionResource);
}
async exportLog(sessionResource: URI): Promise<Uint8Array | undefined> {
for (const provider of this._providers) {
if (provider.provideChatDebugLogExport) {

View File

@@ -737,6 +737,17 @@ declare module 'vscode' {
data: Uint8Array,
token: CancellationToken
): ProviderResult<ChatDebugLogImportResult>;
/**
* Return session resource URIs that have debug log data available,
* including historical sessions persisted on disk.
*
* @param token A cancellation token.
* @returns Session URIs with available debug data and optional titles.
*/
provideAvailableDebugSessionResources?(
token: CancellationToken
): ProviderResult<{ uri: Uri; title?: string }[]>;
}
export namespace chat {