Skip to content

Adds AI feedback support for changelog markdown editors #4479

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

## [Unreleased]

### Added

- Adds 👍 "Helpful" and 👎 "Unhelpful" feedback buttons to AI-generated Changelog ([#4449](https://github.com/gitkraken/vscode-gitlens/issues/4449))

### Changed

- Changes branch creation to avoid setting an upstream branch if the new branch name and remote branch name don't match ([#4477](https://github.com/gitkraken/vscode-gitlens/issues/4477))
Expand Down
20 changes: 20 additions & 0 deletions contributions.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@
"when": "resourceScheme == gitlens-ai-markdown && resource not in gitlens:tabs:ai:helpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && activeCustomEditorId == vscode.markdown.preview.editor",
"group": "navigation",
"order": 1
},
{
"when": "resourceScheme == untitled && resource in gitlens:tabs:ai:changelog && resource not in gitlens:tabs:ai:helpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && resourceLangId == markdown",
"group": "navigation",
"order": 1
}
]
}
Expand All @@ -165,6 +170,11 @@
"when": "resourceScheme == gitlens-ai-markdown && resource in gitlens:tabs:ai:helpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && activeCustomEditorId == vscode.markdown.preview.editor",
"group": "navigation",
"order": 1
},
{
"when": "resourceScheme == untitled && resource in gitlens:tabs:ai:changelog && resource in gitlens:tabs:ai:helpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && resourceLangId == markdown",
"group": "navigation",
"order": 1
}
]
}
Expand All @@ -178,6 +188,11 @@
"when": "resourceScheme == gitlens-ai-markdown && resource not in gitlens:tabs:ai:unhelpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && activeCustomEditorId == vscode.markdown.preview.editor",
"group": "navigation",
"order": 2
},
{
"when": "resourceScheme == untitled && resource in gitlens:tabs:ai:changelog && resource not in gitlens:tabs:ai:unhelpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && resourceLangId == markdown",
"group": "navigation",
"order": 2
}
]
}
Expand All @@ -191,6 +206,11 @@
"when": "resourceScheme == gitlens-ai-markdown && resource in gitlens:tabs:ai:unhelpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && activeCustomEditorId == vscode.markdown.preview.editor",
"group": "navigation",
"order": 2
},
{
"when": "resourceScheme == untitled && resource in gitlens:tabs:ai:changelog && resource in gitlens:tabs:ai:unhelpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && resourceLangId == markdown",
"group": "navigation",
"order": 1
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion docs/telemetry-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -1730,7 +1730,7 @@ void
'repoPrivacy': 'private' | 'public' | 'local',
'repository.visibility': 'private' | 'public' | 'local',
// Provided for compatibility with other GK surfaces
'source': 'account' | 'subscription' | 'graph' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'code-suggest' | 'ai' | 'ai:markdown-preview' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees'
'source': 'account' | 'subscription' | 'graph' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'code-suggest' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees'
}
```

Expand Down
88 changes: 54 additions & 34 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14055,15 +14055,20 @@
}
],
"editor/title": [
{
"command": "gitlens.graph.refresh",
"when": "activeWebviewPanelId === gitlens.graph",
"group": "navigation@-99"
},
{
"command": "gitlens.timeline.refresh",
"when": "activeWebviewPanelId === gitlens.timeline",
"group": "navigation@-99"
},
{
"submenu": "gitlens/graph/configuration",
"when": "activeWebviewPanelId === gitlens.graph",
"group": "navigation@-98"
"command": "gitlens.graph.split",
"when": "activeWebviewPanelId == gitlens.graph && resourceScheme == webview-panel && config.gitlens.graph.allowMultiple",
"group": "navigation@-97"
},
{
"command": "gitlens.timeline.split",
Expand All @@ -14080,37 +14085,6 @@
"when": "resource in gitlens:tabs:blameable && (gitlens:window:annotated == computing || resource in gitlens:tabs:annotated:computing) && config.gitlens.menus.editorGroup.blame",
"group": "navigation@100"
},
{
"command": "gitlens.diffWithPrevious",
"when": "resource in gitlens:tabs:tracked && config.gitlens.menus.editorGroup.compare",
"group": "navigation@97",
"alt": "gitlens.diffWithRevision"
},
{
"command": "gitlens.showQuickRevisionDetails",
"when": "resource in gitlens:tabs:tracked && config.gitlens.menus.editorGroup.compare",
"group": "navigation@98"
},
{
"command": "gitlens.diffWithNext",
"when": "resource in gitlens:tabs:tracked && config.gitlens.menus.editorGroup.compare",
"group": "navigation@99"
},
{
"command": "gitlens.diffWithWorking",
"when": "resourceScheme =~ /^(gitlens|pr)$/ && gitlens:enabled",
"group": "navigation@-99"
},
{
"command": "gitlens.graph.refresh",
"when": "activeWebviewPanelId === gitlens.graph",
"group": "navigation@-99"
},
{
"command": "gitlens.graph.split",
"when": "activeWebviewPanelId == gitlens.graph && resourceScheme == webview-panel && config.gitlens.graph.allowMultiple",
"group": "navigation@-97"
},
{
"command": "gitlens.toggleFileBlame",
"when": "resource in gitlens:tabs:blameable && resource not in gitlens:tabs:annotated && config.gitlens.menus.editorGroup.blame && config.gitlens.fileAnnotations.command == blame",
Expand All @@ -14134,16 +14108,42 @@
"when": "resource in gitlens:tabs:blameable && resource not in gitlens:tabs:annotated && !gitlens:window:annotated && config.gitlens.menus.editorGroup.blame && !config.gitlens.fileAnnotations.command",
"group": "navigation@100"
},
{
"command": "gitlens.diffWithPrevious",
"when": "resource in gitlens:tabs:tracked && config.gitlens.menus.editorGroup.compare",
"group": "navigation@97",
"alt": "gitlens.diffWithRevision"
},
{
"command": "gitlens.showQuickRevisionDetails",
"when": "resource in gitlens:tabs:tracked && config.gitlens.menus.editorGroup.compare",
"group": "navigation@98"
},
{
"command": "gitlens.diffWithNext",
"when": "resource in gitlens:tabs:tracked && config.gitlens.menus.editorGroup.compare",
"group": "navigation@99"
},
{
"command": "gitlens.openWorkingFile",
"when": "resourceScheme == git && gitlens:enabled && !isInDiffEditor",
"group": "navigation@-98"
},
{
"command": "gitlens.diffWithWorking",
"when": "resourceScheme =~ /^(gitlens|pr)$/ && gitlens:enabled",
"group": "navigation@-99"
},
{
"command": "gitlens.openWorkingFile",
"when": "resourceScheme =~ /^(gitlens|pr)$/ && gitlens:enabled",
"group": "navigation@-98"
},
{
"submenu": "gitlens/graph/configuration",
"when": "activeWebviewPanelId === gitlens.graph",
"group": "navigation@-98"
},
{
"command": "gitlens.openRevisionFile",
"when": "resourceScheme =~ /^(gitlens|pr)$/ && gitlens:enabled && isInDiffEditor",
Expand Down Expand Up @@ -14174,6 +14174,26 @@
"when": "resourceScheme == gitlens-ai-markdown && activeCustomEditorId == vscode.markdown.preview.editor && resourcePath =~ /^\\/explain\\//",
"group": "navigation@3"
},
{
"command": "gitlens.ai.feedback.helpful",
"when": "resourceScheme == untitled && resource in gitlens:tabs:ai:changelog && resource not in gitlens:tabs:ai:helpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && resourceLangId == markdown",
"group": "navigation@1"
},
{
"command": "gitlens.ai.feedback.helpful.chosen",
"when": "resourceScheme == untitled && resource in gitlens:tabs:ai:changelog && resource in gitlens:tabs:ai:helpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && resourceLangId == markdown",
"group": "navigation@1"
},
{
"command": "gitlens.ai.feedback.unhelpful.chosen",
"when": "resourceScheme == untitled && resource in gitlens:tabs:ai:changelog && resource in gitlens:tabs:ai:unhelpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && resourceLangId == markdown",
"group": "navigation@1"
},
{
"command": "gitlens.ai.feedback.unhelpful",
"when": "resourceScheme == untitled && resource in gitlens:tabs:ai:changelog && resource not in gitlens:tabs:ai:unhelpful && config.gitlens.telemetry.enabled && config.telemetry.telemetryLevel != off && resourceLangId == markdown",
"group": "navigation@2"
},
{
"command": "gitlens.openPatch",
"when": "false && editorLangId == diff"
Expand Down
64 changes: 61 additions & 3 deletions src/commands/generateChangelog.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { CancellationToken, ProgressOptions } from 'vscode';
import type { CancellationToken, ProgressOptions, Uri } from 'vscode';
import { ProgressLocation, window, workspace } from 'vscode';
import type { Source } from '../constants.telemetry';
import type { Container } from '../container';
import type { GitReference } from '../git/models/reference';
import { getChangesForChangelog } from '../git/utils/-webview/log.utils';
import { createRevisionRange, shortenRevision } from '../git/utils/revision.utils';
import { showGenericErrorMessage } from '../messages';
import type { AIGenerateChangelogChanges } from '../plus/ai/aiProviderService';
import type { AIGenerateChangelogChanges, AIResultContext } from '../plus/ai/aiProviderService';
import { getAIResultContext } from '../plus/ai/utils/-webview/ai.utils';
import { showComparisonPicker } from '../quickpicks/comparisonPicker';
import { command } from '../system/-webview/command';
import { setContext } from '../system/-webview/context';
import type { Deferrable } from '../system/function/debounce';
import { debounce } from '../system/function/debounce';
import type { Lazy } from '../system/lazy';
import { lazy } from '../system/lazy';
import { Logger } from '../system/logger';
Expand All @@ -21,6 +25,36 @@ export interface GenerateChangelogCommandArgs {
source?: Source;
}

// Storage for AI feedback context associated with changelog documents
const changelogFeedbackContexts = new Map<string, AIResultContext>();
export function getChangelogFeedbackContext(documentUri: string): AIResultContext | undefined {
return changelogFeedbackContexts.get(documentUri);
}
function setChangelogFeedbackContext(documentUri: string, context: AIResultContext): void {
changelogFeedbackContexts.set(documentUri, context);
}
function clearChangelogFeedbackContext(documentUri: string): void {
changelogFeedbackContexts.delete(documentUri);
}

// Storage for changelog document URIs
const changelogUris = new Set<Uri>();
let _updateChangelogContextDebounced: Deferrable<() => void> | undefined;
function updateChangelogContext(): void {
_updateChangelogContextDebounced ??= debounce(() => {
void setContext('gitlens:tabs:ai:changelog', [...changelogUris]);
}, 100);
_updateChangelogContextDebounced();
}
function addChangelogUri(uri: Uri): void {
changelogUris.add(uri);
updateChangelogContext();
}
function removeChangelogUri(uri: Uri): void {
changelogUris.delete(uri);
updateChangelogContext();
}

@command()
export class GenerateChangelogCommand extends GlCommandBase {
constructor(private readonly container: Container) {
Expand Down Expand Up @@ -98,12 +132,22 @@ export async function generateChangelogAndOpenMarkdownDocument(
if (result === 'cancelled') return;

const { range, changes: { length: count } = [] } = await changes.value;
const feedbackContext = result && getAIResultContext(result);

let content = `# Changelog for ${range.head.label ?? range.head.ref}\n`;
if (result != null) {
content += `> Generated by ${result.model.name} from ${pluralize('commit', count)} between ${
range.head.label ?? range.head.ref
} and ${range.base.label ?? range.base.ref}\n\n----\n\n${result.content}\n`;
} and ${range.base.label ?? range.base.ref}\n`;

// Add feedback note if telemetry is enabled
if (feedbackContext && container.telemetry.enabled) {
content += '\n\n';
content += 'Use the 👍 and 👎 buttons in the editor toolbar to provide feedback on this AI response. ';
content += '*Your feedback helps us improve our AI features.*';
}

content += `\n\n----\n\n${result.content}\n`;
} else {
content += `> No changes found between ${range.head.label ?? range.head.ref} and ${
range.base.label ?? range.base.ref
Expand All @@ -112,5 +156,19 @@ export async function generateChangelogAndOpenMarkdownDocument(

// open an untitled editor
const document = await workspace.openTextDocument({ language: 'markdown', content: content });
if (feedbackContext) {
// Store feedback context for this document
setChangelogFeedbackContext(document.uri.toString(), feedbackContext);
// Add to changelog URIs context even for no-results documents
addChangelogUri(document.uri);
// Clean up context when document is closed
const disposable = workspace.onDidCloseTextDocument(closedDoc => {
if (closedDoc.uri.toString() === document.uri.toString()) {
clearChangelogFeedbackContext(document.uri.toString());
removeChangelogUri(document.uri);
disposable.dispose();
}
});
}
await window.showTextDocument(document);
}
1 change: 1 addition & 0 deletions src/constants.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type ContextKeys = {
'gitlens:schemes:trackable': string[];
'gitlens:tabs:ai:helpful': Uri[];
'gitlens:tabs:ai:unhelpful': Uri[];
'gitlens:tabs:ai:changelog': Uri[];
'gitlens:tabs:annotated': Uri[];
'gitlens:tabs:annotated:computing': Uri[];
'gitlens:tabs:blameable': Uri[];
Expand Down
1 change: 1 addition & 0 deletions src/constants.telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,7 @@ export type Sources =
| 'account'
| 'ai'
| 'ai:markdown-preview'
| 'ai:markdown-editor'
| 'ai:picker'
| 'associateIssueWithBranch'
| 'cloud-patches'
Expand Down
32 changes: 22 additions & 10 deletions src/plus/ai/utils/-webview/ai.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Disposable, QuickInputButton } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import { getChangelogFeedbackContext } from '../../../../commands/generateChangelog';
import { Schemes } from '../../../../constants';
import type { AIProviders } from '../../../../constants.ai';
import type { Container } from '../../../../container';
Expand Down Expand Up @@ -282,16 +283,27 @@ export function getAIResultContext(result: AIResult): AIResultContext {
}

export function extractAIResultContext(uri: Uri | undefined): AIResultContext | undefined {
if (uri?.scheme !== Schemes.GitLensAIMarkdown) return undefined;

const { authority } = uri;
if (!authority) return undefined;
if (uri?.scheme === Schemes.GitLensAIMarkdown) {
const { authority } = uri;
if (!authority) return undefined;

try {
const metadata = decodeGitLensRevisionUriAuthority<MarkdownContentMetadata>(authority);
return metadata.context;
} catch (ex) {
Logger.error(ex, 'extractResultContext');
return undefined;
}
}

try {
const metadata = decodeGitLensRevisionUriAuthority<MarkdownContentMetadata>(authority);
return metadata.context;
} catch (ex) {
Logger.error(ex, 'extractResultContext');
return undefined;
// Check for untitled documents with stored changelog feedback context
if (uri?.scheme === 'untitled') {
try {
return getChangelogFeedbackContext(uri.toString());
} catch {
return undefined;
}
}

return undefined;
}