diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b7f735851b3..780695516406f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/contributions.json b/contributions.json index 54b615796e633..6008ed1abe712 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 d0a1a72a0756b..f32ca5c1681fc 100644 --- a/docs/telemetry-events.md +++ b/docs/telemetry-events.md @@ -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' } ``` diff --git a/package.json b/package.json index 5d43caf2d87c7..369db81b864d0 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", @@ -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", @@ -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" 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 7ab3fed1318e2..aa2d496e00e20 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -1036,6 +1036,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 18ce3f1696509..10ad0cd7c1288 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'; @@ -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(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; }