Path: blob/main/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugHomeView.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as DOM from '../../../../../base/browser/dom.js';6import { Button } from '../../../../../base/browser/ui/button/button.js';7import { Codicon } from '../../../../../base/common/codicons.js';8import { Emitter } from '../../../../../base/common/event.js';9import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';10import { ThemeIcon } from '../../../../../base/common/themables.js';11import { URI } from '../../../../../base/common/uri.js';12import { isUUID } from '../../../../../base/common/uuid.js';13import { localize } from '../../../../../nls.js';14import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';15import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';16import { IChatDebugService } from '../../common/chatDebugService.js';17import { IChatService } from '../../common/chatService/chatService.js';18import { AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING } from '../../common/promptSyntax/promptTypes.js';19import { getChatSessionType, isUntitledChatSession, LocalChatSessionUri } from '../../common/model/chatUri.js';20import { IChatWidgetService } from '../chat.js';21import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js';22import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';2324const $ = DOM.$;2526const PAGE_SIZE = 5;2728export class ChatDebugHomeView extends Disposable {2930private readonly _onNavigateToSession = this._register(new Emitter<URI>());31readonly onNavigateToSession = this._onNavigateToSession.event;3233readonly container: HTMLElement;34private readonly scrollContent: HTMLElement;35private readonly renderDisposables = this._register(new DisposableStore());3637/** Number of sessions currently visible (grows on "Show More"). */38private _visibleCount = PAGE_SIZE;3940/** Session resource that the user last navigated to from the home view. */41private _lastOpenedSessionResource: URI | undefined;4243/** Tracks the number of known sessions so we can detect new ones. */44private _lastKnownSessionCount = 0;4546constructor(47parent: HTMLElement,48@IChatService private readonly chatService: IChatService,49@IChatDebugService private readonly chatDebugService: IChatDebugService,50@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,51@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,52@IConfigurationService private readonly configurationService: IConfigurationService,53@IPreferencesService private readonly preferencesService: IPreferencesService,54) {55super();56this.container = DOM.append(parent, $('.chat-debug-home'));57this.scrollContent = DOM.append(this.container, $('div.chat-debug-home-content'));5859this._register(this.configurationService.onDidChangeConfiguration(e => {60if (e.affectsConfiguration(AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING)) {61this.render();62}63}));6465// Re-render when a new session appears so it surfaces at the top.66this._register(this.chatDebugService.onDidAddEvent(e => {67const currentCount = this.chatDebugService.getSessionResources().length;68if (currentCount !== this._lastKnownSessionCount) {69this._lastKnownSessionCount = currentCount;70if (this.container.style.display !== 'none') {71this.render();72}73}74}));7576// Re-render when historical sessions are discovered from disk.77this._register(this.chatDebugService.onDidChangeAvailableSessionResources(() => {78if (this.container.style.display !== 'none') {79this.render();80}81}));82}8384show(): void {85this.container.style.display = '';86this.render();87}8889hide(): void {90this.container.style.display = 'none';91}9293render(): void {94const isFileLoggingEnabled = this.configurationService.getValue<boolean>(AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING);95this._lastKnownSessionCount = this.chatDebugService.getSessionResources().length;9697const sessionResources = isFileLoggingEnabled98? this._getFilteredSessionResources(this.chatDebugService.getAvailableSessionResources())99: [];100this._renderWithSessions(sessionResources);101}102103private _getFilteredSessionResources(resources: readonly URI[]): URI[] {104const cliSessionTypes = new Set(['copilotcli', 'claude-code']);105return [...resources]106.filter(r => !cliSessionTypes.has(getChatSessionType(r)) || !isUntitledChatSession(r));107}108109private _renderWithSessions(sessionResources: URI[]): void {110DOM.clearNode(this.scrollContent);111this.renderDisposables.clear();112113DOM.append(this.scrollContent, $('h2.chat-debug-home-title', undefined, localize('chatDebug.title', "Agent Debug Logs")));114115const isEnabled = this.configurationService.getValue<boolean>(AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING);116if (!isEnabled) {117DOM.append(this.scrollContent, $('p.chat-debug-home-subtitle', undefined,118localize('chatDebug.disabled', "Enable to view debug logs and investigate chat issues with /troubleshoot.")119));120121const enableButton = this.renderDisposables.add(new Button(this.scrollContent, { ...defaultButtonStyles, secondary: true }));122enableButton.element.style.width = 'auto';123enableButton.label = localize('chatDebug.openSetting', "Enable in Settings");124this.renderDisposables.add(enableButton.onDidClick(() => {125this.preferencesService.openSettings({ jsonEditor: false, query: `@id:${AGENT_DEBUG_LOG_FILE_LOGGING_ENABLED_SETTING}` });126}));127return;128}129130// Determine the active session resource131const activeWidget = this.chatWidgetService.lastFocusedWidget;132const activeSessionResource = activeWidget?.viewModel?.sessionResource;133134// Bubble active sessions to top135const bubbleToTop = (resource: URI | undefined) => {136if (!resource) {137return;138}139const idx = sessionResources.findIndex(r => r.toString() === resource.toString());140if (idx > 0) {141sessionResources.splice(idx, 1);142sessionResources.unshift(resource);143}144};145bubbleToTop(this._lastOpenedSessionResource);146bubbleToTop(activeSessionResource);147148DOM.append(this.scrollContent, $('p.chat-debug-home-subtitle', undefined,149sessionResources.length > 0150? localize('chatDebug.homeSubtitle', "Select a chat session to debug")151: localize('chatDebug.noSessions', "Send a chat message to get started")152));153154if (sessionResources.length > 0) {155const visibleSessions = sessionResources.slice(0, this._visibleCount);156157const sessionList = DOM.append(this.scrollContent, $('.chat-debug-home-session-list'));158sessionList.setAttribute('role', 'list');159sessionList.setAttribute('aria-label', localize('chatDebug.sessionList', "Chat sessions"));160161const items: HTMLButtonElement[] = [];162163for (const sessionResource of visibleSessions) {164// Resolve title: agent sessions model (same as sidebar) → chat service → historical from JSONL → fallback165const agentSession = this.agentSessionsService.model.getSession(sessionResource);166const rawTitle = agentSession?.label ?? this.chatService.getSessionTitle(sessionResource);167const importedTitle = this.chatDebugService.getImportedSessionTitle(sessionResource);168const historicalTitle = this.chatDebugService.getHistoricalSessionTitle(sessionResource);169let sessionTitle: string;170if (rawTitle && !isUUID(rawTitle)) {171sessionTitle = rawTitle;172} else if (historicalTitle) {173sessionTitle = historicalTitle;174} else if (importedTitle) {175sessionTitle = localize('chatDebug.importedSession', "Imported: {0}", importedTitle);176} else if (LocalChatSessionUri.isLocalSession(sessionResource)) {177sessionTitle = localize('chatDebug.newSession', "New Chat");178} else if (getChatSessionType(sessionResource) === 'copilotcli') {179const pathId = sessionResource.path.replace(/^\//, '').split('-')[0];180const shortId = pathId || sessionResource.authority || sessionResource.toString();181sessionTitle = localize('chatDebug.copilotCliSessionWithId', "Copilot CLI: {0}", shortId);182} else if (getChatSessionType(sessionResource) === 'claude-code') {183const pathId = sessionResource.path.replace(/^\//, '').split('-')[0];184const shortId = pathId || sessionResource.authority || sessionResource.toString();185sessionTitle = localize('chatDebug.claudeCodeSessionWithId', "Claude Code: {0}", shortId);186} else {187sessionTitle = localize('chatDebug.newSession', "New Chat");188}189const isActive = activeSessionResource !== undefined && sessionResource.toString() === activeSessionResource.toString();190191const item = DOM.append(sessionList, $<HTMLButtonElement>('button.chat-debug-home-session-item'));192item.setAttribute('role', 'listitem');193if (isActive) {194item.classList.add('chat-debug-home-session-item-active');195item.setAttribute('aria-current', 'true');196}197198DOM.append(item, $(`span${ThemeIcon.asCSSSelector(Codicon.comment)}`));199200const titleSpan = DOM.append(item, $('span.chat-debug-home-session-item-title'));201titleSpan.textContent = sessionTitle;202const ariaLabel = isActive203? localize('chatDebug.sessionItemActive', "{0} (active)", sessionTitle)204: sessionTitle;205item.setAttribute('aria-label', ariaLabel);206207if (isActive) {208DOM.append(item, $('span.chat-debug-home-session-badge', undefined, localize('chatDebug.active', "Active")));209}210211this.renderDisposables.add(DOM.addDisposableListener(item, DOM.EventType.CLICK, () => {212this._lastOpenedSessionResource = sessionResource;213this._onNavigateToSession.fire(sessionResource);214}));215items.push(item);216}217218// "Show More" button when there are more sessions to display219if (sessionResources.length > this._visibleCount) {220const remaining = sessionResources.length - this._visibleCount;221const showMoreButton = this.renderDisposables.add(new Button(this.scrollContent, { ...defaultButtonStyles, secondary: true }));222showMoreButton.element.classList.add('chat-debug-home-show-more');223showMoreButton.label = localize('chatDebug.showMore', "Show More ({0})", remaining);224this.renderDisposables.add(showMoreButton.onDidClick(() => {225this._visibleCount += PAGE_SIZE;226this.render();227}));228}229230// Arrow key navigation between session items231this.renderDisposables.add(DOM.addDisposableListener(sessionList, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {232if (items.length === 0) {233return;234}235const focused = DOM.getActiveElement() as HTMLElement;236const idx = items.indexOf(focused as HTMLButtonElement);237if (idx === -1) {238return;239}240let nextIdx: number | undefined;241switch (e.key) {242case 'ArrowDown':243nextIdx = idx + 1 < items.length ? idx + 1 : idx;244break;245case 'ArrowUp':246nextIdx = idx - 1 >= 0 ? idx - 1 : idx;247break;248case 'Home':249nextIdx = 0;250break;251case 'End':252nextIdx = items.length - 1;253break;254}255if (nextIdx !== undefined) {256e.preventDefault();257items[nextIdx].focus();258}259}));260}261}262}263264265