diff --git a/contributions.json b/contributions.json index 9fea8a9aeb2dd..eb03581da2987 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 3060ee4b70b71..3f769fc72381a 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", @@ -11752,6 +11757,10 @@ "command": "gitlens.composeCommits:views", "when": "false" }, + { + "command": "gitlens.composer.maximize", + "when": "false" + }, { "command": "gitlens.composer.refresh", "when": "false" @@ -15488,25 +15497,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", @@ -15535,6 +15539,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", @@ -15546,32 +15576,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", 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; diff --git a/src/constants.commands.generated.ts b/src/constants.commands.generated.ts index fb392da74fde4..8f2bbd7651958 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 70e9ca263662b..8158b12e1908f 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/apps/plus/composer/components/app.ts b/src/webviews/apps/plus/composer/components/app.ts index 00e57d2f0d82b..cf50d2acdd123 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; } @@ -407,13 +407,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(); } @@ -611,9 +611,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, }; @@ -622,9 +622,9 @@ export class ComposerApp extends LitElement { private applyDataSnapshot(snapshot: ComposerDataSnapshot) { this.state.hunks = snapshot.hunks; this.state.commits = snapshot.commits; - this.state.selectedCommitId = snapshot.selectedCommitId; + this.selectedCommitId = snapshot.selectedCommitId; this.selectedCommitIds = snapshot.selectedCommitIds; - this.state.selectedUnassignedSection = snapshot.selectedUnassignedSection; + this.selectedUnassignedSection = snapshot.selectedUnassignedSection; this.state.hasUsedAutoCompose = snapshot.hasUsedAutoCompose; this.selectedHunkIds = snapshot.selectedHunkIds; this.requestUpdate(); @@ -823,7 +823,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, }; @@ -1037,7 +1037,7 @@ export class ComposerApp extends LitElement { this.commitMessageBeingEdited = null; }, 1000); - commit.message = message; + commit.message = { content: message, isGenerated: false }; this.requestUpdate(); } } @@ -1208,7 +1208,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 { @@ -1301,7 +1304,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'}.`, @@ -1469,7 +1472,7 @@ export class ComposerApp extends LitElement { this._ipc.sendCommand(GenerateCommitMessageCommand, { commitId: commitId, commitHunkIndices: commit.hunkIndices, - overwriteExistingMessage: commit.message.trim() !== '', + overwriteExistingMessage: commit.message.content.trim() !== '', }); } @@ -1488,7 +1491,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'); @@ -1498,10 +1501,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/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; diff --git a/src/webviews/apps/plus/composer/components/commits-panel.ts b/src/webviews/apps/plus/composer/components/commits-panel.ts index 59c4516feb7e0..b03a01766d2d3 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; @@ -693,7 +693,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 { @@ -1343,7 +1345,7 @@ export class CommitsPanel extends LitElement { return html` ) { super.updated(changedProperties); @@ -274,6 +290,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 +327,10 @@ export class DetailsPanel extends LitElement { clearTimeout(this.dragOverCleanupTimeout); this.dragOverCleanupTimeout = undefined; } + if (this.commitMessageResizeObserver) { + this.commitMessageResizeObserver.disconnect(); + this.commitMessageResizeObserver = undefined; + } } private destroyHunksSortables() { @@ -666,9 +713,10 @@ export class DetailsPanel extends LitElement { return html`
+
`; @@ -730,7 +778,7 @@ export class DetailsPanel extends LitElement { // Handle no changes state if (!this.hasChanges) { return html` -
+
${this.renderNoChangesState()}
`; @@ -739,12 +787,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`
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 { diff --git a/src/webviews/apps/plus/composer/stateProvider.ts b/src/webviews/apps/plus/composer/stateProvider.ts index 0113c1681dc86..ed41e827293ab 100644 --- a/src/webviews/apps/plus/composer/stateProvider.ts +++ b/src/webviews/apps/plus/composer/stateProvider.ts @@ -78,7 +78,9 @@ export class ComposerStateProvider extends StateProviderBase - 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 be98de70ac890..90acb25cb4c66 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'; @@ -452,7 +453,7 @@ export class ComposerWebviewProvider implements WebviewProvider ({ id: commit.id, - message: commit.message, + message: commit.message.content, aiExplanation: commit.aiExplanation, hunkIndices: commit.hunkIndices, })); @@ -1054,7 +1055,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), })); @@ -1550,4 +1551,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/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 43e2f901932f1..76894f86916ff 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/registration.ts b/src/webviews/plus/composer/registration.ts index 908a9033b3e4d..45ddee6f7fa86 100644 --- a/src/webviews/plus/composer/registration.ts +++ b/src/webviews/plus/composer/registration.ts @@ -51,5 +51,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/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, }; diff --git a/src/webviews/webviewController.ts b/src/webviews/webviewController.ts index af82bb01db3fb..83a09b9727df3 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(); + }, }; }