Skip to content

Commit e946d3e

Browse files
committed
Adds generating patch message
1 parent 07dcfa3 commit e946d3e

File tree

7 files changed

+186
-2
lines changed

7 files changed

+186
-2
lines changed

src/ai/aiProviderService.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,3 +522,17 @@ export async function getApiKey(
522522

523523
return apiKey;
524524
}
525+
526+
export function extractDraftMessage(
527+
message: string,
528+
splitter = '\n\n',
529+
): { title: string; description: string | undefined } {
530+
const firstBreak = message.indexOf(splitter) ?? 0;
531+
const title = firstBreak > -1 ? message.substring(0, firstBreak) : message;
532+
const description = firstBreak > -1 ? message.substring(firstBreak + splitter.length) : undefined;
533+
534+
return {
535+
title: title,
536+
description: description,
537+
};
538+
}

src/plus/webviews/patchDetails/patchDetailsWebview.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ConfigurationChangeEvent } from 'vscode';
22
import { Disposable, env, Uri, window } from 'vscode';
3+
import { extractDraftMessage } from '../../../ai/aiProviderService';
34
import { getAvatarUri } from '../../../avatars';
45
import type { ContextKeys, Sources } from '../../../constants';
56
import { Commands, GlyphChars, previewBadge } from '../../../constants';
@@ -56,6 +57,7 @@ import type {
5657
CreateDraft,
5758
CreatePatchParams,
5859
DidExplainParams,
60+
DidGenerateParams,
5961
DraftPatchCheckedParams,
6062
DraftUserSelection,
6163
ExecuteFileActionParams,
@@ -81,6 +83,7 @@ import {
8183
DidChangePreferencesNotification,
8284
DraftPatchCheckedCommand,
8385
ExplainRequest,
86+
GenerateRequest,
8487
OpenFileCommand,
8588
OpenFileComparePreviousCommand,
8689
OpenFileCompareWorkingCommand,
@@ -240,6 +243,10 @@ export class PatchDetailsWebviewProvider
240243
void this.explainRequest(ExplainRequest, e);
241244
break;
242245

246+
case GenerateRequest.is(e):
247+
void this.generateRequest(GenerateRequest, e);
248+
break;
249+
243250
case OpenFileComparePreviousCommand.is(e):
244251
void this.openFileComparisonWithPrevious(e.params);
245252
break;
@@ -823,6 +830,48 @@ export class PatchDetailsWebviewProvider
823830
void this.host.respond(requestType, msg, params);
824831
}
825832

833+
private async generateRequest<T extends typeof GenerateRequest>(requestType: T, msg: IpcCallMessageType<T>) {
834+
let repo: Repository | undefined;
835+
if (this._context.create?.changes != null) {
836+
for (const change of this._context.create.changes.values()) {
837+
if (change.repository) {
838+
repo = change.repository;
839+
break;
840+
}
841+
}
842+
}
843+
844+
if (!repo) {
845+
void this.host.respond(requestType, msg, { error: { message: 'Unable to find changes' } });
846+
return;
847+
}
848+
849+
let params: DidGenerateParams;
850+
851+
try {
852+
// TODO@eamodio HACK -- only works for the first patch
853+
// const patch = await this.getDraftPatch(this._context.draft);
854+
// if (patch == null) throw new Error('Unable to find patch');
855+
856+
// const commit = await this.getOrCreateCommitForPatch(patch.gkRepositoryId);
857+
// if (commit == null) throw new Error('Unable to find commit');
858+
859+
const summary = await (
860+
await this.container.ai
861+
)?.generateDraftMessage(repo, {
862+
progress: { location: { viewId: this.host.id } },
863+
});
864+
if (summary == null) throw new Error('Error retrieving content');
865+
866+
params = extractDraftMessage(summary);
867+
} catch (ex) {
868+
debugger;
869+
params = { error: { message: ex.message } };
870+
}
871+
872+
void this.host.respond(requestType, msg, params);
873+
}
874+
826875
private async openPatchContents(_params: ExecuteFileActionParams) {
827876
// TODO@eamodio Open the patch contents for the selected repo in an untitled editor
828877
}

src/plus/webviews/patchDetails/protocol.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,15 @@ export type DidExplainParams =
275275
| { error: { message: string } };
276276
export const ExplainRequest = new IpcRequest<void, DidExplainParams>(scope, 'explain');
277277

278+
export type DidGenerateParams =
279+
| {
280+
title: string | undefined;
281+
description: string | undefined;
282+
error?: undefined;
283+
}
284+
| { error: { message: string } };
285+
export const GenerateRequest = new IpcRequest<void, DidGenerateParams>(scope, 'generate');
286+
278287
// NOTIFICATIONS
279288

280289
export interface DidChangeParams {

src/webviews/apps/plus/patchDetails/components/gl-patch-create.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ export interface CreatePatchUpdateSelectionEventDetail {
5454
role: Exclude<DraftRole, 'owner'> | 'remove';
5555
}
5656

57+
interface GenerateState {
58+
cancelled?: boolean;
59+
error?: { message: string };
60+
title?: string;
61+
description?: string;
62+
}
63+
5764
// Can only import types from 'vscode'
5865
const BesideViewColumn = -2; /*ViewColumn.Beside*/
5966

@@ -63,6 +70,12 @@ export class GlPatchCreate extends GlTreeBase {
6370

6471
@property({ type: Boolean }) review = false;
6572

73+
@property({ type: Object })
74+
generate?: GenerateState;
75+
76+
@state()
77+
generateBusy = false;
78+
6679
// @state()
6780
// patchTitle = this.create.title ?? '';
6881

@@ -75,6 +88,9 @@ export class GlPatchCreate extends GlTreeBase {
7588
@query('#desc')
7689
descInput!: HTMLInputElement;
7790

91+
@query('#generate-ai')
92+
generateAiButton!: HTMLElement;
93+
7894
@state()
7995
validityMessage?: string;
8096

@@ -126,6 +142,12 @@ export class GlPatchCreate extends GlTreeBase {
126142
defineGkElement(Avatar, Button, Menu, MenuItem, Popover);
127143
}
128144

145+
override updated(changedProperties: Map<string, any>) {
146+
if (changedProperties.has('generate')) {
147+
this.generateBusy = false;
148+
this.generateAiButton.scrollIntoView();
149+
}
150+
}
129151
protected override firstUpdated() {
130152
window.requestAnimationFrame(() => {
131153
this.titleInput.focus();
@@ -258,16 +280,35 @@ export class GlPatchCreate extends GlTreeBase {
258280
${this.renderUserSelectionList()}
259281
`,
260282
)}
261-
<div class="message-input">
283+
<div class="message-input message-input--with-menu">
262284
<input
263285
id="title"
264286
type="text"
265287
class="message-input__control"
266288
placeholder="Title (required)"
267289
maxlength="100"
268290
.value=${this.create.title ?? ''}
291+
?disabled=${this.generateBusy}
269292
@input=${(e: InputEvent) => this.onDebounceTitleInput(e)}
270293
/>
294+
${when(
295+
this.state?.orgSettings.ai === true,
296+
() =>
297+
html`<div class="message-input__menu">
298+
<gl-button
299+
id="generate-ai"
300+
appearance="toolbar"
301+
density="compact"
302+
tooltip="Generate Title and Description..."
303+
@click=${(e: MouseEvent) => this.onGenerateTitleClick(e)}
304+
?disabled=${this.generateBusy}
305+
><code-icon
306+
icon=${this.generateBusy ? 'loading' : 'sparkle'}
307+
modifier="${this.generateBusy ? 'spin' : ''}"
308+
></code-icon
309+
></gl-button>
310+
</div>`,
311+
)}
271312
</div>
272313
<div class="message-input">
273314
<textarea
@@ -276,6 +317,7 @@ export class GlPatchCreate extends GlTreeBase {
276317
placeholder="Description (optional)"
277318
maxlength="10000"
278319
.value=${this.create.description ?? ''}
320+
?disabled=${this.generateBusy}
279321
@input=${(e: InputEvent) => this.onDebounceDescriptionInput(e)}
280322
></textarea>
281323
</div>
@@ -663,6 +705,15 @@ export class GlPatchCreate extends GlTreeBase {
663705
this.fireMetadataUpdate();
664706
}
665707

708+
private onGenerateTitleClick(_e: Event) {
709+
this.generateBusy = true;
710+
this.emit('gl-patch-generate-title', {
711+
title: this.create.title!,
712+
description: this.create.description,
713+
visibility: this.create.visibility,
714+
});
715+
}
716+
666717
private fireMetadataUpdate() {
667718
this.emit('gl-patch-create-update-metadata', {
668719
title: this.create.title!,
@@ -786,6 +837,7 @@ declare global {
786837
'gl-patch-file-open': CustomEvent<ExecuteFileActionParams>;
787838
'gl-patch-file-stage': CustomEvent<ExecuteFileActionParams>;
788839
'gl-patch-file-unstage': CustomEvent<ExecuteFileActionParams>;
840+
'gl-patch-generate-title': CustomEvent<CreatePatchMetadataEventDetail>;
789841
'gl-patch-create-invite-users': CustomEvent<undefined>;
790842
'gl-patch-create-update-selection': CustomEvent<CreatePatchUpdateSelectionEventDetail>;
791843
'gl-patch-create-cancelled': CustomEvent<undefined>;

src/webviews/apps/plus/patchDetails/components/patch-details-app.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ interface ExplainState {
1414
summary?: string;
1515
}
1616

17+
interface GenerateState {
18+
cancelled?: boolean;
19+
error?: { message: string };
20+
title?: string;
21+
description?: string;
22+
}
23+
1724
export interface ApplyPatchDetail {
1825
draft: DraftDetails;
1926
target?: 'current' | 'branch' | 'worktree';
@@ -45,6 +52,9 @@ export class GlPatchDetailsApp extends GlElement {
4552
@property({ type: Object })
4653
explain?: ExplainState;
4754

55+
@property({ type: Object })
56+
generate?: GenerateState;
57+
4858
@property({ attribute: false, type: Object })
4959
app?: PatchDetailsApp;
5060

@@ -111,7 +121,7 @@ export class GlPatchDetailsApp extends GlElement {
111121
${when(
112122
this.mode === 'view',
113123
() => html`<gl-draft-details .state=${this.state} .explain=${this.explain}></gl-draft-details>`,
114-
() => html`<gl-patch-create .state=${this.state}></gl-patch-create>`,
124+
() => html`<gl-patch-create .state=${this.state} .generate=${this.generate}></gl-patch-create>`,
115125
)}
116126
</main>
117127
</div>

src/webviews/apps/plus/patchDetails/patchDetails.scss

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ gk-menu-item {
216216
outline-offset: -1px;
217217
}
218218

219+
&:disabled {
220+
opacity: 0.4;
221+
cursor: not-allowed;
222+
pointer-events: none;
223+
}
224+
219225
&--text {
220226
overflow: hidden;
221227
white-space: nowrap;
@@ -266,12 +272,22 @@ gk-menu-item {
266272
padding-right: 2.4rem;
267273
}
268274

275+
&__menu {
276+
position: absolute;
277+
top: 0.8rem;
278+
right: 0;
279+
}
280+
269281
&--group {
270282
display: flex;
271283
flex-direction: row;
272284
align-items: stretch;
273285
gap: 0.6rem;
274286
}
287+
288+
&--with-menu {
289+
position: relative;
290+
}
275291
}
276292

277293
textarea.message-input__control {

src/webviews/apps/plus/patchDetails/patchDetails.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
DraftPatchCheckedCommand,
1818
ExecuteFileActionCommand,
1919
ExplainRequest,
20+
GenerateRequest,
2021
OpenFileCommand,
2122
OpenFileComparePreviousCommand,
2223
OpenFileCompareWorkingCommand,
@@ -129,6 +130,9 @@ export class PatchDetailsApp extends App<Serialized<State>> {
129130
'gl-patch-create-repo-checked',
130131
e => this.onCreateCheckRepo(e.detail),
131132
),
133+
DOM.on<GlPatchCreate, CreatePatchMetadataEventDetail>('gl-patch-create', 'gl-patch-generate-title', e =>
134+
this.onCreateGenerateTitle(e.detail),
135+
),
132136
DOM.on<GlPatchCreate, CreatePatchMetadataEventDetail>(
133137
'gl-patch-create',
134138
'gl-patch-create-update-metadata',
@@ -255,6 +259,36 @@ export class PatchDetailsApp extends App<Serialized<State>> {
255259
this.sendCommand(UpdateCreatePatchMetadataCommand, e);
256260
}
257261

262+
private async onCreateGenerateTitle(_e: CreatePatchMetadataEventDetail) {
263+
try {
264+
const result = await this.sendRequest(GenerateRequest, undefined);
265+
266+
if (result.error) {
267+
this.component.generate = { error: { message: result.error.message ?? 'Error retrieving content' } };
268+
} else if (result.title || result.description) {
269+
this.component.generate = {
270+
title: result.title,
271+
description: result.description,
272+
};
273+
274+
this.state = {
275+
...this.state,
276+
create: {
277+
...this.state.create!,
278+
title: result.title ?? this.state.create?.title,
279+
description: result.description ?? this.state.create?.description,
280+
},
281+
};
282+
this.setState(this.state);
283+
this.debouncedAttachState();
284+
} else {
285+
this.component.generate = undefined;
286+
}
287+
} catch (ex) {
288+
this.component.generate = { error: { message: 'Error retrieving content' } };
289+
}
290+
}
291+
258292
private onDraftUpdateMetadata(e: { visibility: DraftVisibility }) {
259293
this.sendCommand(UpdatePatchDetailsMetadataCommand, e);
260294
}

0 commit comments

Comments
 (0)