Merge pull request #3134 from gitbutlerapp/Commit-box-two-fields

Add summary and description fields. Show tooltip for long summary messages
This commit is contained in:
Pavel Laptev 2024-03-15 15:15:29 +01:00 committed by GitHub
commit 229b9b9bc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 198 additions and 91 deletions

View File

@ -2,7 +2,7 @@
import BranchFiles from './BranchFiles.svelte';
import Tag from '$lib/components/Tag.svelte';
import TimeAgo from '$lib/components/TimeAgo.svelte';
import { projectCurrentCommitMessage } from '$lib/config/config';
import { persistedCommitMessage } from '$lib/config/config';
import { draggable } from '$lib/dragging/draggable';
import { draggableCommit, nonDraggable } from '$lib/dragging/draggables';
import { openExternalUrl } from '$lib/utils/url';
@ -34,7 +34,7 @@
export let branchId: string | undefined = undefined;
const selectedOwnership = writable(Ownership.default());
const currentCommitMessage = projectCurrentCommitMessage(projectId, branchId || '');
const currentCommitMessage = persistedCommitMessage(projectId, branchId || '');
let showFiles = false;

View File

@ -4,6 +4,7 @@
import Button from '$lib/components/Button.svelte';
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';
@ -12,17 +13,16 @@
projectCommitGenerationExtraConcise,
projectCommitGenerationUseEmojis,
projectRunCommitHooks,
projectCurrentCommitMessage
persistedCommitMessage
} from '$lib/config/config';
import { persisted } from '$lib/persisted/persisted';
import * as toasts from '$lib/utils/toasts';
import { tooltip } from '$lib/utils/tooltip';
import { useAutoHeight } from '$lib/utils/useAutoHeight';
import { invoke } from '@tauri-apps/api/tauri';
import { setAutoHeight } from '$lib/utils/useAutoHeight';
import { useResize } from '$lib/utils/useResize';
import { createEventDispatcher } from 'svelte';
import { quintOut } from 'svelte/easing';
import { get } from 'svelte/store';
import { slide } from 'svelte/transition';
import { fly, slide } from 'svelte/transition';
import type { User, getCloudApiClient } from '$lib/backend/cloud';
import type { BranchController } from '$lib/vbranches/branchController';
import type { Ownership } from '$lib/vbranches/ownership';
@ -39,49 +39,66 @@
export let cloud: ReturnType<typeof getCloudApiClient>;
export let user: User | undefined;
export let selectedOwnership: Writable<Ownership>;
export const expanded = persisted<boolean>(false, 'commitBoxExpanded_' + branch.id);
const aiGenEnabled = projectAiGenEnabled(projectId);
const runCommitHooks = projectRunCommitHooks(projectId);
const currentCommitMessage = projectCurrentCommitMessage(projectId, branch.id);
export const expanded = persisted<boolean>(false, 'commitBoxExpanded_' + branch.id);
const commitMessage = persistedCommitMessage(projectId, branch.id);
const commitGenerationExtraConcise = projectCommitGenerationExtraConcise(projectId);
const commitGenerationUseEmojis = projectCommitGenerationUseEmojis(projectId);
let commitMessage: string = get(currentCommitMessage) || '';
let isCommitting = false;
let textareaElement: HTMLTextAreaElement;
$: if (textareaElement && commitMessage && expanded) {
textareaElement.style.height = 'auto';
textareaElement.style.height = `${textareaElement.scrollHeight + 2}px`;
}
function focusTextareaOnMount(el: HTMLTextAreaElement) {
if (el) el.focus();
}
function commit() {
if (!commitMessage) return;
isCommitting = true;
branchController
.commitBranch(branch.id, commitMessage, $selectedOwnership.toString(), $runCommitHooks)
.then(() => {
commitMessage = '';
currentCommitMessage.set('');
})
.finally(() => (isCommitting = false));
}
export function git_get_config(params: { key: string }) {
return invoke<string>('git_get_global_config', params);
}
let aiLoading = false;
let contextMenu: ContextMenu;
let summarizer: Summarizer | undefined;
let titleTextArea: HTMLTextAreaElement;
let descriptionTextArea: HTMLTextAreaElement;
$: [title, description] = splitMessage($commitMessage);
$: if ($commitMessage) updateHeights();
$: if (user) {
const aiProvider = new ButlerAIProvider(cloud, user);
summarizer = new Summarizer(aiProvider);
}
let isGeneratingCommitMessage = false;
function splitMessage(message: string) {
const parts = message.split(/\n+(.*)/s);
return [parts[0] || '', parts[1] || ''];
}
function concatMessage(title: string, description: string) {
return `${title}\n\n${description}`;
}
function focusTextareaOnMount(el: HTMLTextAreaElement) {
el.focus();
}
function updateHeights() {
setAutoHeight(titleTextArea);
setAutoHeight(descriptionTextArea);
}
async function commit() {
const message = concatMessage(title, description);
isCommitting = true;
try {
await branchController.commitBranch(
branch.id,
message.trim(),
$selectedOwnership.toString(),
$runCommitHooks
);
$commitMessage = '';
} finally {
isCommitting = false;
}
}
async function generateCommitMessage(files: LocalFile[]) {
if (!user || !summarizer) return;
const diff = files
.map((f) => f.hunks.filter((h) => $selectedOwnership.containsHunk(f.id, h.id)))
.flat()
@ -90,9 +107,6 @@
.join('\n')
.slice(0, 5000);
if (!user) return;
if (!summarizer) return;
// 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
@ -101,53 +115,99 @@
dispatch('action', 'generate-branch-name');
}
isGeneratingCommitMessage = true;
summarizer
.commit(diff, $commitGenerationUseEmojis, $commitGenerationExtraConcise)
.then((message) => {
commitMessage = message;
currentCommitMessage.set(message);
aiLoading = true;
try {
$commitMessage = await summarizer.commit(
diff,
$commitGenerationUseEmojis,
$commitGenerationExtraConcise
);
} catch {
toasts.error('Failed to generate commit message');
} finally {
aiLoading = false;
}
setTimeout(() => {
textareaElement.focus();
}, 0);
})
.catch(() => {
toasts.error('Failed to generate commit message');
})
.finally(() => {
isGeneratingCommitMessage = false;
});
setTimeout(() => {
updateHeights();
descriptionTextArea.focus();
}, 0);
}
const commitGenerationExtraConcise = projectCommitGenerationExtraConcise(projectId);
const commitGenerationUseEmojis = projectCommitGenerationUseEmojis(projectId);
let contextMenu: ContextMenu;
</script>
<div class="commit-box" class:commit-box__expanded={$expanded}>
{#if $expanded}
<div class="commit-box__expander" transition:slide={{ duration: 150, easing: quintOut }}>
<div class="commit-box__textarea-wrapper">
<div class="commit-box__textarea-wrapper text-input">
<textarea
bind:this={textareaElement}
bind:value={commitMessage}
value={title}
placeholder="Commit summary"
disabled={aiLoading}
class="text-base-body-13 text-semibold commit-box__textarea commit-box__textarea__title"
class:commit-box__textarea_bottom-padding={title.length == 0 && description.length == 0}
spellcheck="false"
rows="1"
bind:this={titleTextArea}
use:focusTextareaOnMount
on:input={useAutoHeight}
on:focus={useAutoHeight}
on:change={() => currentCommitMessage.set(commitMessage)}
use:useResize={() => {
setAutoHeight(titleTextArea);
setAutoHeight(descriptionTextArea);
}}
on:focus={(e) => setAutoHeight(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.ctrlKey || e.metaKey) && e.key === 'Enter') commit();
if (e.key === 'Tab' || e.key === 'Enter') {
e.preventDefault();
descriptionTextArea.focus();
}
}}
spellcheck={false}
class="text-input text-base-body-13 commit-box__textarea"
rows="1"
disabled={isGeneratingCommitMessage}
placeholder="Your commit message here"
/>
{#if title.length > 0}
<textarea
value={description}
disabled={aiLoading}
placeholder="Commit description (optional)"
class="text-base-body-13 commit-box__textarea commit-box__textarea__description"
class:commit-box__textarea_bottom-padding={description.length > 0 || title.length > 0}
spellcheck="false"
rows="1"
bind:this={descriptionTextArea}
on:focus={(e) => setAutoHeight(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();
setAutoHeight(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 && user
@ -159,7 +219,7 @@
icon="ai-small"
color="neutral"
disabled={!$aiGenEnabled || !user}
loading={isGeneratingCommitMessage}
loading={aiLoading}
on:click={() => generateCommitMessage(branch.files)}
>
Generate message
@ -203,7 +263,7 @@
color="primary"
kind="filled"
loading={isCommitting}
disabled={(isCommitting || !commitMessage || $selectedOwnership.isEmpty()) && $expanded}
disabled={(isCommitting || !title || $selectedOwnership.isEmpty()) && $expanded}
id="commit-to-branch"
on:click={() => {
if ($expanded) {
@ -228,27 +288,57 @@
transition: background-color var(--transition-medium);
border-radius: 0 0 var(--radius-m) var(--radius-m);
}
.commit-box__expander {
display: flex;
flex-direction: column;
margin-bottom: var(--space-12);
}
.commit-box__textarea-wrapper {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: 0;
}
.commit-box__textarea {
overflow: hidden;
display: flex;
flex-direction: column;
padding: var(--space-12) var(--space-12) var(--space-48) var(--space-12);
align-items: flex-end;
gap: var(--space-16);
background: none;
resize: none;
&:focus {
outline: none;
}
}
.commit-box__textarea-tooltip {
position: absolute;
display: flex;
bottom: var(--space-12);
left: var(--space-12);
padding: var(--space-2);
border-radius: 100%;
background: var(--clr-theme-container-pale);
color: var(--clr-theme-scale-ntrl-40);
}
.commit-box__textarea__title {
padding: var(--space-12) var(--space-12) 0 var(--space-12);
}
.commit-box__textarea__description {
padding: 0 var(--space-12) var(--space-12) var(--space-12);
}
.commit-box__textarea_bottom-padding {
padding-bottom: var(--space-48);
}
.commit-box__texarea-actions {
position: absolute;
display: flex;
@ -262,7 +352,6 @@
gap: var(--space-6);
}
/* modifiers */
.commit-box__expanded {
background-color: var(--clr-theme-container-pale);
}

View File

@ -51,10 +51,6 @@ export function projectLaneCollapsed(projectId: string, laneId: string): Persist
return persisted(false, key + projectId + '_' + laneId);
}
export function projectCurrentCommitMessage(
projectId: string,
branchId: string
): Persisted<string> {
const key = 'projectCurrentCommitMessage_';
return persisted('', key + projectId + '_' + branchId);
export function persistedCommitMessage(projectId: string, branchId: string): Persisted<string> {
return persisted('', 'projectCurrentCommitMessage_' + projectId + '_' + branchId);
}

View File

@ -40,6 +40,10 @@
"info-small": "M8 2.5C4.96244 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8C13.5 4.96243 11.0376 2.5 8 2.5ZM9 8.57143C9 8.01914 8.55229 7.57143 8 7.57143C7.44772 7.57143 7 8.01914 7 8.57143V10C7 10.5523 7.44772 11 8 11C8.55229 11 9 10.5523 9 10V8.57143ZM9 5.85714C9 5.38376 8.61625 5 8.14286 5H7.85715C7.38376 5 7 5.38376 7 5.85714C7 6.33053 7.38376 6.71429 7.85714 6.71429H8.14286C8.61625 6.71429 9 6.33053 9 5.85714Z",
"instagram": "M1 5C1 2.79086 2.79086 1 5 1H11C13.2091 1 15 2.79086 15 5V11C15 13.2091 13.2091 15 11 15H5C2.79086 15 1 13.2091 1 11V5ZM11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8ZM4 5C4.55228 5 5 4.55228 5 4C5 3.44772 4.55228 3 4 3C3.44772 3 3 3.44772 3 4C3 4.55228 3.44772 5 4 5Z",
"integrations": "M4.96429 2C4.96429 1.0335 5.74779 0.25 6.71429 0.25H7.5C8.4665 0.25 9.25 1.0335 9.25 2V2.78571C9.25 2.92379 9.36193 3.03571 9.5 3.03571H11.2143C12.1808 3.03571 12.9643 3.81922 12.9643 4.78571V6.5C12.9643 6.63807 13.0762 6.75 13.2143 6.75H14C14.9665 6.75 15.75 7.5335 15.75 8.5V9.28571C15.75 10.2522 14.9665 11.0357 14 11.0357H13.2143C13.0762 11.0357 12.9643 11.1476 12.9643 11.2857V13C12.9643 13.9665 12.1808 14.75 11.2143 14.75H9.5C8.5335 14.75 7.75 13.9665 7.75 13V12.2143C7.75 12.0762 7.63807 11.9643 7.5 11.9643H6.71429C6.57621 11.9643 6.46429 12.0762 6.46429 12.2143V13C6.46429 13.9665 5.68078 14.75 4.71429 14.75H3C2.0335 14.75 1.25 13.9665 1.25 13V11.2857C1.25 10.3192 2.0335 9.53571 3 9.53571H3.78571C3.92379 9.53571 4.03571 9.42379 4.03571 9.28571V8.5C4.03571 8.36193 3.92379 8.25 3.78571 8.25H3C2.0335 8.25 1.25 7.4665 1.25 6.5V4.78571C1.25 3.81922 2.0335 3.03571 3 3.03571H4.71429C4.85236 3.03571 4.96429 2.92379 4.96429 2.78571V2ZM6.71429 1.75C6.57621 1.75 6.46429 1.86193 6.46429 2V2.78571C6.46429 3.75221 5.68078 4.53571 4.71429 4.53571H3C2.86193 4.53571 2.75 4.64764 2.75 4.78571V6.5C2.75 6.63807 2.86193 6.75 3 6.75H3.78571C4.75221 6.75 5.53571 7.5335 5.53571 8.5V9.28571C5.53571 10.2522 4.75221 11.0357 3.78571 11.0357H3C2.86193 11.0357 2.75 11.1476 2.75 11.2857V13C2.75 13.1381 2.86193 13.25 3 13.25H4.71429C4.85236 13.25 4.96429 13.1381 4.96429 13V12.2143C4.96429 11.2478 5.74779 10.4643 6.71429 10.4643H7.5C8.4665 10.4643 9.25 11.2478 9.25 12.2143V13C9.25 13.1381 9.36193 13.25 9.5 13.25H11.2143C11.3524 13.25 11.4643 13.1381 11.4643 13V11.2857C11.4643 10.3192 12.2478 9.53571 13.2143 9.53571H14C14.1381 9.53571 14.25 9.42379 14.25 9.28571V8.5C14.25 8.36193 14.1381 8.25 14 8.25H13.2143C12.2478 8.25 11.4643 7.4665 11.4643 6.5V4.78571C11.4643 4.64764 11.3524 4.53571 11.2143 4.53571H9.5C8.5335 4.53571 7.75 3.75221 7.75 2.78571V2C7.75 1.86193 7.63807 1.75 7.5 1.75H6.71429Z",
"idea": "M2.25 5.94187C2.25 3.07448 4.57448 0.75 7.44187 0.75H8.55813C11.4255 0.75 13.75 3.07448 13.75 5.94187C13.75 7.1208 13.3488 8.26463 12.6123 9.18521L11.6201 10.4254C11.4453 10.644 11.35 10.9156 11.35 11.1955V11.9C11.35 13.7502 9.85015 15.25 8 15.25C6.14985 15.25 4.65 13.7502 4.65 11.9V11.1955C4.65 10.9156 4.55474 10.644 4.37988 10.4254L3.3877 9.18521C2.65123 8.26463 2.25 7.1208 2.25 5.94187ZM7.44187 2.25C5.40291 2.25 3.75 3.90291 3.75 5.94187C3.75 6.78019 4.03531 7.59355 4.55901 8.24817L5.55118 9.48839C5.7476 9.73391 5.89871 10.0096 6 10.3026V10.25H7.25V7.81066L5.46967 6.03033L6.53033 4.96967L8 6.43934L9.46967 4.96967L10.5303 6.03033L8.75 7.81066V10.25H10V10.3026C10.1013 10.0096 10.2524 9.73391 10.4488 9.48839L11.441 8.24817C11.9647 7.59355 12.25 6.78019 12.25 5.94187C12.25 3.90291 10.5971 2.25 8.55813 2.25H7.44187ZM6.15 11.9V11.75H9.85V11.9C9.85 12.9217 9.02173 13.75 8 13.75C6.97827 13.75 6.15 12.9217 6.15 11.9Z",
"idea-small": "M3.40384 6.41683C3.40384 4.11555 5.26939 2.25 7.57067 2.25H8.42932C10.7306 2.25 12.5961 4.11555 12.5961 6.41683C12.5961 7.36299 12.2741 8.28099 11.6831 9.01982L10.9199 9.97384C10.8099 10.1113 10.75 10.2821 10.75 10.4581V11C10.75 12.5188 9.51878 13.75 7.99999 13.75C6.48121 13.75 5.24999 12.5188 5.24999 11V10.4581C5.24999 10.2821 5.19009 10.1113 5.08013 9.97384L4.31692 9.01982C3.72585 8.28099 3.40384 7.36299 3.40384 6.41683ZM7.57067 3.75C6.09782 3.75 4.90384 4.94398 4.90384 6.41683C4.90384 7.02239 5.10993 7.60992 5.48822 8.08278L6.25144 9.03679C6.37941 9.19676 6.48428 9.37211 6.56425 9.55769H7.25001V7.92605L5.93121 6.60725L6.99187 5.54659L8 6.55473L9.00814 5.54659L10.0688 6.60725L8.75 7.92605V9.55769H9.43573C9.51571 9.37211 9.62058 9.19676 9.74855 9.03679L10.5118 8.08278C10.8901 7.60992 11.0961 7.02239 11.0961 6.41683C11.0961 4.94398 9.90217 3.75 8.42932 3.75H7.57067ZM7.99999 12.25C7.32897 12.25 6.78145 11.7213 6.7513 11.0577H9.24868C9.21854 11.7213 8.67101 12.25 7.99999 12.25Z",
"blitz": "M7.864 3.6025L5.55525 7.2965C5.15978 7.92925 5.61469 8.75 6.36085 8.75H8.64681L6.864 11.6025L8.136 12.3975L10.4447 8.7035C10.8402 8.07075 10.3853 7.25 9.63915 7.25H7.35319L9.136 4.3975L7.864 3.6025Z M8 0.25C3.71979 0.25 0.25 3.71979 0.25 8C0.25 12.2802 3.71979 15.75 8 15.75C12.2802 15.75 15.75 12.2802 15.75 8C15.75 3.71979 12.2802 0.25 8 0.25ZM1.75 8C1.75 4.54822 4.54822 1.75 8 1.75C11.4518 1.75 14.25 4.54822 14.25 8C14.25 11.4518 11.4518 14.25 8 14.25C4.54822 14.25 1.75 11.4518 1.75 8Z",
"blitz-small": "M8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8C13.5 4.96243 11.0376 2.5 8 2.5ZM7.739 4.6025L6.05525 7.2965C5.65979 7.92925 6.11469 8.75 6.86085 8.75L8.14682 8.75L6.989 10.6025L8.261 11.3975L9.94475 8.7035C10.3402 8.07075 9.88532 7.25 9.13915 7.25L7.85319 7.25L9.011 5.3975L7.739 4.6025Z",
"kebab": "M4 8C4 8.82843 3.32843 9.5 2.5 9.5C1.67157 9.5 1 8.82843 1 8C1 7.17157 1.67157 6.5 2.5 6.5C3.32843 6.5 4 7.17157 4 8Z M9.5 8C9.5 8.82843 8.82843 9.5 8 9.5C7.17157 9.5 6.5 8.82843 6.5 8C6.5 7.17157 7.17157 6.5 8 6.5C8.82843 6.5 9.5 7.17157 9.5 8Z M13.5 9.5C14.3284 9.5 15 8.82843 15 8C15 7.17157 14.3284 6.5 13.5 6.5C12.6716 6.5 12 7.17157 12 8C12 8.82843 12.6716 9.5 13.5 9.5Z",
"list-view": "M2 3.25H14V4.75H2V3.25Z M2 7.25H14V8.75H2V7.25Z M14 11.25H2V12.75H14V11.25Z",
"locked": "M8.75 9V12H7.25V9H8.75Z M8 1.25C5.92893 1.25 4.25 2.92893 4.25 5V6.25H4C3.0335 6.25 2.25 7.0335 2.25 8V13C2.25 13.9665 3.0335 14.75 4 14.75H12C12.9665 14.75 13.75 13.9665 13.75 13V8C13.75 7.0335 12.9665 6.25 12 6.25H11.75V5C11.75 2.92893 10.0711 1.25 8 1.25ZM10.25 6.25V5C10.25 3.75736 9.24264 2.75 8 2.75C6.75736 2.75 5.75 3.75736 5.75 5V6.25H10.25ZM3.75 8C3.75 7.86193 3.86193 7.75 4 7.75H12C12.1381 7.75 12.25 7.86193 12.25 8V13C12.25 13.1381 12.1381 13.25 12 13.25H4C3.86193 13.25 3.75 13.1381 3.75 13V8Z",

View File

@ -1,6 +1,5 @@
export function useAutoHeight(event: Event) {
const textarea = event.target as HTMLTextAreaElement;
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight + 2}px`;
export function setAutoHeight(element: HTMLTextAreaElement) {
if (!element) return;
element.style.height = 'auto';
element.style.height = `${element.scrollHeight + 2}px`;
}

View File

@ -0,0 +1,17 @@
export function useResize(element: HTMLElement, callback: (width: number, height: number) => void) {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
callback(width, height);
}
});
resizeObserver.observe(element);
return {
destroy() {
resizeObserver.unobserve(element);
}
};
}

View File

@ -64,6 +64,7 @@ export class BranchController {
} catch (err: any) {
toasts.error('Failed to commit changes');
posthog.capture('Commit Failed', err);
throw err;
}
}

View File

@ -18,7 +18,8 @@
);
}
&:focus {
&:focus,
&:focus-within {
background: color-mix(
in srgb,
var(--clr-theme-container-light),