Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import * as DOM from '../../../../../base/browser/dom.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { IChatDebugMessageSection } from '../../common/chatDebugService.js';

const $ = DOM.$;

/**
* Wire up a collapsible toggle on a chevron+header+content triple.
Expand Down Expand Up @@ -47,25 +44,3 @@ export function setupCollapsibleToggle(chevron: HTMLElement, header: HTMLElement
}
}));
}

/**
* Render a collapsible section with a clickable header and pre-formatted content
* wrapped in a scrollable element.
*/
export function renderCollapsibleSection(parent: HTMLElement, section: IChatDebugMessageSection, disposables: DisposableStore, initiallyCollapsed: boolean = false): void {
const sectionEl = DOM.append(parent, $('div.chat-debug-message-section'));

const header = DOM.append(sectionEl, $('div.chat-debug-message-section-header'));

const chevron = DOM.append(header, $(`span.chat-debug-message-section-chevron`));
DOM.append(header, $('span.chat-debug-message-section-title', undefined, section.name));

const contentEl = $('pre.chat-debug-message-section-content');
contentEl.textContent = section.content;
contentEl.tabIndex = 0;

const wrapper = DOM.append(sectionEl, $('div.chat-debug-message-section-content-wrapper'));
wrapper.appendChild(contentEl);

setupCollapsibleToggle(chevron, header, wrapper, disposables, initiallyCollapsed);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

import * as DOM from '../../../../../base/browser/dom.js';
import { Button } from '../../../../../base/browser/ui/button/button.js';
import { Orientation, Sash, SashState } from '../../../../../base/browser/ui/sash/sash.js';
import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { ScrollbarVisibility } from '../../../../../base/common/scrollable.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { Emitter } from '../../../../../base/common/event.js';
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
Expand All @@ -27,6 +30,10 @@ import { renderModelTurnContent, modelTurnContentToPlainText } from './chatDebug

const $ = DOM.$;

const DETAIL_PANEL_DEFAULT_WIDTH = 350;
const DETAIL_PANEL_MIN_WIDTH = 200;
const DETAIL_PANEL_MAX_WIDTH = 800;

/**
* Reusable detail panel that resolves and displays the content of a
* single {@link IChatDebugEvent}. Used by both the logs view and the
Expand All @@ -37,12 +44,23 @@ export class ChatDebugDetailPanel extends Disposable {
private readonly _onDidHide = this._register(new Emitter<void>());
readonly onDidHide = this._onDidHide.event;

private readonly _onDidChangeWidth = this._register(new Emitter<number>());
readonly onDidChangeWidth = this._onDidChangeWidth.event;

readonly element: HTMLElement;
private readonly contentContainer: HTMLElement;
private readonly scrollable: DomScrollableElement;
private readonly sash: Sash;
private headerElement: HTMLElement | undefined;
private readonly detailDisposables = this._register(new DisposableStore());
private currentDetailText: string = '';
private currentDetailEventId: string | undefined;
private firstFocusableElement: HTMLElement | undefined;
private _width: number = DETAIL_PANEL_DEFAULT_WIDTH;

get width(): number {
return this._width;
}

constructor(
parent: HTMLElement,
Expand All @@ -52,12 +70,43 @@ export class ChatDebugDetailPanel extends Disposable {
@IClipboardService private readonly clipboardService: IClipboardService,
@IHoverService private readonly hoverService: IHoverService,
@IOpenerService private readonly openerService: IOpenerService,
@ILanguageService private readonly languageService: ILanguageService,
) {
super();
this.element = DOM.append(parent, $('.chat-debug-detail-panel'));
this.contentContainer = $('.chat-debug-detail-content');
this.scrollable = this._register(new DomScrollableElement(this.contentContainer, {
horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.Auto,
}));
this.element.style.width = `${this._width}px`;
DOM.hide(this.element);

// Sash on the parent container, positioned at the left edge of the detail panel
this.sash = this._register(new Sash(parent, {
getVerticalSashLeft: () => parent.offsetWidth - this._width,
}, { orientation: Orientation.VERTICAL }));
this.sash.state = SashState.Disabled;

let sashStartWidth: number | undefined;
this._register(this.sash.onDidStart(() => sashStartWidth = this._width));
this._register(this.sash.onDidEnd(() => {
sashStartWidth = undefined;
this.sash.layout();
}));
this._register(this.sash.onDidChange(e => {
if (sashStartWidth === undefined) {
return;
}
// Dragging left (negative currentX delta) should increase width
const delta = e.startX - e.currentX;
const newWidth = Math.max(DETAIL_PANEL_MIN_WIDTH, Math.min(DETAIL_PANEL_MAX_WIDTH, sashStartWidth + delta));
this._width = newWidth;
this.element.style.width = `${newWidth}px`;
this.sash.layout();
this._onDidChangeWidth.fire(newWidth);
}));

// Handle Ctrl+A / Cmd+A to select all within the detail panel
this._register(DOM.addDisposableListener(this.element, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
Expand Down Expand Up @@ -87,13 +136,16 @@ export class ChatDebugDetailPanel extends Disposable {
const resolved = event.id ? await this.chatDebugService.resolveEvent(event.id) : undefined;

DOM.show(this.element);
this.sash.state = SashState.Enabled;
this.sash.layout();
DOM.clearNode(this.element);
DOM.clearNode(this.contentContainer);
this.detailDisposables.clear();

// Header with action buttons
const header = DOM.append(this.element, $('.chat-debug-detail-header'));
this.element.appendChild(this.contentContainer);
this.headerElement = header;
this.element.appendChild(this.scrollable.getDomNode());

const fullScreenButton = this.detailDisposables.add(new Button(header, { ariaLabel: localize('chatDebug.openInEditor', "Open in Editor"), title: localize('chatDebug.openInEditor', "Open in Editor") }));
fullScreenButton.element.classList.add('chat-debug-detail-button');
Expand All @@ -120,14 +172,13 @@ export class ChatDebugDetailPanel extends Disposable {
if (resolved && resolved.kind === 'fileList') {
this.currentDetailText = fileListToPlainText(resolved);
const { element: contentEl, disposables: contentDisposables } = this.instantiationService.invokeFunction(accessor =>
renderCustomizationDiscoveryContent(resolved, this.openerService, accessor.get(IModelService), accessor.get(ILanguageService), this.hoverService, accessor.get(ILabelService))
renderCustomizationDiscoveryContent(resolved, this.openerService, accessor.get(IModelService), this.languageService, this.hoverService, accessor.get(ILabelService))
);
this.detailDisposables.add(contentDisposables);
this.contentContainer.appendChild(contentEl);
} else if (resolved && resolved.kind === 'toolCall') {
this.currentDetailText = toolCallContentToPlainText(resolved);
const languageService = this.instantiationService.invokeFunction(accessor => accessor.get(ILanguageService));
const { element: contentEl, disposables: contentDisposables } = await renderToolCallContent(resolved, languageService);
const { element: contentEl, disposables: contentDisposables } = await renderToolCallContent(resolved, this.languageService, this.clipboardService);
if (this.currentDetailEventId !== event.id) {
// Another event was selected while we were rendering
contentDisposables.dispose();
Expand All @@ -137,22 +188,39 @@ export class ChatDebugDetailPanel extends Disposable {
this.contentContainer.appendChild(contentEl);
} else if (resolved && resolved.kind === 'message') {
this.currentDetailText = resolvedMessageToPlainText(resolved);
const { element: contentEl, disposables: contentDisposables } = renderResolvedMessageContent(resolved);
const { element: contentEl, disposables: contentDisposables } = await renderResolvedMessageContent(resolved, this.languageService, this.clipboardService);
if (this.currentDetailEventId !== event.id) {
contentDisposables.dispose();
return;
}
this.detailDisposables.add(contentDisposables);
this.contentContainer.appendChild(contentEl);
} else if (resolved && resolved.kind === 'modelTurn') {
this.currentDetailText = modelTurnContentToPlainText(resolved);
const { element: contentEl, disposables: contentDisposables } = renderModelTurnContent(resolved);
const { element: contentEl, disposables: contentDisposables } = await renderModelTurnContent(resolved, this.languageService, this.clipboardService);
if (this.currentDetailEventId !== event.id) {
// Another event was selected while we were rendering
contentDisposables.dispose();
return;
}
this.detailDisposables.add(contentDisposables);
this.contentContainer.appendChild(contentEl);
} else if (event.kind === 'userMessage') {
this.currentDetailText = messageEventToPlainText(event);
const { element: contentEl, disposables: contentDisposables } = renderUserMessageContent(event);
const { element: contentEl, disposables: contentDisposables } = await renderUserMessageContent(event, this.languageService, this.clipboardService);
if (this.currentDetailEventId !== event.id) {
contentDisposables.dispose();
return;
}
this.detailDisposables.add(contentDisposables);
this.contentContainer.appendChild(contentEl);
} else if (event.kind === 'agentResponse') {
this.currentDetailText = messageEventToPlainText(event);
const { element: contentEl, disposables: contentDisposables } = renderAgentResponseContent(event);
const { element: contentEl, disposables: contentDisposables } = await renderAgentResponseContent(event, this.languageService, this.clipboardService);
if (this.currentDetailEventId !== event.id) {
contentDisposables.dispose();
return;
}
this.detailDisposables.add(contentDisposables);
this.contentContainer.appendChild(contentEl);
} else {
Expand All @@ -165,6 +233,15 @@ export class ChatDebugDetailPanel extends Disposable {
}
pre.textContent = this.currentDetailText;
}

// Compute height from the parent container and set explicit
// dimensions so the scrollable element can show proper scrollbars.
const parentHeight = this.element.parentElement?.clientHeight ?? 0;
if (parentHeight > 0) {
this.layout(parentHeight);
} else {
this.scrollable.scanDomNode();
}
}

get isVisible(): boolean {
Expand All @@ -175,10 +252,29 @@ export class ChatDebugDetailPanel extends Disposable {
this.firstFocusableElement?.focus();
}

/**
* Set explicit dimensions on the scrollable element so the scrollbar
* can compute its size. Call after the panel is shown and whenever
* the available space changes.
*/
layout(height: number): void {
const headerHeight = this.headerElement?.offsetHeight ?? 0;
const scrollableHeight = Math.max(0, height - headerHeight);
this.contentContainer.style.height = `${scrollableHeight}px`;
this.scrollable.scanDomNode();
this.sash.layout();
}

layoutSash(): void {
this.sash.layout();
}

hide(): void {
this.currentDetailEventId = undefined;
this.firstFocusableElement = undefined;
this.headerElement = undefined;
DOM.hide(this.element);
this.sash.state = SashState.Disabled;
DOM.clearNode(this.element);
DOM.clearNode(this.contentContainer);
this.detailDisposables.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,6 @@ export class ChatDebugEditor extends EditorPane {
}));

