From d097bc4a4b303371b3c9e5dbc1accd4ed9c8057e Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 25 Nov 2025 11:01:07 -0700 Subject: [PATCH 01/11] Add maximize command for Composer webview --- contributions.json | 13 ++++++++ package.json | 9 ++++++ src/constants.commands.generated.ts | 1 + src/constants.commands.ts | 2 ++ src/webviews/plus/composer/composerWebview.ts | 30 +++++++++++++++++++ src/webviews/plus/composer/registration.ts | 1 + src/webviews/webviewController.ts | 6 ++++ src/webviews/webviewsController.ts | 4 +++ 8 files changed, 66 insertions(+) diff --git a/contributions.json b/contributions.json index facda5aadcd20..0c7b28cfefcbd 100644 --- a/contributions.json +++ b/contributions.json @@ -612,6 +612,19 @@ ] } }, + "gitlens.composer.maximize": { + "label": "Maximize", + "icon": "$(screen-full)", + "menus": { + "editor/title": [ + { + "when": "activeWebviewPanelId === gitlens.composer", + "group": "navigation", + "order": -98 + } + ] + } + }, "gitlens.composer.refresh": { "label": "Refresh", "icon": "$(refresh)", diff --git a/package.json b/package.json index ff5b4fd022204..d40e175885020 100644 --- a/package.json +++ b/package.json @@ -6511,6 +6511,11 @@ "title": "Compose Commits (Preview)...", "icon": "$(sparkle)" }, + { + "command": "gitlens.composer.maximize", + "title": "Maximize", + "icon": "$(screen-full)" + }, { "command": "gitlens.composer.refresh", "title": "Refresh", @@ -11767,6 +11772,10 @@ "command": "gitlens.composeCommits:views", "when": "false" }, + { + "command": "gitlens.composer.maximize", + "when": "false" + }, { "command": "gitlens.composer.refresh", "when": "false" diff --git a/src/constants.commands.generated.ts b/src/constants.commands.generated.ts index 21568b2cc6370..023cd665d6b30 100644 --- a/src/constants.commands.generated.ts +++ b/src/constants.commands.generated.ts @@ -37,6 +37,7 @@ export type ContributedCommands = | 'gitlens.composeCommits:graph' | 'gitlens.composeCommits:scm' | 'gitlens.composeCommits:views' + | 'gitlens.composer.maximize' | 'gitlens.composer.refresh' | 'gitlens.computingFileAnnotations' | 'gitlens.connectRemoteProvider' diff --git a/src/constants.commands.ts b/src/constants.commands.ts index 737d3ad49f7fa..024fc3ba343b0 100644 --- a/src/constants.commands.ts +++ b/src/constants.commands.ts @@ -206,6 +206,8 @@ export type CoreCommands = | 'workbench.action.openSettings' | 'workbench.action.openWalkthrough' | 'workbench.action.toggleMaximizedPanel' + | 'workbench.action.focusPanel' + | 'workbench.action.togglePanel' | 'workbench.extensions.action.switchToRelease' | 'workbench.extensions.installExtension' | 'workbench.extensions.uninstallExtension' diff --git a/src/webviews/plus/composer/composerWebview.ts b/src/webviews/plus/composer/composerWebview.ts index 18603fc4289f0..429d5a2d3ae3a 100644 --- a/src/webviews/plus/composer/composerWebview.ts +++ b/src/webviews/plus/composer/composerWebview.ts @@ -15,6 +15,7 @@ import { getBranchMergeTargetName } from '../../../git/utils/-webview/branch.uti import { sendFeedbackEvent, showUnhelpfulFeedbackPicker } from '../../../plus/ai/aiFeedbackUtils'; import type { AIModelChangeEvent } from '../../../plus/ai/aiProviderService'; import { getRepositoryPickerTitleAndPlaceholder, showRepositoryPicker } from '../../../quickpicks/repositoryPicker'; +import { executeCoreCommand } from '../../../system/-webview/command'; import { configuration } from '../../../system/-webview/configuration'; import { getContext, onDidChangeContext } from '../../../system/-webview/context'; import { getSettledValue } from '../../../system/promise'; @@ -1611,4 +1612,33 @@ export class ComposerWebviewProvider implements WebviewProvider { + if (this._isMaximized) { + // Restore panel if it was previously visible + if (this._panelWasVisible) { + await executeCoreCommand('workbench.action.togglePanel'); + } + this._isMaximized = false; + this._panelWasVisible = undefined; + } else { + // Check panel visibility by querying the workbench state + // We'll use a workaround: check if the panel is focused + try { + // Try to focus the panel - if it succeeds, panel was visible + await executeCoreCommand('workbench.action.focusPanel'); + this._panelWasVisible = true; + // Now hide it + await executeCoreCommand('workbench.action.togglePanel'); + } catch { + // If focusing failed, panel wasn't visible + this._panelWasVisible = false; + } + + this._isMaximized = true; + } + } } diff --git a/src/webviews/plus/composer/registration.ts b/src/webviews/plus/composer/registration.ts index 5fdd0e3bb0ca2..d096f996cc109 100644 --- a/src/webviews/plus/composer/registration.ts +++ b/src/webviews/plus/composer/registration.ts @@ -52,5 +52,6 @@ export function registerComposerWebviewCommands( ): Disposable { return Disposable.from( registerCommand(`${panels.id}.refresh`, () => void panels.getActiveInstance()?.refresh(true)), + registerCommand(`${panels.id}.maximize`, () => void (panels.getActiveInstance() as any)?.maximize()), ); } diff --git a/src/webviews/webviewController.ts b/src/webviews/webviewController.ts index 5cb5a68b9f49a..c9aa3743f20e3 100644 --- a/src/webviews/webviewController.ts +++ b/src/webviews/webviewController.ts @@ -876,6 +876,12 @@ export class WebviewController< } } } + + async maximize(): Promise { + if (this.provider && 'maximize' in this.provider && typeof this.provider.maximize === 'function') { + await this.provider.maximize(); + } + } } export function replaceWebviewHtmlTokens( diff --git a/src/webviews/webviewsController.ts b/src/webviews/webviewsController.ts index a13c24d1d7175..e5dec01ec3ebf 100644 --- a/src/webviews/webviewsController.ts +++ b/src/webviews/webviewsController.ts @@ -65,6 +65,7 @@ export interface WebviewPanelProxy< ): boolean | undefined; close(): void; refresh(force?: boolean): Promise; + maximize(): Promise; show(options?: WebviewPanelShowOptions, ...args: WebviewShowingArgs): Promise; } @@ -501,6 +502,9 @@ function convertToWebviewPanelProxy< show: function (options?: WebviewPanelShowOptions, ...args: WebviewShowingArgs) { return controller.show(false, options, ...args); }, + maximize: function () { + return controller.maximize(); + }, }; } From dbbdb3ba263b366ae285da8755d97322f57adb26 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 25 Nov 2025 11:01:07 -0700 Subject: [PATCH 02/11] Reorganize editor title menu items --- package.json | 57 ++++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index d40e175885020..8f57a38df436b 100644 --- a/package.json +++ b/package.json @@ -15540,25 +15540,20 @@ "when": "activeWebviewPanelId === gitlens.composer", "group": "navigation@-99" }, - { - "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", + "command": "gitlens.composer.maximize", + "when": "activeWebviewPanelId === gitlens.composer", "group": "navigation@-98" }, { - "command": "gitlens.graph.split", - "when": "activeWebviewPanelId == gitlens.graph && resourceScheme == webview-panel && config.gitlens.graph.allowMultiple", - "group": "navigation@-97" + "submenu": "gitlens/graph/configuration", + "when": "activeWebviewPanelId === gitlens.graph", + "group": "navigation@-98" }, { "command": "gitlens.timeline.split", @@ -15587,6 +15582,32 @@ "group": "navigation@100", "alt": "gitlens.toggleFileBlame:editor/title" }, + { + "command": "gitlens.diffWithPrevious:editor/title", + "when": "resource in gitlens:tabs:tracked && config.gitlens.menus.editorGroup.compare", + "group": "navigation@97", + "alt": "gitlens.diffWithRevision" + }, + { + "command": "gitlens.diffWithNext:editor/title", + "when": "resource in gitlens:tabs:tracked && config.gitlens.menus.editorGroup.compare", + "group": "navigation@99" + }, + { + "command": "gitlens.diffWithWorking:editor/title", + "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.toggleFileHeatmap:editor/title", "when": "resource in gitlens:tabs:blameable && resource not in gitlens:tabs:annotated && config.gitlens.menus.editorGroup.blame && config.gitlens.fileAnnotations.command == heatmap", @@ -15598,32 +15619,16 @@ "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:editor/title", - "when": "resource in gitlens:tabs:tracked && config.gitlens.menus.editorGroup.compare", - "group": "navigation@97", - "alt": "gitlens.diffWithRevision" - }, { "command": "gitlens.showQuickRevisionDetails:editor/title", "when": "resource in gitlens:tabs:tracked && config.gitlens.menus.editorGroup.compare", "group": "navigation@98" }, - { - "command": "gitlens.diffWithNext:editor/title", - "when": "resource in gitlens:tabs:tracked && config.gitlens.menus.editorGroup.compare", - "group": "navigation@99" - }, { "command": "gitlens.openWorkingFile:editor/title", "when": "resourceScheme == git && gitlens:enabled && !isInDiffEditor", "group": "navigation@-98" }, - { - "command": "gitlens.diffWithWorking:editor/title", - "when": "resourceScheme =~ /^(gitlens|pr)$/ && gitlens:enabled", - "group": "navigation@-99" - }, { "command": "gitlens.openWorkingFile:editor/title", "when": "resourceScheme =~ /^(gitlens|pr)$/ && gitlens:enabled", From 14df6990ebda0c720f9c583d79233f9c46067421 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 25 Nov 2025 11:01:08 -0700 Subject: [PATCH 03/11] Add workbench panel visibility configuration type --- src/config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config.ts b/src/config.ts index ae3c0a97db6a4..98475281cfea4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1121,6 +1121,9 @@ export type CoreConfig = { }; readonly workbench: { readonly editorAssociations: Record | { viewType: string; filenamePattern: string }[]; + readonly panel: { + readonly visible: boolean; + }; readonly tree: { readonly renderIndentGuides: 'always' | 'none' | 'onHover'; readonly indent: number; From 0cbc6175686395cd3f24eab904784c682abf9d45 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 25 Nov 2025 11:01:08 -0700 Subject: [PATCH 04/11] Refactor commit message from string to structured object --- .../apps/plus/composer/components/app.ts | 22 ++++++++++++------- .../plus/composer/components/commits-panel.ts | 6 +++-- .../apps/plus/composer/stateProvider.ts | 4 +++- src/webviews/plus/composer/composerWebview.ts | 6 ++--- src/webviews/plus/composer/mockData.ts | 6 ++--- src/webviews/plus/composer/protocol.ts | 7 +++++- .../plus/composer/utils/composer.utils.ts | 6 ++--- 7 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/webviews/apps/plus/composer/components/app.ts b/src/webviews/apps/plus/composer/components/app.ts index d580370759b42..6475cfacc0243 100644 --- a/src/webviews/apps/plus/composer/components/app.ts +++ b/src/webviews/apps/plus/composer/components/app.ts @@ -54,7 +54,7 @@ interface ComposerDataSnapshot { commits: ComposerCommit[]; selectedCommitId: string | null; selectedCommitIds: Set; - selectedUnassignedSection: string | null; + selectedUnassignedSection: 'staged' | 'unstaged' | 'unassigned' | null; selectedHunkIds: Set; hasUsedAutoCompose: boolean; recompose: { enabled: boolean; branchName?: string; locked: boolean; commitShas?: string[] } | null; @@ -834,7 +834,7 @@ export class ComposerApp extends LitElement { // Create new commit const newCommit: ComposerCommit = { id: `commit-${Date.now()}`, - message: '', // Empty message - user will add their own + message: { content: '', isGenerated: false }, hunkIndices: hunkIndices, }; @@ -1070,7 +1070,7 @@ export class ComposerApp extends LitElement { this.commitMessageBeingEdited = null; }, 1000); - commit.message = message; + commit.message = { content: message, isGenerated: false }; this.requestUpdate(); } } @@ -1241,7 +1241,10 @@ export class ComposerApp extends LitElement { } private get isReadyToFinishAndCommit(): boolean { - return this.state.commits.length > 0 && this.state.commits.every(commit => commit.message.trim().length > 0); + return ( + this.state.commits.length > 0 && + this.state.commits.every(commit => commit.message.content.trim().length > 0) + ); } private get canGenerateCommitsWithAI(): boolean { @@ -1334,7 +1337,7 @@ export class ComposerApp extends LitElement { // Create Commits loading dialog if (this.state.committing) { - const commitCount = this.state.commits.filter(c => c.message.trim() !== '').length; + const commitCount = this.state.commits.filter(c => c.message.content.trim() !== '').length; return this.renderLoadingDialog( 'Creating Commits', `Committing ${commitCount} commit${commitCount === 1 ? '' : 's'}.`, @@ -1538,7 +1541,7 @@ export class ComposerApp extends LitElement { this._ipc.sendCommand(GenerateCommitMessageCommand, { commitId: commitId, commitHunkIndices: commit.hunkIndices, - overwriteExistingMessage: commit.message.trim() !== '', + overwriteExistingMessage: commit.message.content.trim() !== '', }); } @@ -1557,7 +1560,7 @@ export class ComposerApp extends LitElement { // Combine commit messages from selected commits const combinedMessage = selectedCommits - .map(commit => commit.message) + .map(commit => commit.message.content) .filter(message => message && message.trim() !== '') .join('\n\n'); @@ -1567,10 +1570,13 @@ export class ComposerApp extends LitElement { .filter(explanation => explanation && explanation.trim() !== '') .join('\n\n'); + // Determine if any of the combined commits were AI-generated + const isGenerated = selectedCommits.some(commit => commit.message.isGenerated); + // Create new combined commit const combinedCommit: ComposerCommit = { id: `commit-${Date.now()}`, - message: combinedMessage || 'Combined commit', + message: { content: combinedMessage || 'Combined commit', isGenerated: isGenerated }, hunkIndices: combinedHunkIndices, aiExplanation: combinedExplanation || undefined, }; diff --git a/src/webviews/apps/plus/composer/components/commits-panel.ts b/src/webviews/apps/plus/composer/components/commits-panel.ts index cbf564048ef8c..32da97963a76e 100644 --- a/src/webviews/apps/plus/composer/components/commits-panel.ts +++ b/src/webviews/apps/plus/composer/components/commits-panel.ts @@ -727,7 +727,9 @@ export class CommitsPanel extends LitElement { private get firstCommitWithoutMessage(): ComposerCommit | null { // Find the first commit that doesn't have a message - return this.commits.find(commit => !commit.message || commit.message.trim().length === 0) || null; + return ( + this.commits.find(commit => !commit.message.content || commit.message.content.trim().length === 0) || null + ); } private get shouldShowAddToDraftButton(): boolean { @@ -1379,7 +1381,7 @@ export class CommitsPanel extends LitElement { return html` - commit.id === msg.params.commitId ? { ...commit, message: msg.params.message } : commit, + commit.id === msg.params.commitId + ? { ...commit, message: { content: msg.params.message, isGenerated: true } } + : commit, ); const updatedState = { diff --git a/src/webviews/plus/composer/composerWebview.ts b/src/webviews/plus/composer/composerWebview.ts index 429d5a2d3ae3a..f08d725bc9262 100644 --- a/src/webviews/plus/composer/composerWebview.ts +++ b/src/webviews/plus/composer/composerWebview.ts @@ -470,7 +470,7 @@ export class ComposerWebviewProvider implements WebviewProvider ({ id: commit.id, - message: commit.message, + message: commit.message.content, aiExplanation: commit.aiExplanation, hunkIndices: commit.hunkIndices, })); @@ -1115,7 +1115,7 @@ export class ComposerWebviewProvider implements WebviewProvider ({ id: `ai-commit-${index}`, - message: commit.message, + message: { content: commit.message, isGenerated: true }, aiExplanation: commit.explanation, hunkIndices: commit.hunks.map(h => h.hunk), })); diff --git a/src/webviews/plus/composer/mockData.ts b/src/webviews/plus/composer/mockData.ts index 745a9e6c8bfd3..fb0d70b080aa0 100644 --- a/src/webviews/plus/composer/mockData.ts +++ b/src/webviews/plus/composer/mockData.ts @@ -327,21 +327,21 @@ return user;`, export const mockCommits: ComposerCommit[] = [ { id: 'commit-1', - message: 'Add user authentication system', + message: { content: 'Add user authentication system', isGenerated: true }, aiExplanation: 'This commit introduces a comprehensive user authentication system with login validation, user types, and session management. The changes include creating a validateUser function for credential checking, defining User and LoginCredentials interfaces with role-based access control, and implementing secure session management with UUID-based session IDs and expiration handling.', hunkIndices: [1, 10, 2, 3], }, { id: 'commit-2', - message: 'Implement database integration with PostgreSQL', + message: { content: 'Implement database integration with PostgreSQL', isGenerated: true }, aiExplanation: 'This commit establishes database connectivity using PostgreSQL with connection pooling for optimal performance. It includes database connection configuration with environment variable support, a reusable query function with proper connection management, and initial database schema migration for the users table with appropriate indexes for efficient querying.', hunkIndices: [4, 5], }, { id: 'commit-3', - message: 'Add error handling and logging', + message: { content: 'Add error handling and logging', isGenerated: true }, aiExplanation: 'This commit establishes a robust error handling and logging infrastructure. It introduces custom error classes (AuthError, ValidationError, NetworkError) for better error categorization and a comprehensive Logger class with different log levels (ERROR, WARN, INFO, DEBUG) to replace basic console logging with structured, configurable logging throughout the application.', hunkIndices: [6, 7, 11], diff --git a/src/webviews/plus/composer/protocol.ts b/src/webviews/plus/composer/protocol.ts index 839d9e7b9d04a..eafca12068ce5 100644 --- a/src/webviews/plus/composer/protocol.ts +++ b/src/webviews/plus/composer/protocol.ts @@ -28,9 +28,14 @@ export interface ComposerHunkBase { coAuthors?: GitCommitIdentityShape[]; // Co-authors of the commit this hunk belongs to, if any } +export interface ComposerCommitMessage { + content: string; + isGenerated: boolean; +} + export interface ComposerCommit { id: string; - message: string; + message: ComposerCommitMessage; sha?: string; // Optional SHA for existing commits aiExplanation?: string; hunkIndices: number[]; // References to hunk indices in the hunk map diff --git a/src/webviews/plus/composer/utils/composer.utils.ts b/src/webviews/plus/composer/utils/composer.utils.ts index 1b56d225a46e7..f390e6ad62ac5 100644 --- a/src/webviews/plus/composer/utils/composer.utils.ts +++ b/src/webviews/plus/composer/utils/composer.utils.ts @@ -250,7 +250,7 @@ export function convertToComposerDiffInfo( const { patch, filePatches } = createCombinedDiffForCommit(getHunksForCommit(commit, hunks)); const commitHunks = getHunksForCommit(commit, hunks); const { author, coAuthors } = getAuthorAndCoAuthorsForCommit(commitHunks); - let message = commit.message; + let message = commit.message.content; if (coAuthors.length > 0) { message += `\n${coAuthors.map(a => `\nCo-authored-by: ${a.name} <${a.email}>`).join()}`; } @@ -279,7 +279,7 @@ export function generateComposerMarkdown( "Here's the breakdown of the commits created from the provided changes, along with explanations for each:\n\n"; for (let i = 0; i < commits.length; i++) { const commit = commits[i]; - const commitTitle = `### Commit ${i + 1}: ${commit.message}`; + const commitTitle = `### Commit ${i + 1}: ${commit.message.content}`; if (commit.aiExplanation) { markdown += `${commitTitle}\n\n${commit.aiExplanation}\n\n`; @@ -707,7 +707,7 @@ export async function createComposerCommitsFromGitCommits( // Create ComposerCommit const composerCommit: ComposerCommit = { id: commit.sha, - message: commit.message || '', + message: { content: commit.message || '', isGenerated: false }, sha: commit.sha, hunkIndices: commitHunkIndices, }; From 9252d986381914aede127e2f583b39e072f41016 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 25 Nov 2025 11:01:08 -0700 Subject: [PATCH 05/11] Fix timing issue with state initialization --- src/webviews/apps/plus/composer/components/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webviews/apps/plus/composer/components/app.ts b/src/webviews/apps/plus/composer/components/app.ts index 6475cfacc0243..b245fb0047505 100644 --- a/src/webviews/apps/plus/composer/components/app.ts +++ b/src/webviews/apps/plus/composer/components/app.ts @@ -410,13 +410,13 @@ export class ComposerApp extends LitElement { private lastMouseEvent?: MouseEvent; override firstUpdated() { - this.initializeResetStateIfNeeded(); // Delay initialization to ensure DOM is ready setTimeout(() => this.initializeSortable(), 200); this.initializeDragTracking(); if (this.state.commits.length > 0) { this.selectCommit(this.state.commits[0].id); } + this.initializeResetStateIfNeeded(); if (!this.state.onboardingDismissed) { this.openOnboarding(); } From 85be5f70b0dfa6aebf1a991479aec57c2dd70b3b Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 25 Nov 2025 11:01:08 -0700 Subject: [PATCH 06/11] Fix snapshot state to use local properties instead of state object --- src/webviews/apps/plus/composer/components/app.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/webviews/apps/plus/composer/components/app.ts b/src/webviews/apps/plus/composer/components/app.ts index b245fb0047505..f66ea5025d714 100644 --- a/src/webviews/apps/plus/composer/components/app.ts +++ b/src/webviews/apps/plus/composer/components/app.ts @@ -614,9 +614,9 @@ export class ComposerApp extends LitElement { return { hunks: JSON.parse(JSON.stringify(this.state?.hunks ?? [])), commits: JSON.parse(JSON.stringify(this.state?.commits ?? [])), - selectedCommitId: this.state?.selectedCommitId ?? null, + selectedCommitId: this.selectedCommitId, selectedCommitIds: new Set([...this.selectedCommitIds]), - selectedUnassignedSection: this.state?.selectedUnassignedSection ?? null, + selectedUnassignedSection: this.selectedUnassignedSection, selectedHunkIds: new Set([...this.selectedHunkIds]), hasUsedAutoCompose: this.state?.hasUsedAutoCompose ?? false, recompose: this.state?.recompose ? JSON.parse(JSON.stringify(this.state.recompose)) : null, @@ -636,7 +636,9 @@ export class ComposerApp extends LitElement { }; (this as any).state = updatedState; + this.selectedCommitId = snapshot.selectedCommitId; this.selectedCommitIds = snapshot.selectedCommitIds; + this.selectedUnassignedSection = snapshot.selectedUnassignedSection; this.selectedHunkIds = snapshot.selectedHunkIds; this.requestUpdate(); } From a797403da2185b6c67c6afd608fc1fd15b67a088 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 25 Nov 2025 11:01:09 -0700 Subject: [PATCH 07/11] Enhance commit message UI with read-only view and editing improvements --- .../composer/components/commit-message.ts | 133 +++++++++++++++--- 1 file changed, 114 insertions(+), 19 deletions(-) diff --git a/src/webviews/apps/plus/composer/components/commit-message.ts b/src/webviews/apps/plus/composer/components/commit-message.ts index 8bf013cc1ba45..8f332b3d4529f 100644 --- a/src/webviews/apps/plus/composer/components/commit-message.ts +++ b/src/webviews/apps/plus/composer/components/commit-message.ts @@ -3,6 +3,7 @@ import { css, html, LitElement, nothing } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { when } from 'lit/directives/when.js'; +import { splitCommitMessage } from '../../../../../git/utils/commit.utils'; import { debounce } from '../../../../../system/function/debounce'; import { focusableBaseStyles } from '../../../shared/components/styles/lit/a11y.css'; import { boxSizingBase, scrollableBase } from '../../../shared/components/styles/lit/base.css'; @@ -20,7 +21,11 @@ export class CommitMessage extends LitElement { focusableBaseStyles, css` :host { - display: contents; + display: block; + position: sticky; + top: var(--sticky-top, 0); + z-index: 2; + background: var(--vscode-editor-background); } .commit-message { @@ -43,6 +48,12 @@ export class CommitMessage extends LitElement { margin-block: 0; } + .commit-message__text[tabindex='0']:hover { + border-color: color-mix(in srgb, transparent 50%, var(--vscode-input-border, #858585)); + background: color-mix(in srgb, transparent 50%, var(--vscode-input-background, #3c3c3c)); + cursor: text; + } + .commit-message__text.placeholder { color: var(--vscode-input-placeholderForeground); font-style: italic; @@ -55,20 +66,32 @@ export class CommitMessage extends LitElement { .commit-message__text .scrollable, .commit-message__input { - padding: 0.5rem; + padding: 0.8rem 1rem; min-height: 1lh; max-height: 10lh; } + .commit-message__summary { + display: block; + } + + p.commit-message__text .scrollable .commit-message__body { + display: block; + margin-top: 0.5rem; + font-size: 1.15rem !important; + line-height: 1.8rem !important; + color: var(--vscode-descriptionForeground) !important; + } + .commit-message__field { position: relative; } .commit-message__input { box-sizing: content-box; - width: calc(100% - 1rem); - border: 1px solid var(--vscode-input-border); - background: var(--vscode-input-background); + width: calc(100% - 2rem); + border: 1px solid var(--vscode-input-border, #858585); + background: var(--vscode-input-background, #3c3c3c); vertical-align: middle; field-sizing: content; resize: none; @@ -105,7 +128,7 @@ export class CommitMessage extends LitElement { .commit-message__input:has(~ .commit-message__ai-button) { padding-right: 3rem; - width: calc(100% - 3.5rem); + width: calc(100% - 4rem); } .commit-message__input.has-explanation { @@ -118,6 +141,11 @@ export class CommitMessage extends LitElement { -webkit-font-smoothing: auto; } + .commit-message__input:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; + } + .commit-message__input[aria-valid='false'] { border-color: var(--vscode-inputValidation-errorBorder); } @@ -190,8 +218,8 @@ export class CommitMessage extends LitElement { .commit-message__ai-button { position: absolute; - top: 0.3rem; - right: 0.3rem; + top: 0.5rem; + right: 0.7rem; z-index: 1; } `, @@ -206,6 +234,9 @@ export class CommitMessage extends LitElement { @property({ type: String }) explanation?: string; + @property({ type: Boolean, attribute: 'ai-generated', reflect: true }) + aiGenerated: boolean = false; + @property({ type: String, attribute: 'explanation-label' }) explanationLabel?: string = 'Auto-composition Summary:'; @@ -230,6 +261,9 @@ export class CommitMessage extends LitElement { @state() validityMessage?: string; + @state() + private isEditing: boolean = false; + protected override updated(changedProperties: PropertyValues): void { if (changedProperties.has('message')) { this.checkValidity(); @@ -237,9 +271,13 @@ export class CommitMessage extends LitElement { } override render() { + const messageContent = this.message ?? ''; + const hasMessage = messageContent.trim().length > 0; + const shouldShowTextarea = this.editable && (!hasMessage || this.isEditing); + return html`
${when( - this.editable, + shouldShowTextarea, () => this.renderEditable(), () => this.renderReadOnly(), )} @@ -258,7 +296,9 @@ export class CommitMessage extends LitElement { rows="3" aria-valid=${this.validityMessage ? 'false' : 'true'} ?invalid=${this.validityMessage ? 'true' : 'false'} + @focus=${() => (this.isEditing = true)} @input=${this.onMessageInput} + @blur=${this.exitEditMode} > ${this.renderHelpText()} ${when( @@ -274,7 +314,9 @@ export class CommitMessage extends LitElement { + ${this.explanation || this.aiGenerated ? 'Regenerate Message' : 'Generate Message'} `, () => html` - + + ${this.explanation || this.aiGenerated ? 'Regenerate Message' : 'Generate Message'} `, )}
@@ -294,16 +337,57 @@ export class CommitMessage extends LitElement { } private renderReadOnly() { - let displayMessage = 'Draft commit (add a commit message)'; - let isPlaceholder = true; - if (this.message && this.message.trim().length > 0) { - displayMessage = this.message.replace(/\n/g, '
'); - isPlaceholder = false; - } + const messageContent = this.message ?? ''; + const { summary, body } = splitCommitMessage(messageContent); + const summaryHtml = summary.replace(/\n/g, '
'); + const bodyHtml = body ? body.replace(/\n/g, '
') : ''; - return html`

- ${unsafeHTML(displayMessage)} -

`; + return html` +
+

this.enterEditMode() : nothing} + tabindex=${this.editable ? '0' : '-1'} + > + + ${unsafeHTML(summaryHtml)} + ${body ? html`${unsafeHTML(bodyHtml)}` : nothing} + +

+ ${this.renderHelpText()} + ${when( + this.editable && this.aiEnabled, + () => + html` this.onGenerateCommitMessageClick()} + > + + ${this.explanation || this.aiGenerated ? 'Regenerate Message' : 'Generate Message'} + `, + () => + this.editable + ? html` + + ${this.explanation || this.aiGenerated ? 'Regenerate Message' : 'Generate Message'} + ` + : nothing, + )} +
+ `; } private renderExplanation() { @@ -330,6 +414,17 @@ export class CommitMessage extends LitElement { ); } + private enterEditMode() { + this.isEditing = true; + void this.updateComplete.then(() => { + this.focusableElement?.focus(); + }); + } + + private exitEditMode() { + this.isEditing = false; + } + private onMessageInput(event: InputEvent) { const target = event.target as HTMLTextAreaElement; const message = target.value; From 5999cb07676cd1fde2db91214766644033c6a787 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 25 Nov 2025 11:01:09 -0700 Subject: [PATCH 08/11] Improve typography in commits panel --- src/webviews/apps/plus/composer/components/commits-panel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webviews/apps/plus/composer/components/commits-panel.ts b/src/webviews/apps/plus/composer/components/commits-panel.ts index 32da97963a76e..4c4658c0b2b55 100644 --- a/src/webviews/apps/plus/composer/components/commits-panel.ts +++ b/src/webviews/apps/plus/composer/components/commits-panel.ts @@ -304,8 +304,8 @@ export class CommitsPanel extends LitElement { background: var(--vscode-input-background); color: var(--vscode-input-foreground); font-family: inherit; - font-size: 1rem; - line-height: 1.6rem; + font-size: 1.3rem; + line-height: 1.8rem; } textarea.auto-compose__instructions-input { box-sizing: content-box; From 61334af7296881ce8eb31872d1c48be3bfc4e058 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 25 Nov 2025 11:01:09 -0700 Subject: [PATCH 09/11] Add default cursor style to composer items --- src/webviews/apps/plus/composer/components/composer.css.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webviews/apps/plus/composer/components/composer.css.ts b/src/webviews/apps/plus/composer/components/composer.css.ts index e8081b8f7275d..b6ca3c3afa3f1 100644 --- a/src/webviews/apps/plus/composer/components/composer.css.ts +++ b/src/webviews/apps/plus/composer/components/composer.css.ts @@ -82,6 +82,7 @@ export const composerItemStyles = css` --composer-item-background: var(--color-background); --composer-item-icon-color: var(--color-foreground--65); --composer-item-color: var(--color-foreground--65); + cursor: default; } .composer-item__content { From ed667381532c3ee36bd741665f71687b1e5f3962 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 25 Nov 2025 11:01:09 -0700 Subject: [PATCH 10/11] Add sticky positioning support for commit messages in details panel --- .../plus/composer/components/details-panel.ts | 40 +++++++++++++++++++ .../plus/composer/components/diff/diff.css.ts | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/webviews/apps/plus/composer/components/details-panel.ts b/src/webviews/apps/plus/composer/components/details-panel.ts index 06af5fbac3d93..8ac8584a29964 100644 --- a/src/webviews/apps/plus/composer/components/details-panel.ts +++ b/src/webviews/apps/plus/composer/components/details-panel.ts @@ -56,6 +56,11 @@ export class DetailsPanel extends LitElement { display: flex; flex-direction: column; gap: 3.2rem; + --commit-message-sticky-top: 0; + } + + .change-details gl-commit-message { + --sticky-top: var(--commit-message-sticky-top); } .change-details { @@ -257,10 +262,14 @@ export class DetailsPanel extends LitElement { private draggedHunkIds: string[] = []; private autoScrollInterval?: number; private dragOverCleanupTimeout?: number; + private commitMessageResizeObserver?: ResizeObserver; @query('.details-panel') private detailsPanel!: HTMLDivElement; + @query('.changes-list') + private changesList?: HTMLDivElement; + override updated(changedProperties: Map) { super.updated(changedProperties); @@ -274,6 +283,33 @@ export class DetailsPanel extends LitElement { this.initializeHunksSortable(); this.setupAutoScroll(); } + + if (changedProperties.has('selectedCommits')) { + this.updateCommitMessageStickyOffset(); + } + } + + private updateCommitMessageStickyOffset() { + if (!this.commitMessageResizeObserver) { + this.commitMessageResizeObserver = new ResizeObserver(() => { + const commitMessage = this.shadowRoot?.querySelector('gl-commit-message'); + if (commitMessage && this.changesList) { + const height = commitMessage.getBoundingClientRect().height; + this.changesList.style.setProperty('--file-header-sticky-top', `${height}px`); + } + }); + } + + this.commitMessageResizeObserver.disconnect(); + + const commitMessage = this.shadowRoot?.querySelector('gl-commit-message'); + if (commitMessage) { + this.commitMessageResizeObserver.observe(commitMessage); + if (this.changesList) { + const height = commitMessage.getBoundingClientRect().height; + this.changesList.style.setProperty('--file-header-sticky-top', `${height}px`); + } + } } override disconnectedCallback() { @@ -284,6 +320,10 @@ export class DetailsPanel extends LitElement { clearTimeout(this.dragOverCleanupTimeout); this.dragOverCleanupTimeout = undefined; } + if (this.commitMessageResizeObserver) { + this.commitMessageResizeObserver.disconnect(); + this.commitMessageResizeObserver = undefined; + } } private destroyHunksSortables() { diff --git a/src/webviews/apps/plus/composer/components/diff/diff.css.ts b/src/webviews/apps/plus/composer/components/diff/diff.css.ts index 6919ab4d79d96..fe39611e1537e 100644 --- a/src/webviews/apps/plus/composer/components/diff/diff.css.ts +++ b/src/webviews/apps/plus/composer/components/diff/diff.css.ts @@ -136,7 +136,7 @@ export const diff2htmlStyles = css` } .d2h-file-header.d2h-sticky-header { position: sticky; - top: 0; + top: var(--file-header-sticky-top, 0); z-index: 1; } .d2h-file-stats { From abd51f70d7e2f5a637506f5cd9f7183505ec0583 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 25 Nov 2025 11:01:10 -0700 Subject: [PATCH 11/11] Style composition summary view and add click handler --- .../plus/composer/components/details-panel.ts | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/webviews/apps/plus/composer/components/details-panel.ts b/src/webviews/apps/plus/composer/components/details-panel.ts index 8ac8584a29964..d3ada90f21e97 100644 --- a/src/webviews/apps/plus/composer/components/details-panel.ts +++ b/src/webviews/apps/plus/composer/components/details-panel.ts @@ -167,6 +167,13 @@ export class DetailsPanel extends LitElement { color: var(--color-foreground--85); } + .change-details.composition-summary { + border: 0.1rem solid var(--vscode-panel-border); + border-radius: 0.3rem; + padding: 1.6rem; + gap: 0; + } + .empty-state { margin-block: 0; font-weight: bold; @@ -710,9 +717,10 @@ export class DetailsPanel extends LitElement { return html`
+
`; @@ -774,7 +782,7 @@ export class DetailsPanel extends LitElement { // Handle no changes state if (!this.hasChanges) { return html` -
+
${this.renderNoChangesState()}
`; @@ -783,12 +791,29 @@ export class DetailsPanel extends LitElement { const isMultiSelect = this.selectedCommits.length > 1; return html` -
+
${this.renderDetails()}
`; } + private handlePanelClick(e: MouseEvent) { + const target = e.target as HTMLElement; + const tagName = target.tagName.toLowerCase(); + + const interactiveTags = ['input', 'textarea', 'button', 'a', 'select', 'gl-button', 'gl-commit-message']; + const isInteractive = + interactiveTags.includes(tagName) || + target.closest('gl-commit-message, gl-button, button, a, input, textarea, select'); + + if (!isInteractive) { + const activeElement = this.shadowRoot?.activeElement; + if (activeElement && 'blur' in activeElement && typeof activeElement.blur === 'function') { + activeElement.blur(); + } + } + } + private renderNoChangesState() { return html`