Skip to content

Commit 3262625

Browse files
committed
Adds AI feedback toolbar actions to markdown previews
Introduces dedicated "Helpful" and "Not helpful" toolbar commands for AI-generated markdown previews, allowing users to quickly provide positive or negative feedback directly from the editor UI. Feedback context is extracted from document metadata when telemetry is enabled, improving the feedback workflow and user experience for AI-assisted features. (#4449)
1 parent ff3858f commit 3262625

File tree

11 files changed

+377
-4
lines changed

11 files changed

+377
-4
lines changed

contributions.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,32 @@
143143
]
144144
}
145145
},
146+
"gitlens.ai.feedback.negative": {
147+
"label": "AI Feedback: Negative",
148+
"icon": "$(thumbsdown)",
149+
"menus": {
150+
"editor/title": [
151+
{
152+
"when": "resourceScheme == gitlens-markdown && config.gitlens.telemetry.enabled && activeCustomEditorId == vscode.markdown.preview.editor",
153+
"group": "navigation",
154+
"order": 2
155+
}
156+
]
157+
}
158+
},
159+
"gitlens.ai.feedback.positive": {
160+
"label": "AI Feedback: Positive",
161+
"icon": "$(thumbsup)",
162+
"menus": {
163+
"editor/title": [
164+
{
165+
"when": "resourceScheme == gitlens-markdown && config.gitlens.telemetry.enabled && activeCustomEditorId == vscode.markdown.preview.editor",
166+
"group": "navigation",
167+
"order": 1
168+
}
169+
]
170+
}
171+
},
146172
"gitlens.ai.generateChangelog": {
147173
"label": "Generate Changelog (Preview)...",
148174
"commandPalette": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"

docs/telemetry-events.md

Lines changed: 36 additions & 2 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6192,6 +6192,16 @@
61926192
"title": "Explain Working Changes (Preview)",
61936193
"icon": "$(sparkle)"
61946194
},
6195+
{
6196+
"command": "gitlens.ai.feedback.negative",
6197+
"title": "AI Feedback: Negative",
6198+
"icon": "$(thumbsdown)"
6199+
},
6200+
{
6201+
"command": "gitlens.ai.feedback.positive",
6202+
"title": "AI Feedback: Positive",
6203+
"icon": "$(thumbsup)"
6204+
},
61956205
{
61966206
"command": "gitlens.ai.generateChangelog",
61976207
"title": "Generate Changelog (Preview)...",
@@ -10722,6 +10732,14 @@
1072210732
"command": "gitlens.ai.explainWip:views",
1072310733
"when": "false"
1072410734
},
10735+
{
10736+
"command": "gitlens.ai.feedback.negative",
10737+
"when": "false"
10738+
},
10739+
{
10740+
"command": "gitlens.ai.feedback.positive",
10741+
"when": "false"
10742+
},
1072510743
{
1072610744
"command": "gitlens.ai.generateChangelog",
1072710745
"when": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled"
@@ -14118,6 +14136,16 @@
1411814136
"when": "resourceScheme == gitlens-markdown && activeCustomEditorId == vscode.markdown.preview.editor",
1411914137
"group": "navigation@0"
1412014138
},
14139+
{
14140+
"command": "gitlens.ai.feedback.positive",
14141+
"when": "resourceScheme == gitlens-markdown && config.gitlens.telemetry.enabled && activeCustomEditorId == vscode.markdown.preview.editor",
14142+
"group": "navigation@1"
14143+
},
14144+
{
14145+
"command": "gitlens.ai.feedback.negative",
14146+
"when": "resourceScheme == gitlens-markdown && config.gitlens.telemetry.enabled && activeCustomEditorId == vscode.markdown.preview.editor",
14147+
"group": "navigation@2"
14148+
},
1412114149
{
1412214150
"command": "gitlens.openPatch",
1412314151
"when": "false && editorLangId == diff"

src/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import './commands/addAuthors';
2+
import './commands/aiFeedback';
23
import './commands/browseRepoAtRevision';
34
import './commands/closeUnchangedFiles';
45
import './commands/cloudIntegrations';

src/commands/aiFeedback.ts

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import type { TextEditor, Uri } from 'vscode';
2+
import { window } from 'vscode';
3+
import { Schemes } from '../constants';
4+
import type { AIFeedbackEvent, Source } from '../constants.telemetry';
5+
import type { Container } from '../container';
6+
import type { MarkdownContentMetadata } from '../documents/markdown';
7+
import { decodeGitLensRevisionUriAuthority } from '../git/gitUri.authority';
8+
import { command } from '../system/-webview/command';
9+
import { Logger } from '../system/logger';
10+
import { ActiveEditorCommand } from './commandBase';
11+
import { getCommandUri } from './commandBase.utils';
12+
13+
export interface AIFeedbackContext {
14+
feature: AIFeedbackEvent['feature'];
15+
model: {
16+
id: string;
17+
providerId: string;
18+
providerName: string;
19+
};
20+
usage?: {
21+
promptTokens?: number;
22+
completionTokens?: number;
23+
totalTokens?: number;
24+
limits?: {
25+
used: number;
26+
limit: number;
27+
resetsOn: Date;
28+
};
29+
};
30+
outputLength: number;
31+
}
32+
33+
export interface AIFeedbackCommandArgs {
34+
context: AIFeedbackContext;
35+
}
36+
37+
function sendFeedbackEvent(
38+
container: Container,
39+
context: AIFeedbackContext,
40+
rating: 'positive' | 'negative',
41+
feedback: {
42+
presetReasons?: string[];
43+
writeInFeedback?: string;
44+
},
45+
source: Source,
46+
): void {
47+
const hasPresetReasons = feedback.presetReasons && feedback.presetReasons.length > 0;
48+
const writeInFeedback = feedback.writeInFeedback?.trim() ?? undefined;
49+
50+
let feedbackType: 'preset' | 'writeIn' | 'both';
51+
if (hasPresetReasons && writeInFeedback?.length) {
52+
feedbackType = 'both';
53+
} else if (hasPresetReasons) {
54+
feedbackType = 'preset';
55+
} else {
56+
feedbackType = 'writeIn';
57+
}
58+
59+
const eventData: AIFeedbackEvent = {
60+
feature: context.feature,
61+
rating: rating,
62+
feedbackType: feedbackType,
63+
presetReason: hasPresetReasons ? feedback.presetReasons!.join(', ') : undefined,
64+
'writeInFeedback.length': writeInFeedback?.length ?? undefined,
65+
'writeInFeedback.text': writeInFeedback?.length ? writeInFeedback : undefined,
66+
'model.id': context.model.id,
67+
'model.provider.id': context.model.providerId as any,
68+
'model.provider.name': context.model.providerName,
69+
'usage.promptTokens': context.usage?.promptTokens,
70+
'usage.completionTokens': context.usage?.completionTokens,
71+
'usage.totalTokens': context.usage?.totalTokens,
72+
'usage.limits.used': context.usage?.limits?.used,
73+
'usage.limits.limit': context.usage?.limits?.limit,
74+
'usage.limits.resetsOn': context.usage?.limits?.resetsOn?.toISOString(),
75+
'output.length': context.outputLength,
76+
};
77+
78+
container.telemetry.sendEvent('ai/feedback', eventData, source);
79+
}
80+
81+
@command()
82+
export class AIFeedbackPositiveCommand extends ActiveEditorCommand {
83+
constructor(private readonly container: Container) {
84+
super('gitlens.ai.feedback.positive');
85+
}
86+
87+
execute(editor?: TextEditor, uri?: Uri): void {
88+
const context = this.extractFeedbackContext(editor, uri);
89+
if (!context) return;
90+
91+
try {
92+
// For positive feedback, just send the event immediately without showing any form
93+
sendFeedbackEvent(
94+
this.container,
95+
context,
96+
'positive',
97+
{
98+
presetReasons: [],
99+
writeInFeedback: '',
100+
},
101+
{ source: 'command' },
102+
);
103+
104+
void window.showInformationMessage('Thank you for your feedback!');
105+
} catch (ex) {
106+
Logger.error(ex, 'AIFeedbackPositiveCommand', 'execute');
107+
}
108+
}
109+
110+
private extractFeedbackContext(editor?: TextEditor, uri?: Uri): AIFeedbackContext | undefined {
111+
uri = getCommandUri(uri, editor);
112+
if (uri?.scheme !== Schemes.GitLensMarkdown) return undefined;
113+
114+
const authority = uri.authority;
115+
if (!authority) return undefined;
116+
117+
try {
118+
const metadata = decodeGitLensRevisionUriAuthority<MarkdownContentMetadata>(authority);
119+
120+
// Extract feedback context from metadata
121+
if (metadata.feedbackContext) {
122+
return metadata.feedbackContext as unknown as AIFeedbackContext;
123+
}
124+
125+
return undefined;
126+
} catch (ex) {
127+
Logger.error(ex, 'AIFeedbackPositiveCommand', 'extractFeedbackContext');
128+
return undefined;
129+
}
130+
}
131+
}
132+
133+
@command()
134+
export class AIFeedbackNegativeCommand extends ActiveEditorCommand {
135+
constructor(private readonly container: Container) {
136+
super('gitlens.ai.feedback.negative');
137+
}
138+
139+
async execute(editor?: TextEditor, uri?: Uri): Promise<void> {
140+
const context = this.extractFeedbackContext(editor, uri);
141+
if (!context) return;
142+
143+
try {
144+
// For negative feedback, always show the detailed form directly
145+
await showDetailedFeedbackForm(this.container, context);
146+
} catch (ex) {
147+
Logger.error(ex, 'AIFeedbackNegativeCommand', 'execute');
148+
}
149+
}
150+
151+
private extractFeedbackContext(editor?: TextEditor, uri?: Uri): AIFeedbackContext | undefined {
152+
uri = getCommandUri(uri, editor);
153+
if (uri?.scheme !== Schemes.GitLensMarkdown) return undefined;
154+
155+
const authority = uri.authority;
156+
if (!authority) return undefined;
157+
158+
try {
159+
const metadata = decodeGitLensRevisionUriAuthority<MarkdownContentMetadata>(authority);
160+
161+
// Extract feedback context from metadata
162+
if (metadata.feedbackContext) {
163+
return metadata.feedbackContext as unknown as AIFeedbackContext;
164+
}
165+
166+
return undefined;
167+
} catch (ex) {
168+
Logger.error(ex, 'AIFeedbackNegativeCommand', 'extractFeedbackContext');
169+
return undefined;
170+
}
171+
}
172+
}
173+
174+
async function showDetailedFeedbackForm(container: Container, context: AIFeedbackContext): Promise<void> {
175+
const negativeReasons = [
176+
'Inaccurate or incorrect response',
177+
'Too generic or not specific enough',
178+
'Poor code quality',
179+
'Missing important details',
180+
'Difficult to understand',
181+
'Not relevant to my needs',
182+
];
183+
184+
// Show quick pick for preset reasons
185+
const selectedReasons = await window.showQuickPick(
186+
negativeReasons.map(reason => ({ label: reason, picked: false })),
187+
{
188+
title: 'What specifically could be improved?',
189+
canPickMany: true,
190+
placeHolder: 'Select all that apply (optional)',
191+
},
192+
);
193+
194+
// Show input box for additional feedback
195+
const writeInFeedback = await window.showInputBox({
196+
title: 'Additional feedback (optional)',
197+
placeHolder: 'Tell us more about your experience...',
198+
prompt: 'Your feedback helps us improve our AI features',
199+
});
200+
201+
// Always send feedback submission telemetry for negative feedback
202+
sendFeedbackEvent(
203+
container,
204+
context,
205+
'negative',
206+
{
207+
presetReasons: selectedReasons?.map(r => r.label),
208+
writeInFeedback: writeInFeedback,
209+
},
210+
{ source: 'command' },
211+
);
212+
213+
void window.showInformationMessage('Thank you for your feedback!');
214+
}

src/commands/explainBase.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { GitUri } from '../git/gitUri';
88
import type { AIExplainSource, AISummarizeResult } from '../plus/ai/aiProviderService';
99
import { getBestRepositoryOrShowPicker } from '../quickpicks/repositoryPicker';
1010
import { showMarkdownPreview } from '../system/-webview/markdown';
11+
import type { AIFeedbackContext } from './aiFeedback';
1112
import { GlCommandBase } from './commandBase';
1213
import { getCommandUri } from './commandBase.utils';
1314

src/commands/generateRebase.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { command, executeCommand } from '../system/-webview/command';
1717
import { showMarkdownPreview } from '../system/-webview/markdown';
1818
import { Logger } from '../system/logger';
1919
import { escapeMarkdownCodeBlocks } from '../system/markdown';
20+
import type { AIFeedbackContext } from './aiFeedback';
2021
import { GlCommandBase } from './commandBase';
2122
import type { CommandContext } from './commandContext';
2223
import {
@@ -349,8 +350,25 @@ export async function generateRebase(
349350
// Extract the diff information from the reorganized commits
350351
const diffInfo = extractRebaseDiffInfo(result.commits, result.diff, result.hunkMap);
351352

353+
// Create feedback context for telemetry
354+
const feedbackContext: AIFeedbackContext = {
355+
feature: 'generateRebase',
356+
model: {
357+
id: result.model.id,
358+
providerId: result.model.provider.id,
359+
providerName: result.model.provider.name,
360+
},
361+
usage: result.usage,
362+
outputLength: result.content?.length,
363+
};
364+
352365
// Generate the markdown content that shows each commit and its diffs
353-
const { content, metadata } = generateRebaseMarkdown(result, title);
366+
const { content, metadata } = generateRebaseMarkdown(
367+
result,
368+
title,
369+
feedbackContext,
370+
container.telemetry.enabled,
371+
);
354372

355373
let generateType: 'commits' | 'rebase' = 'rebase';
356374
let headRefSlug = head.ref;
@@ -522,15 +540,22 @@ export function extractRebaseDiffInfo(
522540
function generateRebaseMarkdown(
523541
result: AIRebaseResult,
524542
title = 'Rebase Commits',
543+
feedbackContext?: AIFeedbackContext,
544+
telemetryEnabled?: boolean,
525545
): { content: string; metadata: MarkdownContentMetadata } {
526-
const metadata = {
546+
const metadata: MarkdownContentMetadata = {
527547
header: {
528548
title: title,
529549
aiModel: result.model.name,
530550
subtitle: 'Explanation',
531551
},
532552
};
533553

554+
// Add feedback context and commands to toolbar if telemetry is enabled
555+
if (feedbackContext && telemetryEnabled) {
556+
metadata.feedbackContext = feedbackContext as unknown as Record<string, unknown>;
557+
}
558+
534559
let markdown = '';
535560
if (result.commits.length === 0) {
536561
markdown = 'No Commits Generated';
@@ -599,6 +624,13 @@ function generateRebaseMarkdown(
599624
markdown += explanations;
600625
markdown += changes;
601626

627+
// Add feedback note if context is provided and telemetry is enabled
628+
if (feedbackContext && telemetryEnabled) {
629+
markdown += '\n\n---\n\n## Feedback\n\n';
630+
markdown += 'Use the 👍 and 👎 buttons in the editor toolbar to provide feedback on this AI response.\n\n';
631+
markdown += '*Your feedback helps us improve our AI features.*';
632+
}
633+
602634
// markdown += `\n\n----\n\n## Raw commits\n\n\`\`\`${escapeMarkdownCodeBlocks(JSON.stringify(commits))}\`\`\``;
603635
// markdown += `\n\n----\n\n## Original Diff\n\n\`\`\`${escapeMarkdownCodeBlocks(originalDiff)}\`\`\`\n`;
604636

src/constants.commands.generated.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export type ContributedCommands =
1212
| 'gitlens.ai.explainStash:views'
1313
| 'gitlens.ai.explainWip:graph'
1414
| 'gitlens.ai.explainWip:views'
15+
| 'gitlens.ai.feedback.negative'
16+
| 'gitlens.ai.feedback.positive'
1517
| 'gitlens.ai.generateChangelog:views'
1618
| 'gitlens.ai.generateChangelogFrom:graph'
1719
| 'gitlens.ai.generateChangelogFrom:views'

0 commit comments

Comments
 (0)