Path: blob/main/src/vs/workbench/contrib/chat/browser/chatDebug/chatDebugOverviewView.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 { BreadcrumbsWidget } from '../../../../../base/browser/ui/breadcrumbs/breadcrumbsWidget.js';7import { Button } from '../../../../../base/browser/ui/button/button.js';8import { RunOnceScheduler } from '../../../../../base/common/async.js';9import { Codicon } from '../../../../../base/common/codicons.js';10import { Emitter } from '../../../../../base/common/event.js';11import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';12import { ThemeIcon } from '../../../../../base/common/themables.js';13import { URI } from '../../../../../base/common/uri.js';14import { localize } from '../../../../../nls.js';15import { defaultBreadcrumbsWidgetStyles, defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';16import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugService } from '../../common/chatDebugService.js';17import { safeIntl } from '../../../../../base/common/date.js';18import { IChatService } from '../../common/chatService/chatService.js';19import { ChatAgentLocation } from '../../common/constants.js';20import { IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js';21import { getChatSessionType, LocalChatSessionUri } from '../../common/model/chatUri.js';22import { IChatWidgetService } from '../chat.js';23import { setupBreadcrumbKeyboardNavigation, TextBreadcrumbItem } from './chatDebugTypes.js';2425const $ = DOM.$;26const numberFormatter = safeIntl.NumberFormat();2728export const enum OverviewNavigation {29Home = 'home',30Logs = 'logs',31FlowChart = 'flowchart',32}3334export class ChatDebugOverviewView extends Disposable {3536private readonly _onNavigate = this._register(new Emitter<OverviewNavigation>());37readonly onNavigate = this._onNavigate.event;3839readonly container: HTMLElement;40private readonly content: HTMLElement;41private readonly breadcrumbWidget: BreadcrumbsWidget;42private readonly loadDisposables = this._register(new DisposableStore());4344private currentSessionResource: URI | undefined;45private metricsContainer: HTMLElement | undefined;46private isFirstLoad: boolean = true;47private readonly refreshScheduler: RunOnceScheduler;4849constructor(50parent: HTMLElement,51@IChatService private readonly chatService: IChatService,52@IChatDebugService private readonly chatDebugService: IChatDebugService,53@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,54@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,55) {56super();57this.container = DOM.append(parent, $('.chat-debug-overview'));58DOM.hide(this.container);5960this.refreshScheduler = this._register(new RunOnceScheduler(() => this.doRefresh(), 100));6162// Breadcrumb63const breadcrumbContainer = DOM.append(this.container, $('.chat-debug-breadcrumb'));64this.breadcrumbWidget = this._register(new BreadcrumbsWidget(breadcrumbContainer, 3, undefined, Codicon.chevronRight, defaultBreadcrumbsWidgetStyles));65this._register(setupBreadcrumbKeyboardNavigation(breadcrumbContainer, this.breadcrumbWidget));66this._register(this.breadcrumbWidget.onDidSelectItem(e => {67if (e.type === 'select' && e.item instanceof TextBreadcrumbItem) {68this.breadcrumbWidget.setSelection(undefined);69const items = this.breadcrumbWidget.getItems();70const idx = items.indexOf(e.item);71if (idx === 0) {72this._onNavigate.fire(OverviewNavigation.Home);73}74}75}));7677this.content = DOM.append(this.container, $('.chat-debug-overview-content'));78}7980setSession(sessionResource: URI): void {81this.currentSessionResource = sessionResource;82this.isFirstLoad = true;83}8485show(): void {86DOM.show(this.container);87this.load();88}8990hide(): void {91DOM.hide(this.container);92this.refreshScheduler.cancel();93}9495refresh(): void {96if (this.container.style.display !== 'none') {97if (!this.refreshScheduler.isScheduled()) {98this.refreshScheduler.schedule();99}100}101}102103private doRefresh(): void {104// On refresh, only update the metrics section in-place105if (this.metricsContainer && this.currentSessionResource) {106DOM.clearNode(this.metricsContainer);107const events = this.chatDebugService.getEvents(this.currentSessionResource);108this.renderMetricsContent(this.metricsContainer, events);109this.isFirstLoad = false;110} else {111this.load();112}113}114115updateBreadcrumb(): void {116if (!this.currentSessionResource) {117return;118}119const sessionTitle = this.chatService.getSessionTitle(this.currentSessionResource) || LocalChatSessionUri.parseLocalSessionId(this.currentSessionResource) || this.currentSessionResource.toString();120this.breadcrumbWidget.setItems([121new TextBreadcrumbItem(localize('chatDebug.title', "Agent Debug Logs"), true),122new TextBreadcrumbItem(sessionTitle),123]);124}125126private load(): void {127DOM.clearNode(this.content);128this.loadDisposables.clear();129this.updateBreadcrumb();130131if (!this.currentSessionResource) {132return;133}134135const sessionTitle = this.chatService.getSessionTitle(this.currentSessionResource) || LocalChatSessionUri.parseLocalSessionId(this.currentSessionResource) || this.currentSessionResource.toString();136137const titleRow = DOM.append(this.content, $('.chat-debug-overview-title-row'));138const titleEl = DOM.append(titleRow, $('h2.chat-debug-overview-title'));139DOM.append(titleEl, $(`span${ThemeIcon.asCSSSelector(Codicon.comment)}`));140titleEl.append(sessionTitle);141142const titleActions = DOM.append(titleRow, $('.chat-debug-overview-title-actions'));143144const revealSessionBtn = this.loadDisposables.add(new Button(titleActions, { ariaLabel: localize('chatDebug.revealChatSession', "Reveal Chat Session"), title: localize('chatDebug.revealChatSession', "Reveal Chat Session") }));145revealSessionBtn.element.classList.add('chat-debug-icon-button');146revealSessionBtn.icon = Codicon.goToFile;147this.loadDisposables.add(revealSessionBtn.onDidClick(() => {148if (this.currentSessionResource) {149this.chatWidgetService.openSession(this.currentSessionResource);150}151}));152153// Session details section154this.renderSessionDetails(this.currentSessionResource);155156// Derived overview metrics — show shimmer only on the very first load157// AND when there are no events yet. If events were already streamed158// (e.g. while viewing logs), render them immediately so the shimmer159// doesn't get stuck forever waiting for an event that already fired.160const events = this.chatDebugService.getEvents(this.currentSessionResource);161this.renderDerivedOverview(events, this.isFirstLoad && events.length === 0);162this.isFirstLoad = false;163}164165private renderSessionDetails(sessionUri: URI): void {166const model = this.chatService.getSession(sessionUri);167168interface DetailItem { label: string; value: string }169const details: DetailItem[] = [];170171// Session type172const sessionType = getChatSessionType(sessionUri);173const contribution = this.chatSessionsService.getChatSessionContribution(sessionType);174const sessionTypeName = contribution?.displayName || (sessionType === localChatSessionType175? localize('chatDebug.sessionType.local', "Local")176: sessionType);177details.push({ label: localize('chatDebug.detail.sessionType', "Session Type"), value: sessionTypeName });178179if (model) {180const locationLabel = this.getLocationLabel(model.initialLocation);181details.push({ label: localize('chatDebug.detail.location', "Location"), value: locationLabel });182183const inProgress = model.requestInProgress.get();184const statusLabel = inProgress185? localize('chatDebug.status.inProgress', "In Progress")186: localize('chatDebug.status.idle', "Idle");187details.push({ label: localize('chatDebug.detail.status', "Status"), value: statusLabel });188189const timing = model.timing;190details.push({ label: localize('chatDebug.detail.created', "Created"), value: new Date(timing.created).toLocaleString() });191192if (timing.lastRequestEnded) {193details.push({ label: localize('chatDebug.detail.lastActivity', "Last Activity"), value: new Date(timing.lastRequestEnded).toLocaleString() });194} else if (timing.lastRequestStarted) {195details.push({ label: localize('chatDebug.detail.lastActivity', "Last Activity"), value: new Date(timing.lastRequestStarted).toLocaleString() });196}197}198199if (details.length > 0) {200const section = DOM.append(this.content, $('.chat-debug-overview-section'));201DOM.append(section, $('h3.chat-debug-overview-section-label', undefined, localize('chatDebug.sessionDetails', "Session Details")));202203const detailsGrid = DOM.append(section, $('.chat-debug-overview-details'));204for (const detail of details) {205const row = DOM.append(detailsGrid, $('.chat-debug-overview-detail-row'));206DOM.append(row, $('span.chat-debug-overview-detail-label', undefined, detail.label));207DOM.append(row, $('span.chat-debug-overview-detail-value', undefined, detail.value));208}209}210}211212private getLocationLabel(location: ChatAgentLocation): string {213switch (location) {214case ChatAgentLocation.Chat: return localize('chatDebug.location.chat', "Chat Panel");215case ChatAgentLocation.Terminal: return localize('chatDebug.location.terminal', "Terminal");216case ChatAgentLocation.Notebook: return localize('chatDebug.location.notebook', "Notebook");217case ChatAgentLocation.EditorInline: return localize('chatDebug.location.editor', "Editor Inline");218default: return String(location);219}220}221222private renderDerivedOverview(events: readonly IChatDebugEvent[], showShimmer: boolean): void {223const metricsSection = DOM.append(this.content, $('.chat-debug-overview-section'));224DOM.append(metricsSection, $('h3.chat-debug-overview-section-label', undefined, localize('chatDebug.summary', "Summary")));225226this.metricsContainer = DOM.append(metricsSection, $('.chat-debug-overview-metrics'));227228if (showShimmer) {229this.renderMetricsShimmer(this.metricsContainer);230} else {231this.renderMetricsContent(this.metricsContainer, events);232}233234// Explore actions235const actionsSection = DOM.append(this.content, $('.chat-debug-overview-section'));236DOM.append(actionsSection, $('h3.chat-debug-overview-section-label', undefined, localize('chatDebug.exploreTraceData', "Explore Trace Data")));237238const row = DOM.append(actionsSection, $('.chat-debug-overview-actions'));239240const viewLogsBtn = this.loadDisposables.add(new Button(row, { ...defaultButtonStyles, secondary: true, supportIcons: true, title: localize('chatDebug.viewLogs', "View Logs") }));241viewLogsBtn.element.classList.add('chat-debug-overview-action-button');242viewLogsBtn.label = `$(list-flat) ${localize('chatDebug.viewLogs', "View Logs")}`;243this.loadDisposables.add(viewLogsBtn.onDidClick(() => {244this._onNavigate.fire(OverviewNavigation.Logs);245}));246247const flowChartBtn = this.loadDisposables.add(new Button(row, { ...defaultButtonStyles, secondary: true, supportIcons: true, title: localize('chatDebug.agentFlowChart', "Agent Flow Chart") }));248flowChartBtn.element.classList.add('chat-debug-overview-action-button');249flowChartBtn.label = `$(type-hierarchy) ${localize('chatDebug.agentFlowChart', "Agent Flow Chart")}`;250this.loadDisposables.add(flowChartBtn.onDidClick(() => {251this._onNavigate.fire(OverviewNavigation.FlowChart);252}));253254}255256private renderMetricsShimmer(container: HTMLElement): void {257// Show placeholder shimmer cards while provider data is loading258const placeholderLabels = [259localize('chatDebug.metric.modelTurns', "Model Turns"),260localize('chatDebug.metric.toolCalls', "Tool Calls"),261localize('chatDebug.metric.totalInputTokens', "Total Input Tokens"),262localize('chatDebug.metric.totalOutputTokens', "Total Output Tokens"),263localize('chatDebug.metric.totalCachedInputTokens', "Total Cached Input Tokens"),264localize('chatDebug.metric.totalTokens', "Total Tokens"),265localize('chatDebug.metric.errors', "Errors"),266];267for (const label of placeholderLabels) {268const card = DOM.append(container, $('.chat-debug-overview-metric-card'));269DOM.append(card, $('div.chat-debug-overview-metric-label', undefined, label));270const valueEl = DOM.append(card, $('div.chat-debug-overview-metric-value'));271const shimmer = DOM.append(valueEl, $('span.chat-debug-overview-metric-shimmer'));272shimmer.textContent = '\u00A0'; // non-breaking space for height273}274}275276private renderMetricsContent(container: HTMLElement, events: readonly IChatDebugEvent[]): void {277const modelTurns = events.filter(e => e.kind === 'modelTurn');278const toolCalls = events.filter(e => e.kind === 'toolCall');279const errors = events.filter(e =>280(e.kind === 'generic' && e.level === ChatDebugLogLevel.Error) ||281(e.kind === 'toolCall' && e.result === 'error')282);283284const fmt = numberFormatter.value;285const totalInputTokens = modelTurns.reduce((sum, e) => sum + (e.inputTokens ?? 0), 0);286const totalOutputTokens = modelTurns.reduce((sum, e) => sum + (e.outputTokens ?? 0), 0);287const totalCachedTokens = modelTurns.reduce((sum, e) => sum + (e.cachedTokens ?? 0), 0);288const totalTokens = modelTurns.reduce((sum, e) => sum + (e.totalTokens ?? 0), 0);289290interface OverviewMetric { label: string; value: string }291const metrics: OverviewMetric[] = [292{ label: localize('chatDebug.metric.modelTurns', "Model Turns"), value: fmt.format(modelTurns.length) },293{ label: localize('chatDebug.metric.toolCalls', "Tool Calls"), value: fmt.format(toolCalls.length) },294{ label: localize('chatDebug.metric.totalInputTokens', "Total Input Tokens"), value: fmt.format(totalInputTokens) },295{ label: localize('chatDebug.metric.totalOutputTokens', "Total Output Tokens"), value: fmt.format(totalOutputTokens) },296{ label: localize('chatDebug.metric.totalCachedInputTokens', "Total Cached Input Tokens"), value: fmt.format(totalCachedTokens) },297{ label: localize('chatDebug.metric.totalTokens', "Total Tokens"), value: fmt.format(totalTokens) },298{ label: localize('chatDebug.metric.errors', "Errors"), value: fmt.format(errors.length) },299];300301for (const metric of metrics) {302const card = DOM.append(container, $('.chat-debug-overview-metric-card'));303DOM.append(card, $('div.chat-debug-overview-metric-label', undefined, metric.label));304DOM.append(card, $('div.chat-debug-overview-metric-value', undefined, metric.value));305}306}307}308309310