mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-18 23:02:31 +03:00
Improve UI of advanced commit options
This commit is contained in:
parent
8db8ec5641
commit
f8109368f3
@ -1,6 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import BranchFilesList from './BranchFilesList.svelte';
|
import BranchFilesList from './BranchFilesList.svelte';
|
||||||
import { Project } from '$lib/backend/projects';
|
import { Project } from '$lib/backend/projects';
|
||||||
|
import Button from '$lib/components/Button.svelte';
|
||||||
|
import CommitMessageInput from '$lib/components/CommitMessageInput.svelte';
|
||||||
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
import Tag from '$lib/components/Tag.svelte';
|
import Tag from '$lib/components/Tag.svelte';
|
||||||
import TimeAgo from '$lib/components/TimeAgo.svelte';
|
import TimeAgo from '$lib/components/TimeAgo.svelte';
|
||||||
import { persistedCommitMessage } from '$lib/config/config';
|
import { persistedCommitMessage } from '$lib/config/config';
|
||||||
@ -49,11 +52,7 @@
|
|||||||
|
|
||||||
function toggleFiles() {
|
function toggleFiles() {
|
||||||
showFiles = !showFiles;
|
showFiles = !showFiles;
|
||||||
if (!showFiles && branch) {
|
|
||||||
if (commit.description != description) {
|
|
||||||
branchController.updateCommitMessage(branch.id, commit.id, description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (showFiles) loadFiles();
|
if (showFiles) loadFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,9 +88,37 @@
|
|||||||
|
|
||||||
$: isUndoable = isHeadCommit;
|
$: isUndoable = isHeadCommit;
|
||||||
const hasCommitUrl = !commit.isLocal && commitUrl;
|
const hasCommitUrl = !commit.isLocal && commitUrl;
|
||||||
let description = commit.description;
|
|
||||||
|
let commitMessageModal: Modal;
|
||||||
|
let description = '';
|
||||||
|
|
||||||
|
function openCommitMessageModal(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
description = commit.description;
|
||||||
|
|
||||||
|
commitMessageModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitCommitMessageModal() {
|
||||||
|
commit.description = description;
|
||||||
|
|
||||||
|
if (branch) {
|
||||||
|
branchController.updateCommitMessage(branch.id, commit.id, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
commitMessageModal.close();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={commitMessageModal}>
|
||||||
|
<CommitMessageInput bind:commitMessage={description} />
|
||||||
|
<svelte:fragment slot="controls">
|
||||||
|
<Button style="ghost" kind="solid" on:click={() => commitMessageModal.close()}>Cancel</Button>
|
||||||
|
<Button style="pop" kind="solid" grow on:click={submitCommitMessageModal}>Submit</Button>
|
||||||
|
</svelte:fragment>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
use:draggable={commit instanceof Commit
|
use:draggable={commit instanceof Commit
|
||||||
? {
|
? {
|
||||||
@ -101,17 +128,6 @@
|
|||||||
class="commit"
|
class="commit"
|
||||||
class:is-commit-open={showFiles}
|
class:is-commit-open={showFiles}
|
||||||
>
|
>
|
||||||
{#if $advancedCommitOperations}
|
|
||||||
{#if isUndoable && showFiles}
|
|
||||||
<div class="commit__edit_description">
|
|
||||||
<textarea
|
|
||||||
placeholder="commit message here"
|
|
||||||
bind:value={description}
|
|
||||||
rows={commit.description.split('\n').length + 1}
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
<div class="commit__header" on:click={toggleFiles} on:keyup={onKeyup} role="button" tabindex="0">
|
<div class="commit__header" on:click={toggleFiles} on:keyup={onKeyup} role="button" tabindex="0">
|
||||||
<div class="commit__message">
|
<div class="commit__message">
|
||||||
{#if $advancedCommitOperations}
|
{#if $advancedCommitOperations}
|
||||||
@ -154,6 +170,19 @@
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if showFiles}
|
||||||
|
{#if commit.descriptionBody}
|
||||||
|
<div class="commit__row" transition:slide={{ duration: 100 }}>
|
||||||
|
<span class="commit__body text-base-body-12">
|
||||||
|
{commit.descriptionBody}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $advancedCommitOperations}
|
||||||
|
<Tag clickable on:click={openCommitMessageModal}>Edit</Tag>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="commit__row">
|
<div class="commit__row">
|
||||||
<div class="commit__author">
|
<div class="commit__author">
|
||||||
@ -326,16 +355,6 @@
|
|||||||
gap: var(--size-8);
|
gap: var(--size-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.commit__edit_description {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.commit__edit_description textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 14px;
|
|
||||||
margin: 0;
|
|
||||||
border-bottom: 1px solid #dddddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit__id {
|
.commit__id {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -382,6 +401,7 @@
|
|||||||
.files__footer {
|
.files__footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: var(--size-8);
|
gap: var(--size-8);
|
||||||
padding: var(--size-14);
|
padding: var(--size-14);
|
||||||
background-color: var(--clr-bg-1);
|
background-color: var(--clr-bg-1);
|
||||||
|
@ -1,80 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte';
|
||||||
import { AIService } from '$lib/ai/service';
|
import CommitMessageInput from '$lib/components/CommitMessageInput.svelte';
|
||||||
import Checkbox from '$lib/components/Checkbox.svelte';
|
import { projectRunCommitHooks, persistedCommitMessage } from '$lib/config/config';
|
||||||
import DropDownButton from '$lib/components/DropDownButton.svelte';
|
|
||||||
import Icon from '$lib/components/Icon.svelte';
|
|
||||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
|
||||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
|
||||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
|
||||||
import {
|
|
||||||
projectAiGenEnabled,
|
|
||||||
projectCommitGenerationExtraConcise,
|
|
||||||
projectCommitGenerationUseEmojis,
|
|
||||||
projectRunCommitHooks,
|
|
||||||
persistedCommitMessage
|
|
||||||
} from '$lib/config/config';
|
|
||||||
import { showError } from '$lib/notifications/toasts';
|
|
||||||
import { User } from '$lib/stores/user';
|
|
||||||
import { splitMessage } from '$lib/utils/commitMessage';
|
|
||||||
import { getContext, getContextStore } from '$lib/utils/context';
|
import { getContext, getContextStore } from '$lib/utils/context';
|
||||||
import { tooltip } from '$lib/utils/tooltip';
|
|
||||||
import { useAutoHeight } from '$lib/utils/useAutoHeight';
|
|
||||||
import { useResize } from '$lib/utils/useResize';
|
|
||||||
import { BranchController } from '$lib/vbranches/branchController';
|
import { BranchController } from '$lib/vbranches/branchController';
|
||||||
import { Ownership } from '$lib/vbranches/ownership';
|
import { Ownership } from '$lib/vbranches/ownership';
|
||||||
import { Branch, type LocalFile } from '$lib/vbranches/types';
|
import { Branch } from '$lib/vbranches/types';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
import { fly, slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
|
|
||||||
const aiService = getContext(AIService);
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
|
||||||
action: 'generate-branch-name';
|
|
||||||
}>();
|
|
||||||
|
|
||||||
export let projectId: string;
|
export let projectId: string;
|
||||||
export let expanded: Writable<boolean>;
|
export let expanded: Writable<boolean>;
|
||||||
|
|
||||||
const branchController = getContext(BranchController);
|
const branchController = getContext(BranchController);
|
||||||
const selectedOwnership = getContextStore(Ownership);
|
const selectedOwnership = getContextStore(Ownership);
|
||||||
const branch = getContextStore(Branch);
|
const branch = getContextStore(Branch);
|
||||||
const user = getContextStore(User);
|
|
||||||
|
|
||||||
const aiGenEnabled = projectAiGenEnabled(projectId);
|
|
||||||
const runCommitHooks = projectRunCommitHooks(projectId);
|
const runCommitHooks = projectRunCommitHooks(projectId);
|
||||||
const commitMessage = persistedCommitMessage(projectId, $branch.id);
|
const commitMessage = persistedCommitMessage(projectId, $branch.id);
|
||||||
const commitGenerationExtraConcise = projectCommitGenerationExtraConcise(projectId);
|
|
||||||
const commitGenerationUseEmojis = projectCommitGenerationUseEmojis(projectId);
|
|
||||||
|
|
||||||
let isCommitting = false;
|
let isCommitting = false;
|
||||||
let aiLoading = false;
|
|
||||||
|
|
||||||
let contextMenu: ContextMenu;
|
|
||||||
|
|
||||||
let titleTextArea: HTMLTextAreaElement;
|
|
||||||
let descriptionTextArea: HTMLTextAreaElement;
|
|
||||||
|
|
||||||
$: ({ title, description } = splitMessage($commitMessage));
|
|
||||||
$: if ($commitMessage) updateHeights();
|
|
||||||
|
|
||||||
function concatMessage(title: string, description: string) {
|
|
||||||
return `${title}\n\n${description}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusTextareaOnMount(el: HTMLTextAreaElement) {
|
|
||||||
el.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHeights() {
|
|
||||||
useAutoHeight(titleTextArea);
|
|
||||||
useAutoHeight(descriptionTextArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function commit() {
|
async function commit() {
|
||||||
const message = concatMessage(title, description);
|
const message = $commitMessage;
|
||||||
isCommitting = true;
|
isCommitting = true;
|
||||||
try {
|
try {
|
||||||
await branchController.commitBranch(
|
await branchController.commitBranch(
|
||||||
@ -88,158 +37,12 @@
|
|||||||
isCommitting = false;
|
isCommitting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateCommitMessage(files: LocalFile[]) {
|
|
||||||
const hunks = files.flatMap((f) =>
|
|
||||||
f.hunks.filter((h) => $selectedOwnership.contains(f.id, h.id))
|
|
||||||
);
|
|
||||||
// Branches get their names generated only if there are at least 4 lines of code
|
|
||||||
// If the change is a 'one-liner', the branch name is either left as "virtual branch"
|
|
||||||
// or the user has to manually trigger the name generation from the meatball menu
|
|
||||||
// This saves people this extra click
|
|
||||||
if ($branch.name.toLowerCase().includes('virtual branch')) {
|
|
||||||
dispatch('action', 'generate-branch-name');
|
|
||||||
}
|
|
||||||
|
|
||||||
aiLoading = true;
|
|
||||||
try {
|
|
||||||
const generatedMessage = await aiService.summarizeCommit({
|
|
||||||
hunks,
|
|
||||||
useEmojiStyle: $commitGenerationUseEmojis,
|
|
||||||
useBriefStyle: $commitGenerationExtraConcise,
|
|
||||||
userToken: $user?.access_token
|
|
||||||
});
|
|
||||||
|
|
||||||
if (generatedMessage) {
|
|
||||||
$commitMessage = generatedMessage;
|
|
||||||
} else {
|
|
||||||
throw new Error('Prompt generated no response');
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
showError('Failed to generate commit message', e);
|
|
||||||
} finally {
|
|
||||||
aiLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
updateHeights();
|
|
||||||
descriptionTextArea.focus();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let aiConfigurationValid = false;
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
aiConfigurationValid = await aiService.validateConfiguration($user?.access_token);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="commit-box" class:commit-box__expanded={$expanded}>
|
<div class="commit-box" class:commit-box__expanded={$expanded}>
|
||||||
{#if $expanded}
|
{#if $expanded}
|
||||||
<div class="commit-box__expander" transition:slide={{ duration: 150, easing: quintOut }}>
|
<div class="commit-box__expander" transition:slide={{ duration: 150, easing: quintOut }}>
|
||||||
<div class="commit-box__textarea-wrapper text-input">
|
<CommitMessageInput bind:commitMessage={$commitMessage} {commit} />
|
||||||
<textarea
|
|
||||||
value={title}
|
|
||||||
placeholder="Commit summary"
|
|
||||||
disabled={aiLoading}
|
|
||||||
class="text-base-body-13 text-semibold commit-box__textarea commit-box__textarea__title"
|
|
||||||
spellcheck="false"
|
|
||||||
rows="1"
|
|
||||||
bind:this={titleTextArea}
|
|
||||||
use:focusTextareaOnMount
|
|
||||||
use:useResize={() => {
|
|
||||||
useAutoHeight(titleTextArea);
|
|
||||||
}}
|
|
||||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
|
||||||
on:input={(e) => {
|
|
||||||
$commitMessage = concatMessage(e.currentTarget.value, description);
|
|
||||||
}}
|
|
||||||
on:keydown={(e) => {
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') commit();
|
|
||||||
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
descriptionTextArea.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if title.length > 0 || description}
|
|
||||||
<textarea
|
|
||||||
value={description}
|
|
||||||
disabled={aiLoading}
|
|
||||||
placeholder="Commit description (optional)"
|
|
||||||
class="text-base-body-13 commit-box__textarea commit-box__textarea__description"
|
|
||||||
spellcheck="false"
|
|
||||||
rows="1"
|
|
||||||
bind:this={descriptionTextArea}
|
|
||||||
use:useResize={() => useAutoHeight(descriptionTextArea)}
|
|
||||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
|
||||||
on:input={(e) => {
|
|
||||||
$commitMessage = concatMessage(title, e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
on:keydown={(e) => {
|
|
||||||
const value = e.currentTarget.value;
|
|
||||||
if (e.key == 'Backspace' && value.length == 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
titleTextArea.focus();
|
|
||||||
useAutoHeight(e.currentTarget);
|
|
||||||
} else if (e.key == 'a' && (e.metaKey || e.ctrlKey) && value.length == 0) {
|
|
||||||
// select previous textarea on cmd+a if this textarea is empty
|
|
||||||
e.preventDefault();
|
|
||||||
titleTextArea.select();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if title.length > 50}
|
|
||||||
<div
|
|
||||||
transition:fly={{ y: 2, duration: 150 }}
|
|
||||||
class="commit-box__textarea-tooltip"
|
|
||||||
use:tooltip={{
|
|
||||||
text: '50 characters or less is best. Extra info can be added in the description.',
|
|
||||||
delay: 200
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="blitz" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="commit-box__texarea-actions"
|
|
||||||
use:tooltip={$aiGenEnabled && aiConfigurationValid
|
|
||||||
? ''
|
|
||||||
: 'You must be logged in or have provided your own API key and have summary generation enabled to use this feature'}
|
|
||||||
>
|
|
||||||
<DropDownButton
|
|
||||||
style="ghost"
|
|
||||||
kind="solid"
|
|
||||||
icon="ai-small"
|
|
||||||
disabled={!($aiGenEnabled && aiConfigurationValid)}
|
|
||||||
loading={aiLoading}
|
|
||||||
on:click={async () => await generateCommitMessage($branch.files)}
|
|
||||||
>
|
|
||||||
Generate message
|
|
||||||
<ContextMenu type="checklist" slot="context-menu" bind:this={contextMenu}>
|
|
||||||
<ContextMenuSection>
|
|
||||||
<ContextMenuItem
|
|
||||||
label="Extra concise"
|
|
||||||
on:click={() => ($commitGenerationExtraConcise = !$commitGenerationExtraConcise)}
|
|
||||||
>
|
|
||||||
<Checkbox small slot="control" bind:checked={$commitGenerationExtraConcise} />
|
|
||||||
</ContextMenuItem>
|
|
||||||
|
|
||||||
<ContextMenuItem
|
|
||||||
label="Use emojis 😎"
|
|
||||||
on:click={() => ($commitGenerationUseEmojis = !$commitGenerationUseEmojis)}
|
|
||||||
>
|
|
||||||
<Checkbox small slot="control" bind:checked={$commitGenerationUseEmojis} />
|
|
||||||
</ContextMenuItem>
|
|
||||||
</ContextMenuSection>
|
|
||||||
</ContextMenu>
|
|
||||||
</DropDownButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@ -260,7 +63,7 @@
|
|||||||
kind="solid"
|
kind="solid"
|
||||||
grow
|
grow
|
||||||
loading={isCommitting}
|
loading={isCommitting}
|
||||||
disabled={(isCommitting || !title || $selectedOwnership.isEmpty()) && $expanded}
|
disabled={(isCommitting || !$commitMessage || $selectedOwnership.isEmpty()) && $expanded}
|
||||||
id="commit-to-branch"
|
id="commit-to-branch"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if ($expanded) {
|
if ($expanded) {
|
||||||
@ -292,57 +95,6 @@
|
|||||||
margin-bottom: var(--size-12);
|
margin-bottom: var(--size-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.commit-box__textarea-wrapper {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
padding: 0 0 var(--size-48);
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--size-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-box__textarea {
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: var(--size-16);
|
|
||||||
background: none;
|
|
||||||
resize: none;
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: oklch(from var(--clr-scale-ntrl-30) l c h / 0.4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-box__textarea-tooltip {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
bottom: var(--size-12);
|
|
||||||
left: var(--size-12);
|
|
||||||
padding: var(--size-2);
|
|
||||||
border-radius: 100%;
|
|
||||||
background: var(--clr-bg-2);
|
|
||||||
color: var(--clr-scale-ntrl-40);
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-box__textarea__title {
|
|
||||||
padding: var(--size-12) var(--size-12) 0 var(--size-12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-box__textarea__description {
|
|
||||||
padding: 0 var(--size-12) 0 var(--size-12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-box__texarea-actions {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
right: var(--size-12);
|
|
||||||
bottom: var(--size-12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
|
266
app/src/lib/components/CommitMessageInput.svelte
Normal file
266
app/src/lib/components/CommitMessageInput.svelte
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AIService } from '$lib/ai/service';
|
||||||
|
import { Project } from '$lib/backend/projects';
|
||||||
|
import Checkbox from '$lib/components/Checkbox.svelte';
|
||||||
|
import DropDownButton from '$lib/components/DropDownButton.svelte';
|
||||||
|
import Icon from '$lib/components/Icon.svelte';
|
||||||
|
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||||
|
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||||
|
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||||
|
import {
|
||||||
|
projectAiGenEnabled,
|
||||||
|
projectCommitGenerationExtraConcise,
|
||||||
|
projectCommitGenerationUseEmojis
|
||||||
|
} from '$lib/config/config';
|
||||||
|
import { showError } from '$lib/notifications/toasts';
|
||||||
|
import { User } from '$lib/stores/user';
|
||||||
|
import { splitMessage } from '$lib/utils/commitMessage';
|
||||||
|
import { getContext, getContextStore } from '$lib/utils/context';
|
||||||
|
import { tooltip } from '$lib/utils/tooltip';
|
||||||
|
import { useAutoHeight } from '$lib/utils/useAutoHeight';
|
||||||
|
import { useResize } from '$lib/utils/useResize';
|
||||||
|
import { Ownership } from '$lib/vbranches/ownership';
|
||||||
|
import { Branch, LocalFile } from '$lib/vbranches/types';
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
|
||||||
|
export let commitMessage: string;
|
||||||
|
export let commit: (() => void) | undefined = undefined;
|
||||||
|
|
||||||
|
const user = getContextStore(User);
|
||||||
|
const selectedOwnership = getContextStore(Ownership);
|
||||||
|
const aiService = getContext(AIService);
|
||||||
|
const branch = getContextStore(Branch);
|
||||||
|
const project = getContext(Project);
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
action: 'generate-branch-name';
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const aiGenEnabled = projectAiGenEnabled(project.id);
|
||||||
|
const commitGenerationExtraConcise = projectCommitGenerationExtraConcise(project.id);
|
||||||
|
const commitGenerationUseEmojis = projectCommitGenerationUseEmojis(project.id);
|
||||||
|
|
||||||
|
let aiLoading = false;
|
||||||
|
let aiConfigurationValid = false;
|
||||||
|
|
||||||
|
let contextMenu: ContextMenu;
|
||||||
|
|
||||||
|
let titleTextArea: HTMLTextAreaElement;
|
||||||
|
let descriptionTextArea: HTMLTextAreaElement;
|
||||||
|
|
||||||
|
$: ({ title, description } = splitMessage(commitMessage));
|
||||||
|
$: if (commitMessage) updateHeights();
|
||||||
|
|
||||||
|
function concatMessage(title: string, description: string) {
|
||||||
|
return `${title}\n\n${description}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHeights() {
|
||||||
|
useAutoHeight(titleTextArea);
|
||||||
|
useAutoHeight(descriptionTextArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusTextareaOnMount(el: HTMLTextAreaElement) {
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateCommitMessage(files: LocalFile[]) {
|
||||||
|
const hunks = files.flatMap((f) =>
|
||||||
|
f.hunks.filter((h) => $selectedOwnership.contains(f.id, h.id))
|
||||||
|
);
|
||||||
|
// Branches get their names generated only if there are at least 4 lines of code
|
||||||
|
// If the change is a 'one-liner', the branch name is either left as "virtual branch"
|
||||||
|
// or the user has to manually trigger the name generation from the meatball menu
|
||||||
|
// This saves people this extra click
|
||||||
|
if ($branch.name.toLowerCase().includes('virtual branch')) {
|
||||||
|
dispatch('action', 'generate-branch-name');
|
||||||
|
}
|
||||||
|
|
||||||
|
aiLoading = true;
|
||||||
|
try {
|
||||||
|
const generatedMessage = await aiService.summarizeCommit({
|
||||||
|
hunks,
|
||||||
|
useEmojiStyle: $commitGenerationUseEmojis,
|
||||||
|
useBriefStyle: $commitGenerationExtraConcise,
|
||||||
|
userToken: $user?.access_token
|
||||||
|
});
|
||||||
|
|
||||||
|
if (generatedMessage) {
|
||||||
|
commitMessage = generatedMessage;
|
||||||
|
} else {
|
||||||
|
throw new Error('Prompt generated no response');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
showError('Failed to generate commit message', e);
|
||||||
|
} finally {
|
||||||
|
aiLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
updateHeights();
|
||||||
|
descriptionTextArea.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
aiConfigurationValid = await aiService.validateConfiguration($user?.access_token);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="commit-box__textarea-wrapper text-input">
|
||||||
|
<textarea
|
||||||
|
value={title}
|
||||||
|
placeholder="Commit summary"
|
||||||
|
disabled={aiLoading}
|
||||||
|
class="text-base-body-13 text-semibold commit-box__textarea commit-box__textarea__title"
|
||||||
|
spellcheck="false"
|
||||||
|
rows="1"
|
||||||
|
bind:this={titleTextArea}
|
||||||
|
use:focusTextareaOnMount
|
||||||
|
use:useResize={() => {
|
||||||
|
useAutoHeight(titleTextArea);
|
||||||
|
}}
|
||||||
|
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||||
|
on:input={(e) => {
|
||||||
|
commitMessage = concatMessage(e.currentTarget.value, description);
|
||||||
|
}}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (commit && (e.ctrlKey || e.metaKey) && e.key === 'Enter') commit();
|
||||||
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
descriptionTextArea.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if title.length > 0 || description}
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
disabled={aiLoading}
|
||||||
|
placeholder="Commit description (optional)"
|
||||||
|
class="text-base-body-13 commit-box__textarea commit-box__textarea__description"
|
||||||
|
spellcheck="false"
|
||||||
|
rows="1"
|
||||||
|
bind:this={descriptionTextArea}
|
||||||
|
use:useResize={() => useAutoHeight(descriptionTextArea)}
|
||||||
|
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||||
|
on:input={(e) => {
|
||||||
|
commitMessage = concatMessage(title, e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
const value = e.currentTarget.value;
|
||||||
|
if (e.key == 'Backspace' && value.length == 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
titleTextArea.focus();
|
||||||
|
useAutoHeight(e.currentTarget);
|
||||||
|
} else if (e.key == 'a' && (e.metaKey || e.ctrlKey) && value.length == 0) {
|
||||||
|
// select previous textarea on cmd+a if this textarea is empty
|
||||||
|
e.preventDefault();
|
||||||
|
titleTextArea.select();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if title.length > 50}
|
||||||
|
<div
|
||||||
|
transition:fly={{ y: 2, duration: 150 }}
|
||||||
|
class="commit-box__textarea-tooltip"
|
||||||
|
use:tooltip={{
|
||||||
|
text: '50 characters or less is best. Extra info can be added in the description.',
|
||||||
|
delay: 200
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="blitz" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="commit-box__texarea-actions"
|
||||||
|
use:tooltip={$aiGenEnabled && aiConfigurationValid
|
||||||
|
? ''
|
||||||
|
: 'You must be logged in or have provided your own API key and have summary generation enabled to use this feature'}
|
||||||
|
>
|
||||||
|
<DropDownButton
|
||||||
|
style="ghost"
|
||||||
|
kind="solid"
|
||||||
|
icon="ai-small"
|
||||||
|
disabled={!($aiGenEnabled && aiConfigurationValid)}
|
||||||
|
loading={aiLoading}
|
||||||
|
on:click={async () => await generateCommitMessage($branch.files)}
|
||||||
|
>
|
||||||
|
Generate message
|
||||||
|
<ContextMenu type="checklist" slot="context-menu" bind:this={contextMenu}>
|
||||||
|
<ContextMenuSection>
|
||||||
|
<ContextMenuItem
|
||||||
|
label="Extra concise"
|
||||||
|
on:click={() => ($commitGenerationExtraConcise = !$commitGenerationExtraConcise)}
|
||||||
|
>
|
||||||
|
<Checkbox small slot="control" bind:checked={$commitGenerationExtraConcise} />
|
||||||
|
</ContextMenuItem>
|
||||||
|
|
||||||
|
<ContextMenuItem
|
||||||
|
label="Use emojis 😎"
|
||||||
|
on:click={() => ($commitGenerationUseEmojis = !$commitGenerationUseEmojis)}
|
||||||
|
>
|
||||||
|
<Checkbox small slot="control" bind:checked={$commitGenerationUseEmojis} />
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuSection>
|
||||||
|
</ContextMenu>
|
||||||
|
</DropDownButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.commit-box__textarea-wrapper {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 0 var(--size-48);
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-box__textarea {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: var(--size-16);
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: oklch(from var(--clr-scale-ntrl-30) l c h / 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-box__textarea-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
bottom: var(--size-12);
|
||||||
|
left: var(--size-12);
|
||||||
|
padding: var(--size-2);
|
||||||
|
border-radius: 100%;
|
||||||
|
background: var(--clr-bg-2);
|
||||||
|
color: var(--clr-scale-ntrl-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-box__textarea__title {
|
||||||
|
padding: var(--size-12) var(--size-12) 0 var(--size-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-box__textarea__description {
|
||||||
|
padding: 0 var(--size-12) 0 var(--size-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-box__texarea-actions {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
right: var(--size-12);
|
||||||
|
bottom: var(--size-12);
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user