From c58ec7b3c1052fc52b1b20415b81b8d7d11a4582 Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Tue, 8 Jul 2025 19:48:10 +0200 Subject: [PATCH 1/2] Adds AI feedback support for changelog markdown editors Enables the use of helpful/unhelpful AI feedback buttons in untitled markdown editors generated by the changelog command. Tracks feedback context for these documents, updates relevant context keys, and ensures telemetry compatibility. Also improves feedback instructions in generated changelogs to encourage user interaction. Updates telemetry and menu configuration to support this new AI feedback capability for changelog editors. (#4449, #4479) --- contributions.json | 20 ++++++ docs/telemetry-events.md | 2 +- package.json | 88 ++++++++++++++++---------- src/commands/generateChangelog.ts | 64 ++++++++++++++++++- src/constants.context.ts | 1 + src/constants.telemetry.ts | 1 + src/plus/ai/utils/-webview/ai.utils.ts | 32 +++++++--- 7 files changed, 160 insertions(+), 48 deletions(-) diff --git a/contributions.json b/contributions.json index ee0b5382a6f5d..2743b6aba19d8 100644 --- a/contributions.json +++ b/contributions.json @@ -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 } ] } @@ -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 } ] } @@ -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 } ] } @@ -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 } ] } diff --git a/docs/telemetry-events.md b/docs/telemetry-events.md index ff5ef7cd92aaa..448d25053af59 100644 --- a/docs/telemetry-events.md +++ b/docs/telemetry-events.md @@ -1731,7 +1731,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' } ``` diff --git a/package.json b/package.json index 84336aad206e4..0d40f507ca8e3 100644 --- a/package.json +++ b/package.json @@ -14124,15 +14124,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", @@ -14149,37 +14154,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", @@ -14203,16 +14177,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", @@ -14243,6 +14243,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" diff --git a/src/commands/generateChangelog.ts b/src/commands/generateChangelog.ts index b2c1e73a755e1..5abcdb7ac88bb 100644 --- a/src/commands/generateChangelog.ts +++ b/src/commands/generateChangelog.ts @@ -1,4 +1,4 @@ -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'; @@ -6,9 +6,13 @@ 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'; @@ -21,6 +25,36 @@ export interface GenerateChangelogCommandArgs { source?: Source; } +// Storage for AI feedback context associated with changelog documents +const changelogFeedbackContexts = new Map(); +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(); +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) { @@ -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 @@ -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); } diff --git a/src/constants.context.ts b/src/constants.context.ts index f95e5ded5226a..9783a7ae70114 100644 --- a/src/constants.context.ts +++ b/src/constants.context.ts @@ -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[]; diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index a906ed0de1c0d..728901b406562 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -1037,6 +1037,7 @@ export type Sources = | 'account' | 'ai' | 'ai:markdown-preview' + | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' diff --git a/src/plus/ai/utils/-webview/ai.utils.ts b/src/plus/ai/utils/-webview/ai.utils.ts index 339b7538410df..34d00b7522638 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 { getChangelogFeedbackContext } from '../../../../commands/generateChangelog'; import { Schemes } from '../../../../constants'; import type { AIProviders } from '../../../../constants.ai'; import type { Container } from '../../../../container'; @@ -283,16 +284,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(authority); + return metadata.context; + } catch (ex) { + Logger.error(ex, 'extractResultContext'); + return undefined; + } + } - try { - const metadata = decodeGitLensRevisionUriAuthority(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; } From 91f9b4af37b65c2d7e3d675aa4642cc0aa12783f Mon Sep 17 00:00:00 2001 From: Sergei Shmakov Date: Wed, 9 Jul 2025 16:19:21 +0200 Subject: [PATCH 2/2] Updates CHANGELOG: mentions the new feedback buttons to AI-generated Changelog feature (#4449, #4479) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c33980cb837c..ec477701b4b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Adds a new "Favorited Branches" option to the branches visibility dropdown on the _Commit Graph_ - Adds _Add to Favorites_ or _Remove from Favorites_ context menu items to branches in the _Commit Graph_ - Adds _Add to Favorites_ or _Remove from Favorites_ context menu items to worktrees in the views +- Adds 👍 "Helpful" and 👎 "Unhelpful" feedback buttons to AI-generated Changelog ([#4449](https://github.com/gitkraken/vscode-gitlens/issues/4449)) ### Changed