Skip to content

Commit 233a7c8

Browse files
committed
Opens the virtual document immediately with metadata header and loading indicator, load summary when ready.
(#4328, #4489)
1 parent a669ddc commit 233a7c8

File tree

7 files changed

+234
-19
lines changed

7 files changed

+234
-19
lines changed

src/commands/explainBase.ts

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import type { TextEditor, Uri } from 'vscode';
2+
import { md5 } from '@env/crypto';
23
import type { GlCommands } from '../constants.commands';
34
import type { Container } from '../container';
45
import type { MarkdownContentMetadata } from '../documents/markdown';
56
import { getMarkdownHeaderContent } from '../documents/markdown';
67
import type { GitRepositoryService } from '../git/gitRepositoryService';
78
import { GitUri } from '../git/gitUri';
8-
import type { AIExplainSource, AISummarizeResult } from '../plus/ai/aiProviderService';
9+
import type { AIExplainSource, AIResultContext, AISummarizeResult } from '../plus/ai/aiProviderService';
10+
import type { AIModel } from '../plus/ai/models/model';
911
import { getAIResultContext } from '../plus/ai/utils/-webview/ai.utils';
1012
import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker';
1113
import { showMarkdownPreview } from '../system/-webview/markdown';
@@ -55,23 +57,102 @@ export abstract class ExplainCommandBase extends GlCommandBase {
5557
return svc;
5658
}
5759

60+
/**
61+
* Opens a document immediately with loading state, then updates it when AI content is ready
62+
*/
5863
protected openDocument(
59-
result: AISummarizeResult,
64+
aiPromise: Promise<AISummarizeResult | 'cancelled' | undefined>,
6065
path: string,
66+
model: AIModel,
6167
metadata: Omit<MarkdownContentMetadata, 'context'>,
6268
): void {
63-
const metadataWithContext: MarkdownContentMetadata = { ...metadata, context: getAIResultContext(result) };
69+
// Create a placeholder AI context for the loading state
70+
const loadingContext: AIResultContext = {
71+
id: `loading-${md5(path)}`,
72+
type: 'explain-changes',
73+
model: model,
74+
};
6475

76+
const metadataWithContext: MarkdownContentMetadata = { ...metadata, context: loadingContext };
6577
const headerContent = getMarkdownHeaderContent(metadataWithContext, this.container.telemetry.enabled);
66-
const content = `${headerContent}\n\n${result.parsed.summary}\n\n${result.parsed.body}`;
78+
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.*`;
6779

80+
// Open the document immediately with loading content
6881
const documentUri = this.container.markdown.openDocument(
69-
content,
82+
loadingContent,
7083
path,
7184
metadata.header.title,
7285
metadataWithContext,
7386
);
7487

7588
showMarkdownPreview(documentUri);
89+
90+
// Update the document when AI content is ready
91+
void this.updateDocumentWhenReady(documentUri, aiPromise, metadataWithContext);
92+
}
93+
94+
/**
95+
* Updates the document content when AI generation completes
96+
*/
97+
private async updateDocumentWhenReady(
98+
documentUri: Uri,
99+
aiPromise: Promise<AISummarizeResult | 'cancelled' | undefined>,
100+
metadata: MarkdownContentMetadata,
101+
): Promise<void> {
102+
try {
103+
const result = await aiPromise;
104+
105+
if (result === 'cancelled') {
106+
// Update with cancellation message
107+
const cancelledContent = this.createCancelledContent(metadata);
108+
this.container.markdown.updateDocument(documentUri, cancelledContent);
109+
return;
110+
}
111+
112+
if (result == null) {
113+
// Update with error message
114+
const errorContent = this.createErrorContent(metadata);
115+
this.container.markdown.updateDocument(documentUri, errorContent);
116+
return;
117+
}
118+
119+
// Update with successful AI content
120+
this.updateDocumentWithResult(documentUri, result, metadata);
121+
} catch (_error) {
122+
// Update with error message
123+
const errorContent = this.createErrorContent(metadata);
124+
this.container.markdown.updateDocument(documentUri, errorContent);
125+
}
126+
}
127+
128+
/**
129+
* Updates the document with successful AI result
130+
*/
131+
private updateDocumentWithResult(
132+
documentUri: Uri,
133+
result: AISummarizeResult,
134+
metadata: MarkdownContentMetadata,
135+
): void {
136+
const metadataWithContext: MarkdownContentMetadata = { ...metadata, context: getAIResultContext(result) };
137+
const headerContent = getMarkdownHeaderContent(metadataWithContext, this.container.telemetry.enabled);
138+
const content = `${headerContent}\n\n${result.parsed.summary}\n\n${result.parsed.body}`;
139+
140+
this.container.markdown.updateDocument(documentUri, content);
141+
}
142+
143+
/**
144+
* Creates content for cancelled AI generation
145+
*/
146+
private createCancelledContent(metadata: MarkdownContentMetadata): string {
147+
const headerContent = getMarkdownHeaderContent(metadata, this.container.telemetry.enabled);
148+
return `${headerContent}\n\n---\n\n⚠️ **Generation Cancelled**\n\nThe AI explanation was cancelled before completion.`;
149+
}
150+
151+
/**
152+
* Creates content for failed AI generation
153+
*/
154+
private createErrorContent(metadata: MarkdownContentMetadata): string {
155+
const headerContent = getMarkdownHeaderContent(metadata, this.container.telemetry.enabled);
156+
return `${headerContent}\n\n---\n\n❌ **Generation Failed**\n\nUnable to generate an explanation for the changes. Please try again.`;
76157
}
77158
}

src/commands/explainBranch.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,12 @@ export class ExplainBranchCommand extends ExplainCommandBase {
121121
return;
122122
}
123123

124-
this.openDocument(result, `/explain/branch/${branch.ref}/${result.model.id}`, {
124+
const {
125+
aiPromise,
126+
info: { model },
127+
} = result;
128+
129+
this.openDocument(aiPromise, `/explain/branch/${branch.ref}/${model.id}`, model, {
125130
header: { title: 'Branch Summary', subtitle: branch.name },
126131
command: {
127132
label: 'Explain Branch Changes',

src/commands/explainCommit.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,12 @@ export class ExplainCommitCommand extends ExplainCommandBase {
9797
return;
9898
}
9999

100-
this.openDocument(result, `/explain/commit/${commit.ref}/${result.model.id}`, {
100+
const {
101+
aiPromise,
102+
info: { model },
103+
} = result;
104+
105+
this.openDocument(aiPromise, `/explain/commit/${commit.ref}/${model.id}`, model, {
101106
header: { title: 'Commit Summary', subtitle: `${commit.summary} (${commit.shortSha})` },
102107
command: {
103108
label: 'Explain Commit Summary',

src/commands/explainStash.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,16 @@ export class ExplainStashCommand extends ExplainCommandBase {
7979
if (result === 'cancelled') return;
8080

8181
if (result == null) {
82-
void showGenericErrorMessage('No changes found to explain for stash');
82+
void showGenericErrorMessage('Unable to explain stash');
8383
return;
8484
}
8585

86-
this.openDocument(result, `/explain/stash/${commit.ref}/${result.model.id}`, {
86+
const {
87+
aiPromise,
88+
info: { model },
89+
} = result;
90+
91+
this.openDocument(aiPromise, `/explain/stash/${commit.ref}/${model.id}`, model, {
8792
header: { title: 'Stash Summary', subtitle: commit.message || commit.ref },
8893
command: {
8994
label: 'Explain Stash Changes',

src/commands/explainWip.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,12 @@ export class ExplainWipCommand extends ExplainCommandBase {
119119
return;
120120
}
121121

122-
this.openDocument(result, `/explain/wip/${svc.path}/${result.model.id}`, {
122+
const {
123+
aiPromise,
124+
info: { model },
125+
} = result;
126+
127+
this.openDocument(aiPromise, `/explain/wip/${svc.path}/${model.id}`, model, {
123128
header: {
124129
title: `${capitalize(label)} Changes Summary`,
125130
subtitle: `${capitalize(label)} Changes (${repoName})`,

src/plus/ai/aiProviderService.ts

Lines changed: 115 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,11 @@ export class AIProviderService implements Disposable {
565565
commitOrRevision: GitRevisionReference | GitCommit,
566566
sourceContext: AIExplainSource,
567567
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
568-
): Promise<AISummarizeResult | 'cancelled' | undefined> {
568+
): Promise<
569+
| undefined
570+
| 'cancelled'
571+
| { aiPromise: Promise<AISummarizeResult | 'cancelled' | undefined>; info: { model: AIModel } }
572+
> {
569573
const svc = this.container.git.getRepositoryService(commitOrRevision.repoPath);
570574
return this.explainChanges(
571575
async cancellation => {
@@ -598,10 +602,14 @@ export class AIProviderService implements Disposable {
598602
| ((cancellationToken: CancellationToken) => Promise<PromptTemplateContext<'explain-changes'>>),
599603
sourceContext: AIExplainSource,
600604
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
601-
): Promise<AISummarizeResult | 'cancelled' | undefined> {
605+
): Promise<
606+
| undefined
607+
| 'cancelled'
608+
| { aiPromise: Promise<AISummarizeResult | 'cancelled' | undefined>; info: { model: AIModel } }
609+
> {
602610
const { type, ...source } = sourceContext;
603611

604-
const result = await this.sendRequest(
612+
const complexResult = await this.sendRequestAndGetPartialRequestInfo(
605613
'explain-changes',
606614
async (model, reporting, cancellation, maxInputTokens, retries) => {
607615
if (typeof promptContext === 'function') {
@@ -644,11 +652,22 @@ export class AIProviderService implements Disposable {
644652
}),
645653
options,
646654
);
647-
return result === 'cancelled'
648-
? result
649-
: result != null
650-
? { ...result, type: 'explain-changes', parsed: parseSummarizeResult(result.content) }
651-
: undefined;
655+
656+
if (complexResult === 'cancelled') return complexResult;
657+
if (complexResult == null) return undefined;
658+
659+
const aiPromise: Promise<AISummarizeResult | 'cancelled' | undefined> = complexResult.aiPromise.then(result =>
660+
result === 'cancelled'
661+
? result
662+
: result != null
663+
? { ...result, type: 'explain-changes', parsed: parseSummarizeResult(result.content) }
664+
: undefined,
665+
);
666+
667+
return {
668+
aiPromise: aiPromise,
669+
info: complexResult.info,
670+
};
652671
}
653672

654673
async generateCommitMessage(
@@ -1392,6 +1411,56 @@ export class AIProviderService implements Disposable {
13921411
}
13931412
}
13941413

1414+
private async sendRequestAndGetPartialRequestInfo<T extends AIActionType>(
1415+
action: T,
1416+
getMessages: (
1417+
model: AIModel,
1418+
reporting: TelemetryEvents['ai/generate' | 'ai/explain'],
1419+
cancellation: CancellationToken,
1420+
maxCodeCharacters: number,
1421+
retries: number,
1422+
) => Promise<AIChatMessage[]>,
1423+
getProgressTitle: (model: AIModel) => string,
1424+
source: Source,
1425+
getTelemetryInfo: (model: AIModel) => {
1426+
key: 'ai/generate' | 'ai/explain';
1427+
data: TelemetryEvents['ai/generate' | 'ai/explain'];
1428+
},
1429+
options?: {
1430+
cancellation?: CancellationToken;
1431+
generating?: Deferred<AIModel>;
1432+
modelOptions?: { outputTokens?: number; temperature?: number };
1433+
progress?: ProgressOptions;
1434+
},
1435+
): Promise<
1436+
| undefined
1437+
| 'cancelled'
1438+
| {
1439+
aiPromise: Promise<AIRequestResult | 'cancelled' | undefined>;
1440+
info: { model: AIModel };
1441+
}
1442+
> {
1443+
if (!(await this.ensureFeatureAccess(action, source))) {
1444+
return 'cancelled';
1445+
}
1446+
const model = await this.getModel(undefined, source);
1447+
if (model == null || options?.cancellation?.isCancellationRequested) {
1448+
options?.generating?.cancel();
1449+
return undefined;
1450+
}
1451+
1452+
const aiPromise = this.sendRequestWithModel(
1453+
model,
1454+
action,
1455+
getMessages,
1456+
getProgressTitle,
1457+
source,
1458+
getTelemetryInfo,
1459+
options,
1460+
);
1461+
return { aiPromise: aiPromise, info: { model: model } };
1462+
}
1463+
13951464
private async sendRequest<T extends AIActionType>(
13961465
action: T,
13971466
getMessages: (
@@ -1419,6 +1488,44 @@ export class AIProviderService implements Disposable {
14191488
}
14201489

14211490
const model = await this.getModel(undefined, source);
1491+
return this.sendRequestWithModel(
1492+
model,
1493+
action,
1494+
getMessages,
1495+
getProgressTitle,
1496+
source,
1497+
getTelemetryInfo,
1498+
options,
1499+
);
1500+
}
1501+
1502+
private async sendRequestWithModel<T extends AIActionType>(
1503+
model: AIModel | undefined,
1504+
action: T,
1505+
getMessages: (
1506+
model: AIModel,
1507+
reporting: TelemetryEvents['ai/generate' | 'ai/explain'],
1508+
cancellation: CancellationToken,
1509+
maxCodeCharacters: number,
1510+
retries: number,
1511+
) => Promise<AIChatMessage[]>,
1512+
getProgressTitle: (model: AIModel) => string,
1513+
source: Source,
1514+
getTelemetryInfo: (model: AIModel) => {
1515+
key: 'ai/generate' | 'ai/explain';
1516+
data: TelemetryEvents['ai/generate' | 'ai/explain'];
1517+
},
1518+
options?: {
1519+
cancellation?: CancellationToken;
1520+
generating?: Deferred<AIModel>;
1521+
modelOptions?: { outputTokens?: number; temperature?: number };
1522+
progress?: ProgressOptions;
1523+
},
1524+
): Promise<AIRequestResult | 'cancelled' | undefined> {
1525+
if (!(await this.ensureFeatureAccess(action, source))) {
1526+
return 'cancelled';
1527+
}
1528+
14221529
if (options?.cancellation?.isCancellationRequested) {
14231530
options?.generating?.cancel();
14241531
return 'cancelled';

src/webviews/plus/patchDetails/patchDetailsWebview.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -841,7 +841,14 @@ export class PatchDetailsWebviewProvider
841841

842842
if (result == null) throw new Error('Error retrieving content');
843843

844-
params = { result: result.parsed };
844+
const { aiPromise } = result;
845+
846+
const aiResult = await aiPromise;
847+
if (aiResult === 'cancelled') throw new Error('Operation was canceled');
848+
849+
if (aiResult == null) throw new Error('Error retrieving content');
850+
851+
params = { result: aiResult.parsed };
845852
} catch (ex) {
846853
debugger;
847854
params = { error: { message: ex.message } };

0 commit comments

Comments
 (0)