Path: blob/main/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts
3296 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 { allowedMarkdownHtmlAttributes, MarkdownRendererMarkedOptions, type MarkdownRenderOptions } from '../../../../../base/browser/markdownRenderer.js';7import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';8import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';9import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js';10import { coalesce } from '../../../../../base/common/arrays.js';11import { findLast } from '../../../../../base/common/arraysFind.js';12import { Codicon } from '../../../../../base/common/codicons.js';13import { Emitter } from '../../../../../base/common/event.js';14import { Lazy } from '../../../../../base/common/lazy.js';15import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';16import { autorun, IObservable } from '../../../../../base/common/observable.js';17import { ScrollbarVisibility } from '../../../../../base/common/scrollable.js';18import { equalsIgnoreCase } from '../../../../../base/common/strings.js';19import { ThemeIcon } from '../../../../../base/common/themables.js';20import { URI } from '../../../../../base/common/uri.js';21import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';22import { Range } from '../../../../../editor/common/core/range.js';23import { ILanguageService } from '../../../../../editor/common/languages/language.js';24import { ITextModel } from '../../../../../editor/common/model.js';25import { getIconClasses } from '../../../../../editor/common/services/getIconClasses.js';26import { IModelService } from '../../../../../editor/common/services/model.js';27import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';28import { localize } from '../../../../../nls.js';29import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';30import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';31import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';32import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';33import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';34import { FileKind } from '../../../../../platform/files/common/files.js';35import { IHoverService } from '../../../../../platform/hover/browser/hover.js';36import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';37import { ILabelService } from '../../../../../platform/label/common/label.js';38import { IEditorService } from '../../../../services/editor/common/editorService.js';39import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';40import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js';41import { MarkedKatexSupport } from '../../../markdown/browser/markedKatexSupport.js';42import { IMarkdownVulnerability } from '../../common/annotations.js';43import { IEditSessionEntryDiff } from '../../common/chatEditingService.js';44import { IChatProgressRenderableResponseContent } from '../../common/chatModel.js';45import { IChatMarkdownContent, IChatService, IChatUndoStop } from '../../common/chatService.js';46import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js';47import { CodeBlockEntry, CodeBlockModelCollection } from '../../common/codeBlockModelCollection.js';48import { ChatConfiguration } from '../../common/constants.js';49import { IChatCodeBlockInfo } from '../chat.js';50import { IChatRendererDelegate } from '../chatListRenderer.js';51import { ChatMarkdownDecorationsRenderer } from '../chatMarkdownDecorationsRenderer.js';52import { allowedChatMarkdownHtmlTags } from '../chatMarkdownRenderer.js';53import { ChatEditorOptions } from '../chatOptions.js';54import { CodeBlockPart, ICodeBlockData, ICodeBlockRenderOptions, localFileLanguageId, parseLocalFileData } from '../codeBlockPart.js';55import '../media/chatCodeBlockPill.css';56import { IDisposableReference, ResourcePool } from './chatCollections.js';57import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';58import { ChatExtensionsContentPart } from './chatExtensionsContentPart.js';59import './media/chatMarkdownPart.css';6061const $ = dom.$;6263export interface IChatMarkdownContentPartOptions {64readonly codeBlockRenderOptions?: ICodeBlockRenderOptions;65}6667export class ChatMarkdownContentPart extends Disposable implements IChatContentPart {68private static idPool = 0;6970public readonly codeblocksPartId = String(++ChatMarkdownContentPart.idPool);71public readonly domNode: HTMLElement;72private readonly allRefs: IDisposableReference<CodeBlockPart | CollapsedCodeBlock>[] = [];7374private readonly _onDidChangeHeight = this._register(new Emitter<void>());75public readonly onDidChangeHeight = this._onDidChangeHeight.event;7677public readonly codeblocks: IChatCodeBlockInfo[] = [];7879private readonly mathLayoutParticipants = new Set<() => void>();8081private _isDisposed = false;8283constructor(84private readonly markdown: IChatMarkdownContent,85context: IChatContentPartRenderContext,86private readonly editorPool: EditorPool,87fillInIncompleteTokens = false,88codeBlockStartIndex = 0,89renderer: MarkdownRenderer,90markdownRenderOptions: MarkdownRenderOptions | undefined,91currentWidth: number,92private readonly codeBlockModelCollection: CodeBlockModelCollection,93private readonly rendererOptions: IChatMarkdownContentPartOptions,94@IContextKeyService contextKeyService: IContextKeyService,95@IConfigurationService configurationService: IConfigurationService,96@ITextModelService private readonly textModelService: ITextModelService,97@IInstantiationService private readonly instantiationService: IInstantiationService,98@IAiEditTelemetryService private readonly aiEditTelemetryService: IAiEditTelemetryService,99) {100super();101102const element = context.element;103const inUndoStop = (findLast(context.content, e => e.kind === 'undoStop', context.contentIndex) as IChatUndoStop | undefined)?.id;104105// We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering106const orderedDisposablesList: IDisposable[] = [];107108// Need to track the index of the codeblock within the response so it can have a unique ID,109// and within this part to find it within the codeblocks array110let globalCodeBlockIndexStart = codeBlockStartIndex;111let thisPartCodeBlockIndexStart = 0;112113this.domNode = $('div.chat-markdown-part');114115const enableMath = configurationService.getValue<boolean>(ChatConfiguration.EnableMath);116117const doRenderMarkdown = () => {118if (this._isDisposed) {119return;120}121122// TODO: Move katex support into chatMarkdownRenderer123const markedExtensions = enableMath124? coalesce([MarkedKatexSupport.getExtension(dom.getWindow(context.container), {125throwOnError: false126})])127: [];128129// Don't set to 'false' for responses, respect defaults130const markedOpts: MarkdownRendererMarkedOptions = isRequestVM(element) ? {131gfm: true,132breaks: true,133} : {};134135const result = this._register(renderer.render(markdown.content, {136sanitizerConfig: MarkedKatexSupport.getSanitizerOptions({137allowedTags: allowedChatMarkdownHtmlTags,138allowedAttributes: allowedMarkdownHtmlAttributes,139}),140fillInIncompleteTokens,141codeBlockRendererSync: (languageId, text, raw) => {142const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || codeblockHasClosingBackticks(raw);143if ((!text || (text.startsWith('<vscode_codeblock_uri') && !text.includes('\n'))) && !isCodeBlockComplete) {144const hideEmptyCodeblock = $('div');145hideEmptyCodeblock.style.display = 'none';146return hideEmptyCodeblock;147}148if (languageId === 'vscode-extensions') {149const chatExtensions = this._register(instantiationService.createInstance(ChatExtensionsContentPart, { kind: 'extensions', extensions: text.split(',') }));150this._register(chatExtensions.onDidChangeHeight(() => this._onDidChangeHeight.fire()));151return chatExtensions.domNode;152}153const globalIndex = globalCodeBlockIndexStart++;154const thisPartIndex = thisPartCodeBlockIndexStart++;155let textModel: Promise<ITextModel>;156let range: Range | undefined;157let vulns: readonly IMarkdownVulnerability[] | undefined;158let codeblockEntry: CodeBlockEntry | undefined;159if (equalsIgnoreCase(languageId, localFileLanguageId)) {160try {161const parsedBody = parseLocalFileData(text);162range = parsedBody.range && Range.lift(parsedBody.range);163textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object.textEditorModel);164} catch (e) {165return $('div');166}167} else {168const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : '';169const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, globalIndex);170const fastUpdateModelEntry = this.codeBlockModelCollection.updateSync(sessionId, element, globalIndex, { text, languageId, isComplete: isCodeBlockComplete });171vulns = modelEntry.vulns;172codeblockEntry = fastUpdateModelEntry;173textModel = modelEntry.model;174}175176const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered;177const renderOptions = {178...this.rendererOptions.codeBlockRenderOptions,179};180if (hideToolbar !== undefined) {181renderOptions.hideToolbar = hideToolbar;182}183const codeBlockInfo: ICodeBlockData = { languageId, textModel, codeBlockIndex: globalIndex, codeBlockPartIndex: thisPartIndex, element, range, parentContextKeyService: contextKeyService, vulns, codemapperUri: codeblockEntry?.codemapperUri, renderOptions, chatSessionId: element.sessionId };184185if (element.isCompleteAddedRequest || !codeblockEntry?.codemapperUri || !codeblockEntry.isEdit) {186const ref = this.renderCodeBlock(codeBlockInfo, text, isCodeBlockComplete, currentWidth);187this.allRefs.push(ref);188189// Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping)190// not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render)191this._register(ref.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire()));192193const ownerMarkdownPartId = this.codeblocksPartId;194const info: IChatCodeBlockInfo = new class implements IChatCodeBlockInfo {195readonly ownerMarkdownPartId = ownerMarkdownPartId;196readonly codeBlockIndex = globalIndex;197readonly elementId = element.id;198readonly isStreaming = false;199readonly chatSessionId = element.sessionId;200readonly languageId = languageId;201readonly editDeltaInfo = EditDeltaInfo.fromText(text);202codemapperUri = undefined; // will be set async203public get uri() {204// here we must do a getter because the ref.object is rendered205// async and the uri might be undefined when it's read immediately206return ref.object.uri;207}208readonly uriPromise = textModel.then(model => model.uri);209public focus() {210ref.object.focus();211}212}();213this.codeblocks.push(info);214orderedDisposablesList.push(ref);215return ref.object.element;216} else {217const requestId = isRequestVM(element) ? element.id : element.requestId;218const ref = this.renderCodeBlockPill(element.sessionId, requestId, inUndoStop, codeBlockInfo.codemapperUri, !isCodeBlockComplete);219if (isResponseVM(codeBlockInfo.element)) {220// TODO@joyceerhl: remove this code when we change the codeblockUri API to make the URI available synchronously221this.codeBlockModelCollection.update(codeBlockInfo.element.sessionId, codeBlockInfo.element, codeBlockInfo.codeBlockIndex, { text, languageId: codeBlockInfo.languageId, isComplete: isCodeBlockComplete }).then((e) => {222// Update the existing object's codemapperUri223this.codeblocks[codeBlockInfo.codeBlockPartIndex].codemapperUri = e.codemapperUri;224this._onDidChangeHeight.fire();225});226}227this.allRefs.push(ref);228const ownerMarkdownPartId = this.codeblocksPartId;229const info: IChatCodeBlockInfo = new class implements IChatCodeBlockInfo {230readonly ownerMarkdownPartId = ownerMarkdownPartId;231readonly codeBlockIndex = globalIndex;232readonly elementId = element.id;233readonly isStreaming = !isCodeBlockComplete;234readonly codemapperUri = codeblockEntry?.codemapperUri;235readonly chatSessionId = element.sessionId;236public get uri() {237return undefined;238}239readonly uriPromise = Promise.resolve(undefined);240public focus() {241return ref.object.element.focus();242}243readonly languageId = languageId;244readonly editDeltaInfo = EditDeltaInfo.fromText(text);245}();246this.codeblocks.push(info);247orderedDisposablesList.push(ref);248return ref.object.element;249}250},251asyncRenderCallback: () => this._onDidChangeHeight.fire(),252markedOptions: markedOpts,253markedExtensions,254...markdownRenderOptions,255}, this.domNode));256257// Ideally this would happen earlier, but we need to parse the markdown.258if (isResponseVM(element) && !element.model.codeBlockInfos && element.model.isComplete) {259element.model.initializeCodeBlockInfos(this.codeblocks.map(info => {260return {261suggestionId: this.aiEditTelemetryService.createSuggestionId({262presentation: 'codeBlock',263feature: 'sideBarChat',264editDeltaInfo: info.editDeltaInfo,265languageId: info.languageId,266modeId: element.model.request?.modeInfo?.modeId,267modelId: element.model.request?.modelId,268applyCodeBlockSuggestionId: undefined,269})270};271}));272}273274const markdownDecorationsRenderer = instantiationService.createInstance(ChatMarkdownDecorationsRenderer);275this._register(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(markdown, result.element));276277const layoutParticipants = new Lazy(() => {278const observer = new ResizeObserver(() => this.mathLayoutParticipants.forEach(layout => layout()));279observer.observe(this.domNode);280this._register(toDisposable(() => observer.disconnect()));281return this.mathLayoutParticipants;282});283284// Make katex blocks horizontally scrollable285for (const katexBlock of this.domNode.querySelectorAll('.katex-display')) {286if (!dom.isHTMLElement(katexBlock)) {287continue;288}289290const scrollable = new DomScrollableElement(katexBlock.cloneNode(true) as HTMLElement, {291vertical: ScrollbarVisibility.Hidden,292horizontal: ScrollbarVisibility.Auto,293});294orderedDisposablesList.push(scrollable);295katexBlock.replaceWith(scrollable.getDomNode());296297layoutParticipants.value.add(() => { scrollable.scanDomNode(); });298scrollable.scanDomNode();299}300301orderedDisposablesList.reverse().forEach(d => this._register(d));302};303304if (enableMath && !MarkedKatexSupport.getExtension(dom.getWindow(context.container))) {305// Need to load async306MarkedKatexSupport.loadExtension(dom.getWindow(context.container))307.catch(e => {308console.error('Failed to load MarkedKatexSupport extension:', e);309}).finally(() => {310doRenderMarkdown();311if (!this._isDisposed) {312this._onDidChangeHeight.fire();313}314});315} else {316doRenderMarkdown();317}318}319320override dispose(): void {321this._isDisposed = true;322super.dispose();323}324325private renderCodeBlockPill(sessionId: string, requestId: string, inUndoStop: string | undefined, codemapperUri: URI | undefined, isStreaming: boolean): IDisposableReference<CollapsedCodeBlock> {326const codeBlock = this.instantiationService.createInstance(CollapsedCodeBlock, sessionId, requestId, inUndoStop);327if (codemapperUri) {328codeBlock.render(codemapperUri, isStreaming);329}330return {331object: codeBlock,332isStale: () => false,333dispose: () => codeBlock.dispose()334};335}336337private renderCodeBlock(data: ICodeBlockData, text: string, isComplete: boolean, currentWidth: number): IDisposableReference<CodeBlockPart> {338const ref = this.editorPool.get();339const editorInfo = ref.object;340if (isResponseVM(data.element)) {341this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId, isComplete }).then((e) => {342// Update the existing object's codemapperUri343this.codeblocks[data.codeBlockPartIndex].codemapperUri = e.codemapperUri;344this._onDidChangeHeight.fire();345});346}347348editorInfo.render(data, currentWidth);349350return ref;351}352353hasSameContent(other: IChatProgressRenderableResponseContent): boolean {354return other.kind === 'markdownContent' && !!(other.content.value === this.markdown.content.value355|| this.codeblocks.at(-1)?.isStreaming && this.codeblocks.at(-1)?.codemapperUri !== undefined && other.content.value.lastIndexOf('```') === this.markdown.content.value.lastIndexOf('```'));356}357358layout(width: number): void {359this.allRefs.forEach((ref, index) => {360if (ref.object instanceof CodeBlockPart) {361ref.object.layout(width);362} else if (ref.object instanceof CollapsedCodeBlock) {363const codeblockModel = this.codeblocks[index];364if (codeblockModel.codemapperUri && ref.object.uri?.toString() !== codeblockModel.codemapperUri.toString()) {365ref.object.render(codeblockModel.codemapperUri, codeblockModel.isStreaming);366}367}368});369370this.mathLayoutParticipants.forEach(layout => layout());371}372373addDisposable(disposable: IDisposable): void {374this._register(disposable);375}376}377378export class EditorPool extends Disposable {379380private readonly _pool: ResourcePool<CodeBlockPart>;381382public inUse(): Iterable<CodeBlockPart> {383return this._pool.inUse;384}385386constructor(387options: ChatEditorOptions,388delegate: IChatRendererDelegate,389overflowWidgetsDomNode: HTMLElement | undefined,390private readonly isSimpleWidget: boolean = false,391@IInstantiationService instantiationService: IInstantiationService,392) {393super();394this._pool = this._register(new ResourcePool(() => {395return instantiationService.createInstance(CodeBlockPart, options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode, this.isSimpleWidget);396}));397}398399get(): IDisposableReference<CodeBlockPart> {400const codeBlock = this._pool.get();401let stale = false;402return {403object: codeBlock,404isStale: () => stale,405dispose: () => {406codeBlock.reset();407stale = true;408this._pool.release(codeBlock);409}410};411}412}413414export function codeblockHasClosingBackticks(str: string): boolean {415str = str.trim();416return !!str.match(/\n```+$/);417}418419export class CollapsedCodeBlock extends Disposable {420421public readonly element: HTMLElement;422423private readonly hover = this._register(new MutableDisposable());424private tooltip: string | undefined;425426private _uri: URI | undefined;427public get uri(): URI | undefined {428return this._uri;429}430431private _currentDiff: IEditSessionEntryDiff | undefined;432433private readonly _progressStore = this._store.add(new DisposableStore());434435constructor(436private readonly sessionId: string,437private readonly requestId: string,438private readonly inUndoStop: string | undefined,439@ILabelService private readonly labelService: ILabelService,440@IEditorService private readonly editorService: IEditorService,441@IModelService private readonly modelService: IModelService,442@ILanguageService private readonly languageService: ILanguageService,443@IContextMenuService private readonly contextMenuService: IContextMenuService,444@IContextKeyService private readonly contextKeyService: IContextKeyService,445@IMenuService private readonly menuService: IMenuService,446@IHoverService private readonly hoverService: IHoverService,447@IChatService private readonly chatService: IChatService,448) {449super();450this.element = $('.chat-codeblock-pill-widget');451this.element.tabIndex = 0;452this.element.classList.add('show-file-icons');453this.element.role = 'button';454this._register(dom.addDisposableListener(this.element, 'click', () => this._showDiff()));455this._register(dom.addDisposableListener(this.element, 'keydown', (e) => {456if (e.key === 'Enter' || e.key === ' ') {457this._showDiff();458}459}));460this._register(dom.addDisposableListener(this.element, dom.EventType.CONTEXT_MENU, domEvent => {461const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);462dom.EventHelper.stop(domEvent, true);463464this.contextMenuService.showContextMenu({465contextKeyService: this.contextKeyService,466getAnchor: () => event,467getActions: () => {468const menu = this.menuService.getMenuActions(MenuId.ChatEditingCodeBlockContext, this.contextKeyService, { arg: { sessionId, requestId, uri: this.uri, stopId: inUndoStop } });469return getFlatContextMenuActions(menu);470},471});472}));473}474475private _showDiff(): void {476if (this._currentDiff) {477this.editorService.openEditor({478original: { resource: this._currentDiff.originalURI },479modified: { resource: this._currentDiff.modifiedURI },480options: { transient: true },481});482} else if (this.uri) {483this.editorService.openEditor({ resource: this.uri });484}485}486487render(uri: URI, isStreaming?: boolean): void {488this._progressStore.clear();489490this._uri = uri;491492const session = this.chatService.getSession(this.sessionId);493const iconText = this.labelService.getUriBasenameLabel(uri);494495let editSession = session?.editingSessionObs?.promiseResult.get()?.data;496let modifiedEntry = editSession?.getEntry(uri);497let modifiedByResponse = modifiedEntry?.isCurrentlyBeingModifiedBy.get();498const isComplete = !modifiedByResponse || modifiedByResponse.requestId !== this.requestId;499500let iconClasses: string[] = [];501if (isStreaming || !isComplete) {502const codicon = ThemeIcon.modify(Codicon.loading, 'spin');503iconClasses = ThemeIcon.asClassNameArray(codicon);504} else {505const fileKind = uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE;506iconClasses = getIconClasses(this.modelService, this.languageService, uri, fileKind);507}508509const iconEl = dom.$('span.icon');510iconEl.classList.add(...iconClasses);511512const children = [dom.$('span.icon-label', {}, iconText)];513const labelDetail = dom.$('span.label-detail', {}, '');514children.push(labelDetail);515if (isStreaming) {516labelDetail.textContent = localize('chat.codeblock.generating', "Generating edits...");517}518519this.element.replaceChildren(iconEl, ...children);520this.updateTooltip(this.labelService.getUriLabel(uri, { relative: false }));521522const renderDiff = (changes: IEditSessionEntryDiff | undefined) => {523const labelAdded = this.element.querySelector('.label-added') ?? this.element.appendChild(dom.$('span.label-added'));524const labelRemoved = this.element.querySelector('.label-removed') ?? this.element.appendChild(dom.$('span.label-removed'));525if (changes && !changes?.identical && !changes?.quitEarly) {526this._currentDiff = changes;527labelAdded.textContent = `+${changes.added}`;528labelRemoved.textContent = `-${changes.removed}`;529const insertionsFragment = changes.added === 1 ? localize('chat.codeblock.insertions.one', "1 insertion") : localize('chat.codeblock.insertions', "{0} insertions", changes.added);530const deletionsFragment = changes.removed === 1 ? localize('chat.codeblock.deletions.one', "1 deletion") : localize('chat.codeblock.deletions', "{0} deletions", changes.removed);531const summary = localize('summary', 'Edited {0}, {1}, {2}', iconText, insertionsFragment, deletionsFragment);532this.element.ariaLabel = summary;533this.updateTooltip(summary);534}535};536537let diffBetweenStops: IObservable<IEditSessionEntryDiff | undefined> | undefined;538539// Show a percentage progress that is driven by the rewrite540541this._progressStore.add(autorun(r => {542if (!editSession) {543editSession = session?.editingSessionObs?.promiseResult.read(r)?.data;544modifiedEntry = editSession?.getEntry(uri);545}546547modifiedByResponse = modifiedEntry?.isCurrentlyBeingModifiedBy.read(r);548let diffValue = diffBetweenStops?.read(r);549const isComplete = !!diffValue || !modifiedByResponse || modifiedByResponse.requestId !== this.requestId;550const rewriteRatio = modifiedEntry?.rewriteRatio.read(r);551552if (!isStreaming && !isComplete) {553const value = rewriteRatio;554labelDetail.textContent = value === 0 || !value ? localize('chat.codeblock.generating', "Generating edits...") : localize('chat.codeblock.applyingPercentage', "Applying edits ({0}%)...", Math.round(value * 100));555} else if (!isStreaming && isComplete) {556iconEl.classList.remove(...iconClasses);557const fileKind = uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE;558iconEl.classList.add(...getIconClasses(this.modelService, this.languageService, uri, fileKind));559labelDetail.textContent = '';560}561562if (!diffBetweenStops) {563diffBetweenStops = modifiedEntry && editSession564? editSession.getEntryDiffBetweenStops(modifiedEntry.modifiedURI, this.requestId, this.inUndoStop)565: undefined;566diffValue = diffBetweenStops?.read(r);567}568569if (!isStreaming && isComplete) {570renderDiff(diffValue);571}572}));573}574575private updateTooltip(tooltip: string): void {576this.tooltip = tooltip;577if (!this.hover.value) {578this.hover.value = this.hoverService.setupDelayedHover(this.element, () => (579{580content: this.tooltip!,581appearance: { compact: true, showPointer: true },582position: { hoverPosition: HoverPosition.BELOW },583persistence: { hideOnKeyDown: true },584}585));586}587}588}589590591