Skip to content

Commit bbb323b

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

File tree

7 files changed

+241
-24
lines changed

7 files changed

+241
-24
lines changed

src/commands/explainBase.ts

Lines changed: 88 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,104 @@ 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,
67+
feature: string,
6168
metadata: Omit<MarkdownContentMetadata, 'context'>,
6269
): void {
63-
const metadataWithContext: MarkdownContentMetadata = { ...metadata, context: getAIResultContext(result) };
70+
// Create a placeholder AI context for the loading state
71+
const loadingContext: AIResultContext = {
72+
id: `loading-${md5(path)}`,
73+
type: 'explain-changes',
74+
feature: feature,
75+
model: model,
76+
};
6477

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

82+
// Open the document immediately with loading content
6883
const documentUri = this.container.markdown.openDocument(
69-
content,
84+
loadingContent,
7085
path,
7186
metadata.header.title,
7287
metadataWithContext,
7388
);
7489

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

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, 'explain-branch', {
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, 'explain-commit', {
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, 'explain-stash', {
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, 'explain-wip', {
123128
header: {
124129
title: `${capitalize(label)} Changes Summary`,
125130
subtitle: `${capitalize(label)} Changes (${repoName})`,

src/plus/ai/aiProviderService.ts

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,11 @@ export class AIProviderService implements Disposable {
566566
commitOrRevision: GitRevisionReference | GitCommit,
567567
sourceContext: AIExplainSource,
568568
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
569-
): Promise<AISummarizeResult | 'cancelled' | undefined> {
569+
): Promise<
570+
| undefined
571+
| 'cancelled'
572+
| { aiPromise: Promise<AISummarizeResult | 'cancelled' | undefined>; info: { model: AIModel } }
573+
> {
570574
const svc = this.container.git.getRepositoryService(commitOrRevision.repoPath);
571575
return this.explainChanges(
572576
async cancellation => {
@@ -599,10 +603,14 @@ export class AIProviderService implements Disposable {
599603
| ((cancellationToken: CancellationToken) => Promise<PromptTemplateContext<'explain-changes'>>),
600604
sourceContext: AIExplainSource,
601605
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
602-
): Promise<AISummarizeResult | 'cancelled' | undefined> {
606+
): Promise<
607+
| undefined
608+
| 'cancelled'
609+
| { aiPromise: Promise<AISummarizeResult | 'cancelled' | undefined>; info: { model: AIModel } }
610+
> {
603611
const { type, ...source } = sourceContext;
604612

605-
const result = await this.sendRequest(
613+
const complexResult = await this.sendRequestAndGetPartialRequestInfo(
606614
'explain-changes',
607615
async (model, reporting, cancellation, maxInputTokens, retries) => {
608616
if (typeof promptContext === 'function') {
@@ -645,16 +653,27 @@ export class AIProviderService implements Disposable {
645653
}),
646654
options,
647655
);
648-
return result === 'cancelled'
649-
? result
650-
: result != null
651-
? {
652-
...result,
653-
type: 'explain-changes',
654-
feature: `explain-${type}`,
655-
parsed: parseSummarizeResult(result.content),
656-
}
657-
: undefined;
656+
657+
if (complexResult === 'cancelled') return complexResult;
658+
if (complexResult == null) return undefined;
659+
660+
const aiPromise: Promise<AISummarizeResult | 'cancelled' | undefined> = complexResult.aiPromise.then(result =>
661+
result === 'cancelled'
662+
? result
663+
: result != null
664+
? {
665+
...result,
666+
type: 'explain-changes',
667+
feature: `explain-${type}`,
668+
parsed: parseSummarizeResult(result.content),
669+
}
670+
: undefined,
671+
);
672+
673+
return {
674+
aiPromise: aiPromise,
675+
info: complexResult.info,
676+
};
658677
}
659678

660679
async generateCommitMessage(
@@ -1422,6 +1441,56 @@ export class AIProviderService implements Disposable {
14221441
}
14231442
}
14241443

1444+
private async sendRequestAndGetPartialRequestInfo<T extends AIActionType>(
1445+
action: T,
1446+
getMessages: (
1447+
model: AIModel,
1448+
reporting: TelemetryEvents['ai/generate' | 'ai/explain'],
1449+
cancellation: CancellationToken,
1450+
maxCodeCharacters: number,
1451+
retries: number,
1452+
) => Promise<AIChatMessage[]>,
1453+
getProgressTitle: (model: AIModel) => string,
1454+
source: Source,
1455+
getTelemetryInfo: (model: AIModel) => {
1456+
key: 'ai/generate' | 'ai/explain';
1457+
data: TelemetryEvents['ai/generate' | 'ai/explain'];
1458+
},
1459+
options?: {
1460+
cancellation?: CancellationToken;
1461+
generating?: Deferred<AIModel>;
1462+
modelOptions?: { outputTokens?: number; temperature?: number };
1463+
progress?: ProgressOptions;
1464+
},
1465+
): Promise<
1466+
| undefined
1467+
| 'cancelled'
1468+
| {
1469+
aiPromise: Promise<AIRequestResult | 'cancelled' | undefined>;
1470+
info: { model: AIModel };
1471+
}
1472+
> {
1473+
if (!(await this.ensureFeatureAccess(action, source))) {
1474+
return 'cancelled';
1475+
}
1476+
const model = await this.getModel(undefined, source);
1477+
if (model == null || options?.cancellation?.isCancellationRequested) {
1478+
options?.generating?.cancel();
1479+
return undefined;
1480+
}
1481+
1482+
const aiPromise = this.sendRequestWithModel(
1483+
model,
1484+
action,
1485+
getMessages,
1486+
getProgressTitle,
1487+
source,
1488+
getTelemetryInfo,
1489+
options,
1490+
);
1491+
return { aiPromise: aiPromise, info: { model: model } };
1492+
}
1493+
14251494
private async sendRequest<T extends AIActionType>(
14261495
action: T,
14271496
getMessages: (
@@ -1449,6 +1518,44 @@ export class AIProviderService implements Disposable {
14491518
}
14501519

14511520
const model = await this.getModel(undefined, source);
1521+
return this.sendRequestWithModel(
1522+
model,
1523+
action,
1524+
getMessages,
1525+
getProgressTitle,
1526+
source,
1527+
getTelemetryInfo,
1528+
options,
1529+
);
1530+
}
1531+
1532+
private async sendRequestWithModel<T extends AIActionType>(
1533+
model: AIModel | undefined,
1534+
action: T,
1535+
getMessages: (
1536+
model: AIModel,
1537+
reporting: TelemetryEvents['ai/generate' | 'ai/explain'],
1538+
cancellation: CancellationToken,
1539+
maxCodeCharacters: number,
1540+
retries: number,
1541+
) => Promise<AIChatMessage[]>,
1542+
getProgressTitle: (model: AIModel) => string,
1543+
source: Source,
1544+
getTelemetryInfo: (model: AIModel) => {
1545+
key: 'ai/generate' | 'ai/explain';
1546+
data: TelemetryEvents['ai/generate' | 'ai/explain'];
1547+
},
1548+
options?: {
1549+
cancellation?: CancellationToken;
1550+
generating?: Deferred<AIModel>;
1551+
modelOptions?: { outputTokens?: number; temperature?: number };
1552+
progress?: ProgressOptions;
1553+
},
1554+
): Promise<AIRequestResult | 'cancelled' | undefined> {
1555+
if (!(await this.ensureFeatureAccess(action, source))) {
1556+
return 'cancelled';
1557+
}
1558+
14521559
if (options?.cancellation?.isCancellationRequested) {
14531560
options?.generating?.cancel();
14541561
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)