From ca533342d348f32e3cef5dd23abde9a106dfc029 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Fri, 11 Jul 2025 16:02:11 +0200 Subject: [PATCH 1/6] Recovers markdown preview content on tab visibility changes Addresses issues where markdown previews may display corrupted or outdated content by detecting tab visibility changes and forcing content recovery for affected documents. Improves reliability of previews when switching tabs or reopening custom editors. (#4328, #4489) --- src/documents/markdown.ts | 70 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/src/documents/markdown.ts b/src/documents/markdown.ts index 188f87df1b927..9731ec000919d 100644 --- a/src/documents/markdown.ts +++ b/src/documents/markdown.ts @@ -1,5 +1,5 @@ -import type { Disposable, Event, TextDocumentContentProvider } from 'vscode'; -import { EventEmitter, Uri, workspace } from 'vscode'; +import type { Event, TabChangeEvent, TextDocumentContentProvider } from 'vscode'; +import { Disposable, EventEmitter, TabInputCustom, Uri, window, workspace } from 'vscode'; import { Schemes } from '../constants'; import type { GlCommands } from '../constants.commands'; import type { Container } from '../container'; @@ -17,6 +17,7 @@ export interface MarkdownContentMetadata { export class MarkdownContentProvider implements TextDocumentContentProvider { private contents = new Map(); private registration: Disposable; + private visibilityTracker: Disposable; private _onDidChange = new EventEmitter(); get onDidChange(): Event { @@ -26,6 +27,18 @@ export class MarkdownContentProvider implements TextDocumentContentProvider { constructor(private container: Container) { this.registration = workspace.registerTextDocumentContentProvider(Schemes.GitLensAIMarkdown, this); + // Track document visibility to detect when content needs recovery + this.visibilityTracker = Disposable.from( + window.tabGroups.onDidChangeTabs((e: TabChangeEvent) => { + this.onTabsChanged(e); + }), + // // Also track when tabs change which might affect preview visibility + // window.onDidChangeActiveTextEditor(() => { + // // Delay to allow VS Code to finish tab switching + // setTimeout(() => this.forceRecoveryForAllOpenedDocuments(), 1000); + // }), + ); + workspace.onDidCloseTextDocument(document => { if (document.uri.scheme === Schemes.GitLensAIMarkdown) { this.contents.delete(document.uri.toString()); @@ -71,6 +84,30 @@ export class MarkdownContentProvider implements TextDocumentContentProvider { this._onDidChange.fire(uri); } + /** + * Forces content recovery for a document - useful when content gets corrupted + */ + forceContentRecovery(uri: Uri): void { + const uriString = uri.toString(); + if (!this.contents.has(uriString)) return; + + const storedContent = this.contents.get(uriString); + if (!storedContent) return; + + // I'm deleting the content because if I just fire the change once to make VSCode + // reach our `provideTextDocumentContent` method + // and `provideTextDocumentContent` returns the unchanged conent, + // VSCode will not refresh the content, instead it keeps displaying the original conetnt + // that the view had when it was opened initially. + // That's why I need to blink the content. + if (storedContent.at(storedContent.length - 1) === '\n') { + this.contents.set(uriString, storedContent.substring(0, storedContent.length - 1)); + } else { + this.contents.set(uriString, `${storedContent}\n`); + } + this._onDidChange.fire(uri); + } + closeDocument(uri: Uri): void { this.contents.delete(uri.toString()); } @@ -78,6 +115,35 @@ export class MarkdownContentProvider implements TextDocumentContentProvider { dispose(): void { this.contents.clear(); this.registration.dispose(); + this.visibilityTracker.dispose(); + } + + // /** + // * Checks preview visibility by examining workspace documents + // */ + // private forceRecoveryForAllOpenedDocuments(): void { + // // Check all workspace documents for our markdown scheme + // for (const document of workspace.textDocuments) { + // if (document.uri.scheme === Schemes.GitLensAIMarkdown) { + // const uriString = document.uri.toString(); + // if (this.contents.has(uriString)) { + // console.log(`[GitLens] Checking preview visibility for: ${document.uri.path}`); + // // Trigger recovery check for this document + // setTimeout(() => { + // this.forceContentRecovery(document.uri); + // }, 1000); + // } + // } + // } + // } + + private onTabsChanged(e: TabChangeEvent) { + for (const tab of e.changed) { + if (tab.input instanceof TabInputCustom && tab.input.uri.scheme === Schemes.GitLensAIMarkdown) { + const uri = tab.input.uri; + this.forceContentRecovery(uri); + } + } } } From fe40f2fdf997364aaa6da9af3b9ccba14d5daf31 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Fri, 11 Jul 2025 18:41:57 +0200 Subject: [PATCH 2/6] Opens the virtual document immediately with metadata header and loading indicator, load summary when ready. (#4328, #4489) --- src/commands/explainBase.ts | 93 +++++++++++- src/commands/explainBranch.ts | 7 +- src/commands/explainCommit.ts | 7 +- src/commands/explainStash.ts | 9 +- src/commands/explainWip.ts | 7 +- src/plus/ai/aiProviderService.ts | 133 ++++++++++++++++-- .../plus/patchDetails/patchDetailsWebview.ts | 9 +- 7 files changed, 241 insertions(+), 24 deletions(-) diff --git a/src/commands/explainBase.ts b/src/commands/explainBase.ts index eb78201a1ad30..03a5413af4c43 100644 --- a/src/commands/explainBase.ts +++ b/src/commands/explainBase.ts @@ -1,11 +1,13 @@ import type { TextEditor, Uri } from 'vscode'; +import { md5 } from '@env/crypto'; import type { GlCommands } from '../constants.commands'; import type { Container } from '../container'; import type { MarkdownContentMetadata } from '../documents/markdown'; import { getMarkdownHeaderContent } from '../documents/markdown'; import type { GitRepositoryService } from '../git/gitRepositoryService'; import { GitUri } from '../git/gitUri'; -import type { AIExplainSource, AISummarizeResult } from '../plus/ai/aiProviderService'; +import type { AIExplainSource, AIResultContext, AISummarizeResult } from '../plus/ai/aiProviderService'; +import type { AIModel } from '../plus/ai/models/model'; import { getAIResultContext } from '../plus/ai/utils/-webview/ai.utils'; import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker'; import { showMarkdownPreview } from '../system/-webview/markdown'; @@ -55,23 +57,104 @@ export abstract class ExplainCommandBase extends GlCommandBase { return svc; } + /** + * Opens a document immediately with loading state, then updates it when AI content is ready + */ protected openDocument( - result: AISummarizeResult, + aiPromise: Promise, path: string, + model: AIModel, + feature: string, metadata: Omit, ): void { - const metadataWithContext: MarkdownContentMetadata = { ...metadata, context: getAIResultContext(result) }; + // Create a placeholder AI context for the loading state + const loadingContext: AIResultContext = { + id: `loading-${md5(path)}`, + type: 'explain-changes', + feature: feature, + model: model, + }; + const metadataWithContext: MarkdownContentMetadata = { ...metadata, context: loadingContext }; const headerContent = getMarkdownHeaderContent(metadataWithContext, this.container.telemetry.enabled); - const content = `${headerContent}\n\n${result.parsed.summary}\n\n${result.parsed.body}`; + const loadingContent = `${headerContent}\n\n---\n\n🤖 **Generating explanation...**\n\nPlease wait while the AI analyzes the changes and generates an explanation. This document will update automatically when the content is ready.\n\n*This may take a few moments depending on the complexity of the changes.*`; + // Open the document immediately with loading content const documentUri = this.container.markdown.openDocument( - content, + loadingContent, path, metadata.header.title, metadataWithContext, ); showMarkdownPreview(documentUri); + + // Update the document when AI content is ready + void this.updateDocumentWhenReady(documentUri, aiPromise, metadataWithContext); + } + + /** + * Updates the document content when AI generation completes + */ + private async updateDocumentWhenReady( + documentUri: Uri, + aiPromise: Promise, + metadata: MarkdownContentMetadata, + ): Promise { + try { + const result = await aiPromise; + + if (result === 'cancelled') { + // Update with cancellation message + const cancelledContent = this.createCancelledContent(metadata); + this.container.markdown.updateDocument(documentUri, cancelledContent); + return; + } + + if (result == null) { + // Update with error message + const errorContent = this.createErrorContent(metadata); + this.container.markdown.updateDocument(documentUri, errorContent); + return; + } + + // Update with successful AI content + this.updateDocumentWithResult(documentUri, result, metadata); + } catch (_error) { + // Update with error message + const errorContent = this.createErrorContent(metadata); + this.container.markdown.updateDocument(documentUri, errorContent); + } + } + + /** + * Updates the document with successful AI result + */ + private updateDocumentWithResult( + documentUri: Uri, + result: AISummarizeResult, + metadata: MarkdownContentMetadata, + ): void { + const metadataWithContext: MarkdownContentMetadata = { ...metadata, context: getAIResultContext(result) }; + const headerContent = getMarkdownHeaderContent(metadataWithContext, this.container.telemetry.enabled); + const content = `${headerContent}\n\n${result.parsed.summary}\n\n${result.parsed.body}`; + + this.container.markdown.updateDocument(documentUri, content); + } + + /** + * Creates content for cancelled AI generation + */ + private createCancelledContent(metadata: MarkdownContentMetadata): string { + const headerContent = getMarkdownHeaderContent(metadata, this.container.telemetry.enabled); + return `${headerContent}\n\n---\n\n⚠️ **Generation Cancelled**\n\nThe AI explanation was cancelled before completion.`; + } + + /** + * Creates content for failed AI generation + */ + private createErrorContent(metadata: MarkdownContentMetadata): string { + const headerContent = getMarkdownHeaderContent(metadata, this.container.telemetry.enabled); + return `${headerContent}\n\n---\n\n❌ **Generation Failed**\n\nUnable to generate an explanation for the changes. Please try again.`; } } diff --git a/src/commands/explainBranch.ts b/src/commands/explainBranch.ts index d504534ae7674..1193691d324c5 100644 --- a/src/commands/explainBranch.ts +++ b/src/commands/explainBranch.ts @@ -121,7 +121,12 @@ export class ExplainBranchCommand extends ExplainCommandBase { return; } - this.openDocument(result, `/explain/branch/${branch.ref}/${result.model.id}`, { + const { + aiPromise, + info: { model }, + } = result; + + this.openDocument(aiPromise, `/explain/branch/${branch.ref}/${model.id}`, model, 'explain-branch', { header: { title: 'Branch Summary', subtitle: branch.name }, command: { label: 'Explain Branch Changes', diff --git a/src/commands/explainCommit.ts b/src/commands/explainCommit.ts index eeacc9fec5302..a753be0feb7fb 100644 --- a/src/commands/explainCommit.ts +++ b/src/commands/explainCommit.ts @@ -97,7 +97,12 @@ export class ExplainCommitCommand extends ExplainCommandBase { return; } - this.openDocument(result, `/explain/commit/${commit.ref}/${result.model.id}`, { + const { + aiPromise, + info: { model }, + } = result; + + this.openDocument(aiPromise, `/explain/commit/${commit.ref}/${model.id}`, model, 'explain-commit', { header: { title: 'Commit Summary', subtitle: `${commit.summary} (${commit.shortSha})` }, command: { label: 'Explain Commit Summary', diff --git a/src/commands/explainStash.ts b/src/commands/explainStash.ts index 50c26786cf679..435df0121c598 100644 --- a/src/commands/explainStash.ts +++ b/src/commands/explainStash.ts @@ -79,11 +79,16 @@ export class ExplainStashCommand extends ExplainCommandBase { if (result === 'cancelled') return; if (result == null) { - void showGenericErrorMessage('No changes found to explain for stash'); + void showGenericErrorMessage('Unable to explain stash'); return; } - this.openDocument(result, `/explain/stash/${commit.ref}/${result.model.id}`, { + const { + aiPromise, + info: { model }, + } = result; + + this.openDocument(aiPromise, `/explain/stash/${commit.ref}/${model.id}`, model, 'explain-stash', { header: { title: 'Stash Summary', subtitle: commit.message || commit.ref }, command: { label: 'Explain Stash Changes', diff --git a/src/commands/explainWip.ts b/src/commands/explainWip.ts index 6925d2d6a51ab..81c095d459ea4 100644 --- a/src/commands/explainWip.ts +++ b/src/commands/explainWip.ts @@ -119,7 +119,12 @@ export class ExplainWipCommand extends ExplainCommandBase { return; } - this.openDocument(result, `/explain/wip/${svc.path}/${result.model.id}`, { + const { + aiPromise, + info: { model }, + } = result; + + this.openDocument(aiPromise, `/explain/wip/${svc.path}/${model.id}`, model, 'explain-wip', { header: { title: `${capitalize(label)} Changes Summary`, subtitle: `${capitalize(label)} Changes (${repoName})`, diff --git a/src/plus/ai/aiProviderService.ts b/src/plus/ai/aiProviderService.ts index 064569bb5cdb4..8f9ad46530a89 100644 --- a/src/plus/ai/aiProviderService.ts +++ b/src/plus/ai/aiProviderService.ts @@ -566,7 +566,11 @@ export class AIProviderService implements Disposable { commitOrRevision: GitRevisionReference | GitCommit, sourceContext: AIExplainSource, options?: { cancellation?: CancellationToken; progress?: ProgressOptions }, - ): Promise { + ): Promise< + | undefined + | 'cancelled' + | { aiPromise: Promise; info: { model: AIModel } } + > { const svc = this.container.git.getRepositoryService(commitOrRevision.repoPath); return this.explainChanges( async cancellation => { @@ -599,10 +603,14 @@ export class AIProviderService implements Disposable { | ((cancellationToken: CancellationToken) => Promise>), sourceContext: AIExplainSource, options?: { cancellation?: CancellationToken; progress?: ProgressOptions }, - ): Promise { + ): Promise< + | undefined + | 'cancelled' + | { aiPromise: Promise; info: { model: AIModel } } + > { const { type, ...source } = sourceContext; - const result = await this.sendRequest( + const complexResult = await this.sendRequestAndGetPartialRequestInfo( 'explain-changes', async (model, reporting, cancellation, maxInputTokens, retries) => { if (typeof promptContext === 'function') { @@ -645,16 +653,27 @@ export class AIProviderService implements Disposable { }), options, ); - return result === 'cancelled' - ? result - : result != null - ? { - ...result, - type: 'explain-changes', - feature: `explain-${type}`, - parsed: parseSummarizeResult(result.content), - } - : undefined; + + if (complexResult === 'cancelled') return complexResult; + if (complexResult == null) return undefined; + + const aiPromise: Promise = complexResult.aiPromise.then(result => + result === 'cancelled' + ? result + : result != null + ? { + ...result, + type: 'explain-changes', + feature: `explain-${type}`, + parsed: parseSummarizeResult(result.content), + } + : undefined, + ); + + return { + aiPromise: aiPromise, + info: complexResult.info, + }; } async generateCommitMessage( @@ -1422,6 +1441,56 @@ export class AIProviderService implements Disposable { } } + private async sendRequestAndGetPartialRequestInfo( + action: T, + getMessages: ( + model: AIModel, + reporting: TelemetryEvents['ai/generate' | 'ai/explain'], + cancellation: CancellationToken, + maxCodeCharacters: number, + retries: number, + ) => Promise, + getProgressTitle: (model: AIModel) => string, + source: Source, + getTelemetryInfo: (model: AIModel) => { + key: 'ai/generate' | 'ai/explain'; + data: TelemetryEvents['ai/generate' | 'ai/explain']; + }, + options?: { + cancellation?: CancellationToken; + generating?: Deferred; + modelOptions?: { outputTokens?: number; temperature?: number }; + progress?: ProgressOptions; + }, + ): Promise< + | undefined + | 'cancelled' + | { + aiPromise: Promise; + info: { model: AIModel }; + } + > { + if (!(await this.ensureFeatureAccess(action, source))) { + return 'cancelled'; + } + const model = await this.getModel(undefined, source); + if (model == null || options?.cancellation?.isCancellationRequested) { + options?.generating?.cancel(); + return undefined; + } + + const aiPromise = this.sendRequestWithModel( + model, + action, + getMessages, + getProgressTitle, + source, + getTelemetryInfo, + options, + ); + return { aiPromise: aiPromise, info: { model: model } }; + } + private async sendRequest( action: T, getMessages: ( @@ -1449,6 +1518,44 @@ export class AIProviderService implements Disposable { } const model = await this.getModel(undefined, source); + return this.sendRequestWithModel( + model, + action, + getMessages, + getProgressTitle, + source, + getTelemetryInfo, + options, + ); + } + + private async sendRequestWithModel( + model: AIModel | undefined, + action: T, + getMessages: ( + model: AIModel, + reporting: TelemetryEvents['ai/generate' | 'ai/explain'], + cancellation: CancellationToken, + maxCodeCharacters: number, + retries: number, + ) => Promise, + getProgressTitle: (model: AIModel) => string, + source: Source, + getTelemetryInfo: (model: AIModel) => { + key: 'ai/generate' | 'ai/explain'; + data: TelemetryEvents['ai/generate' | 'ai/explain']; + }, + options?: { + cancellation?: CancellationToken; + generating?: Deferred; + modelOptions?: { outputTokens?: number; temperature?: number }; + progress?: ProgressOptions; + }, + ): Promise { + if (!(await this.ensureFeatureAccess(action, source))) { + return 'cancelled'; + } + if (options?.cancellation?.isCancellationRequested) { options?.generating?.cancel(); return 'cancelled'; diff --git a/src/webviews/plus/patchDetails/patchDetailsWebview.ts b/src/webviews/plus/patchDetails/patchDetailsWebview.ts index c042414b3ec9b..004b159ea2559 100644 --- a/src/webviews/plus/patchDetails/patchDetailsWebview.ts +++ b/src/webviews/plus/patchDetails/patchDetailsWebview.ts @@ -841,7 +841,14 @@ export class PatchDetailsWebviewProvider if (result == null) throw new Error('Error retrieving content'); - params = { result: result.parsed }; + const { aiPromise } = result; + + const aiResult = await aiPromise; + if (aiResult === 'cancelled') throw new Error('Operation was canceled'); + + if (aiResult == null) throw new Error('Error retrieving content'); + + params = { result: aiResult.parsed }; } catch (ex) { debugger; params = { error: { message: ex.message } }; From f723d092c4f905d5d791aa8e4504de7ab3f499fd Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Mon, 14 Jul 2025 11:17:11 +0200 Subject: [PATCH 3/6] Store AI result context in memory for markdown documents. Thats because we cannot modify URI of document to review context after the result is rendered. (#4328, #4489) --- src/commands/aiFeedback.ts | 30 ++++++++++++++++++++++++-- src/commands/explainBase.ts | 7 +++++- src/plus/ai/utils/-webview/ai.utils.ts | 4 +++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/commands/aiFeedback.ts b/src/commands/aiFeedback.ts index 8e88489cb10ad..5e1677e45a3bf 100644 --- a/src/commands/aiFeedback.ts +++ b/src/commands/aiFeedback.ts @@ -1,5 +1,5 @@ -import type { TextEditor, Uri } from 'vscode'; -import { window } from 'vscode'; +import type { Disposable, TextEditor, Uri } from 'vscode'; +import { window, workspace } from 'vscode'; import type { AIFeedbackEvent, AIFeedbackUnhelpfulReasons, Source } from '../constants.telemetry'; import type { Container } from '../container'; import type { AIResultContext } from '../plus/ai/aiProviderService'; @@ -12,6 +12,7 @@ import type { Deferrable } from '../system/function/debounce'; import { debounce } from '../system/function/debounce'; import { filterMap, map } from '../system/iterable'; import { Logger } from '../system/logger'; +import { createDisposable } from '../system/unifiedDisposable'; import { ActiveEditorCommand } from './commandBase'; import { getCommandUri } from './commandBase.utils'; @@ -45,6 +46,31 @@ export class AIFeedbackUnhelpfulCommand extends ActiveEditorCommand { type UnhelpfulResult = { reasons?: AIFeedbackUnhelpfulReasons[]; custom?: string }; +let _documentCloseTracker: Disposable | undefined; +const _markdownDocuments = new Map(); +export function getMarkdownDocument(documentUri: string): AIResultContext | undefined { + return _markdownDocuments.get(documentUri); +} +export function setMarkdownDocument(documentUri: string, context: AIResultContext, container: Container): void { + _markdownDocuments.set(documentUri, context); + + if (!_documentCloseTracker) { + _documentCloseTracker = workspace.onDidCloseTextDocument(document => { + deleteMarkdownDocument(document.uri.toString()); + }); + container.context.subscriptions.push( + createDisposable(() => { + _documentCloseTracker?.dispose(); + _documentCloseTracker = undefined; + _markdownDocuments.clear(); + }), + ); + } +} +function deleteMarkdownDocument(documentUri: string): void { + _markdownDocuments.delete(documentUri); +} + const uriResponses = new UriMap(); let _updateContextDebounced: Deferrable<() => void> | undefined; diff --git a/src/commands/explainBase.ts b/src/commands/explainBase.ts index 03a5413af4c43..8a977df2c95c1 100644 --- a/src/commands/explainBase.ts +++ b/src/commands/explainBase.ts @@ -11,6 +11,7 @@ import type { AIModel } from '../plus/ai/models/model'; import { getAIResultContext } from '../plus/ai/utils/-webview/ai.utils'; import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker'; import { showMarkdownPreview } from '../system/-webview/markdown'; +import { setMarkdownDocument } from './aiFeedback'; import { GlCommandBase } from './commandBase'; import { getCommandUri } from './commandBase.utils'; @@ -135,10 +136,14 @@ export abstract class ExplainCommandBase extends GlCommandBase { result: AISummarizeResult, metadata: MarkdownContentMetadata, ): void { - const metadataWithContext: MarkdownContentMetadata = { ...metadata, context: getAIResultContext(result) }; + const context = getAIResultContext(result); + const metadataWithContext: MarkdownContentMetadata = { ...metadata, context: context }; const headerContent = getMarkdownHeaderContent(metadataWithContext, this.container.telemetry.enabled); const content = `${headerContent}\n\n${result.parsed.summary}\n\n${result.parsed.body}`; + // Store the AI result context in the feedback provider for documents that cannot store it in their URI + setMarkdownDocument(documentUri.toString(), context, this.container); + this.container.markdown.updateDocument(documentUri, content); } diff --git a/src/plus/ai/utils/-webview/ai.utils.ts b/src/plus/ai/utils/-webview/ai.utils.ts index 339b7538410df..0c5e7e551acd9 100644 --- a/src/plus/ai/utils/-webview/ai.utils.ts +++ b/src/plus/ai/utils/-webview/ai.utils.ts @@ -1,5 +1,6 @@ import type { Disposable, QuickInputButton } from 'vscode'; import { env, ThemeIcon, Uri, window } from 'vscode'; +import { getMarkdownDocument } from '../../../../commands/aiFeedback'; import { Schemes } from '../../../../constants'; import type { AIProviders } from '../../../../constants.ai'; import type { Container } from '../../../../container'; @@ -289,8 +290,9 @@ export function extractAIResultContext(uri: Uri | undefined): AIResultContext | if (!authority) return undefined; try { + const context: AIResultContext | undefined = getMarkdownDocument(uri.toString()); const metadata = decodeGitLensRevisionUriAuthority(authority); - return metadata.context; + return context ?? metadata.context; } catch (ex) { Logger.error(ex, 'extractResultContext'); return undefined; From 4bb09dcdad393b2e5bdd43d0a0fd60a884e19bea Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Mon, 14 Jul 2025 12:49:29 +0200 Subject: [PATCH 4/6] Mentions changes of summary documents in CHANGELOG: (#4328, #4489) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f15918aee9544..4ca294c014915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Changed - Changes branch favoriting to apply to both local and remote branch pairs ([#4497](https://github.com/gitkraken/vscode-gitlens/issues/4497)) +- Supports opening an explain summary document before summary content is generated ([#4328](https://github.com/gitkraken/vscode-gitlens/issues/4328)) ## [17.3.3] - 2025-07-28 From dbf0f241629aa3ad3962bca7bd203eba09596bb2 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Mon, 14 Jul 2025 12:57:30 +0200 Subject: [PATCH 5/6] Removes retired recovery code (#4328, #4489) --- src/documents/markdown.ts | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/documents/markdown.ts b/src/documents/markdown.ts index 9731ec000919d..578307b96ecbb 100644 --- a/src/documents/markdown.ts +++ b/src/documents/markdown.ts @@ -27,16 +27,11 @@ export class MarkdownContentProvider implements TextDocumentContentProvider { constructor(private container: Container) { this.registration = workspace.registerTextDocumentContentProvider(Schemes.GitLensAIMarkdown, this); - // Track document visibility to detect when content needs recovery + // Track tab changes to detect when content needs recovery this.visibilityTracker = Disposable.from( window.tabGroups.onDidChangeTabs((e: TabChangeEvent) => { this.onTabsChanged(e); }), - // // Also track when tabs change which might affect preview visibility - // window.onDidChangeActiveTextEditor(() => { - // // Delay to allow VS Code to finish tab switching - // setTimeout(() => this.forceRecoveryForAllOpenedDocuments(), 1000); - // }), ); workspace.onDidCloseTextDocument(document => { @@ -118,25 +113,6 @@ export class MarkdownContentProvider implements TextDocumentContentProvider { this.visibilityTracker.dispose(); } - // /** - // * Checks preview visibility by examining workspace documents - // */ - // private forceRecoveryForAllOpenedDocuments(): void { - // // Check all workspace documents for our markdown scheme - // for (const document of workspace.textDocuments) { - // if (document.uri.scheme === Schemes.GitLensAIMarkdown) { - // const uriString = document.uri.toString(); - // if (this.contents.has(uriString)) { - // console.log(`[GitLens] Checking preview visibility for: ${document.uri.path}`); - // // Trigger recovery check for this document - // setTimeout(() => { - // this.forceContentRecovery(document.uri); - // }, 1000); - // } - // } - // } - // } - private onTabsChanged(e: TabChangeEvent) { for (const tab of e.changed) { if (tab.input instanceof TabInputCustom && tab.input.uri.scheme === Schemes.GitLensAIMarkdown) { From 888f52114a0833f539f6859e33e3caecd6d464e0 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Mon, 28 Jul 2025 16:27:59 +0200 Subject: [PATCH 6/6] Uses quote block for the loading content instead of the double divider (#4328, #4489) --- src/commands/explainBase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/explainBase.ts b/src/commands/explainBase.ts index 8a977df2c95c1..927a877194d62 100644 --- a/src/commands/explainBase.ts +++ b/src/commands/explainBase.ts @@ -78,7 +78,7 @@ export abstract class ExplainCommandBase extends GlCommandBase { const metadataWithContext: MarkdownContentMetadata = { ...metadata, context: loadingContext }; const headerContent = getMarkdownHeaderContent(metadataWithContext, this.container.telemetry.enabled); - const loadingContent = `${headerContent}\n\n---\n\n🤖 **Generating explanation...**\n\nPlease wait while the AI analyzes the changes and generates an explanation. This document will update automatically when the content is ready.\n\n*This may take a few moments depending on the complexity of the changes.*`; + const loadingContent = `${headerContent}\n\n> 🤖 **Generating explanation...**\n> Please wait while the AI analyzes the changes and generates an explanation. This document will update automatically when the content is ready.\n>\n> *This may take a few moments depending on the complexity of the changes.*`; // Open the document immediately with loading content const documentUri = this.container.markdown.openDocument(