this._register(this.chatService.onDidCreateModel(model => {
if (this.viewState === ViewState.Home && this.configurationService.getValue<boolean>(AGENT_DEBUG_LOG_ENABLED_SETTING)) {
// Auto-navigate to the new session when the Agent Debug Logs is
// already open on the home view. This avoids the user having to
// wait for the title to resolve and manually clicking the session.
this.navigateToSession(model.sessionResource);
}

// Track title changes per model, disposing the previous listener
// for the same model URI to avoid leaks.
const key = model.sessionResource.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

import { localize } from '../../../../../nls.js';
import { IChatDebugEvent } from '../../common/chatDebugService.js';
import { safeIntl } from '../../../../../base/common/date.js';

const numberFormatter = safeIntl.NumberFormat();

/**
* Format the detail text for a debug event (used when no resolved content is available).
Expand All @@ -15,17 +18,17 @@ export function formatEventDetail(event: IChatDebugEvent): string {
const parts = [localize('chatDebug.detail.tool', "Tool: {0}", event.toolName)];
if (event.toolCallId) { parts.push(localize('chatDebug.detail.callId', "Call ID: {0}", event.toolCallId)); }
if (event.result) { parts.push(localize('chatDebug.detail.result', "Result: {0}", event.result)); }
if (event.durationInMillis !== undefined) { parts.push(localize('chatDebug.detail.durationMs', "Duration: {0}ms", event.durationInMillis)); }
if (event.durationInMillis !== undefined) { parts.push(localize('chatDebug.detail.durationMs', "Duration: {0}ms", numberFormatter.value.format(event.durationInMillis))); }
if (event.input) { parts.push(`\n${localize('chatDebug.detail.input', "Input:")}\n${event.input}`); }
if (event.output) { parts.push(`\n${localize('chatDebug.detail.output', "Output:")}\n${event.output}`); }
return parts.join('\n');
}
case 'modelTurn': {
const parts = [event.model ?? localize('chatDebug.detail.modelTurn', "Model Turn")];
if (event.inputTokens !== undefined) { parts.push(localize('chatDebug.detail.inputTokens', "Input tokens: {0}", event.inputTokens)); }
if (event.outputTokens !== undefined) { parts.push(localize('chatDebug.detail.outputTokens', "Output tokens: {0}", event.outputTokens)); }
if (event.totalTokens !== undefined) { parts.push(localize('chatDebug.detail.totalTokens', "Total tokens: {0}", event.totalTokens)); }
if (event.durationInMillis !== undefined) { parts.push(localize('chatDebug.detail.durationMs', "Duration: {0}ms", event.durationInMillis)); }
if (event.inputTokens !== undefined) { parts.push(localize('chatDebug.detail.inputTokens', "Input tokens: {0}", numberFormatter.value.format(event.inputTokens))); }
if (event.outputTokens !== undefined) { parts.push(localize('chatDebug.detail.outputTokens', "Output tokens: {0}", numberFormatter.value.format(event.outputTokens))); }
if (event.totalTokens !== undefined) { parts.push(localize('chatDebug.detail.totalTokens', "Total tokens: {0}", numberFormatter.value.format(event.totalTokens))); }
if (event.durationInMillis !== undefined) { parts.push(localize('chatDebug.detail.durationMs', "Duration: {0}ms", numberFormatter.value.format(event.durationInMillis))); }
return parts.join('\n');
}
case 'generic':
Expand All @@ -34,9 +37,9 @@ export function formatEventDetail(event: IChatDebugEvent): string {
const parts = [localize('chatDebug.detail.agent', "Agent: {0}", event.agentName)];
if (event.description) { parts.push(localize('chatDebug.detail.description', "Description: {0}", event.description)); }
if (event.status) { parts.push(localize('chatDebug.detail.status', "Status: {0}", event.status)); }
if (event.durationInMillis !== undefined) { parts.push(localize('chatDebug.detail.durationMs', "Duration: {0}ms", event.durationInMillis)); }
if (event.toolCallCount !== undefined) { parts.push(localize('chatDebug.detail.toolCallCount', "Tool calls: {0}", event.toolCallCount)); }
if (event.modelTurnCount !== undefined) { parts.push(localize('chatDebug.detail.modelTurnCount', "Model turns: {0}", event.modelTurnCount)); }
if (event.durationInMillis !== undefined) { parts.push(localize('chatDebug.detail.durationMs', "Duration: {0}ms", numberFormatter.value.format(event.durationInMillis))); }
if (event.toolCallCount !== undefined) { parts.push(localize('chatDebug.detail.toolCallCount', "Tool calls: {0}", numberFormatter.value.format(event.toolCallCount))); }
if (event.modelTurnCount !== undefined) { parts.push(localize('chatDebug.detail.modelTurnCount', "Model turns: {0}", numberFormatter.value.format(event.modelTurnCount))); }
return parts.join('\n');
}
case 'userMessage': {
Expand Down
Loading
Loading