import { useState, useEffect, useRef } from 'react' import { Sparkle } from '@phosphor-icons/react' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Textarea } from '@/components/ui/textarea' import { formatShortcutDisplay } from '../hooks/appCommandCatalog' import type { CommitMode } from '../hooks/useCommitFlow' import { GitRepositorySelect } from './GitRepositorySelect' import type { GitRepositoryOption } from '../utils/gitRepositories' import { translate, type AppLocale } from '../lib/i18n' import type { GitAuthorIdentity } from '../types' type CommitDialogCopy = { title: string description: string actionLabel: string shortcutHint: string } const getDialogCopy = (commitMode: CommitMode): CommitDialogCopy => { const submitShortcut = formatShortcutDisplay({ display: '⌘↵' }) if (commitMode === 'local') { return { title: 'Commit', description: 'This vault has no git remote configured. Tolaria will create a local commit only.', actionLabel: 'Commit', shortcutHint: `${submitShortcut} to commit locally`, } } return { title: 'Commit & Push', description: 'Review changed files and enter a commit message before committing and pushing.', actionLabel: 'Commit & Push', shortcutHint: `${submitShortcut} to commit`, } } const changedFilesLabel = (modifiedCount: number): string => ( `${modifiedCount} file${modifiedCount !== 1 ? 's' : ''} changed` ) const isSubmitShortcut = (event: React.KeyboardEvent): boolean => ( event.key === 'Enter' && (event.metaKey || event.ctrlKey) ) const isCloseShortcut = (event: React.KeyboardEvent): boolean => event.key === 'Escape' const formatAuthorIdentity = (identity: GitAuthorIdentity): string => ( `${identity.name} <${identity.email}>` ) const formatOptionalAuthorIdentity = (identity: GitAuthorIdentity | null): string => ( identity ? formatAuthorIdentity(identity) : '' ) const focusMessageInput = (inputRef: React.RefObject): void => { setTimeout(() => inputRef.current?.focus(), 50) } const generationButtonDisabled = ({ isGenerating, modifiedCount, onGenerateMessage, }: { isGenerating: boolean modifiedCount: number onGenerateMessage?: () => Promise | string }): boolean => !onGenerateMessage || isGenerating || modifiedCount === 0 const authorWarningText = (identity: GitAuthorIdentity, locale: AppLocale): string | null => { if (identity.warning === 'local_overrides_global') { return translate(locale, 'git.author.warning.localOverridesGlobal') } return null } const CommitAuthorIdentity = ({ identity, locale, }: { identity: GitAuthorIdentity | null locale: AppLocale }) => { const hidden = identity === null const warningText = identity ? authorWarningText(identity, locale) : null return ( ) } function useGeneratedCommitMessage({ generatedMessage, generatedMessageKey, inputRef, open, setMessage, }: { generatedMessage?: string generatedMessageKey: number inputRef: React.RefObject open: boolean setMessage: (message: string) => void }) { useEffect(() => { if (!open || generatedMessageKey === 0 || !generatedMessage) return setMessage(generatedMessage) focusMessageInput(inputRef) }, [generatedMessage, generatedMessageKey, inputRef, open, setMessage]) } function CommitMessageGenerateButton({ isGeneratingMessage, locale, modifiedCount, onGenerateMessage, onGeneratedMessage, }: { isGeneratingMessage: boolean locale: AppLocale modifiedCount: number onGenerateMessage?: () => Promise | string onGeneratedMessage: (message: string) => void }) { const handleGenerateMessage = async () => { const generated = await onGenerateMessage?.() if (generated) onGeneratedMessage(generated) } return ( ) } function CommitDialogActions({ actionLabel, isGeneratingMessage, locale, message, modifiedCount, onClose, onGenerateMessage, onGeneratedMessage, onSubmit, shortcutHint, }: CommitDialogCopy & { isGeneratingMessage: boolean locale: AppLocale message: string modifiedCount: number onClose: () => void onGenerateMessage?: () => Promise | string onGeneratedMessage: (message: string) => void onSubmit: () => void }) { return ( {shortcutHint}
) } interface CommitDialogProps { open: boolean modifiedCount: number authorIdentity?: GitAuthorIdentity | null commitMode?: CommitMode locale?: AppLocale repositories?: GitRepositoryOption[] selectedRepositoryPath?: string generatedMessage?: string generatedMessageKey?: number isGeneratingMessage?: boolean suggestedMessage?: string onGenerateMessage?: () => Promise | string onRepositoryChange?: (path: string) => void onCommit: (message: string) => void onClose: () => void } export function CommitDialog(props: CommitDialogProps) { const { open, modifiedCount, authorIdentity = null, commitMode = 'push', locale = 'en', repositories = [], selectedRepositoryPath = '', generatedMessage, generatedMessageKey = 0, isGeneratingMessage = false, suggestedMessage, onGenerateMessage, onRepositoryChange, onCommit, onClose, } = props const [message, setMessage] = useState('') const inputRef = useRef(null) const suggestedMessageRef = useRef(suggestedMessage) const copy = getDialogCopy(commitMode) useEffect(() => { suggestedMessageRef.current = suggestedMessage }, [suggestedMessage]) useEffect(() => { if (open) { setMessage(suggestedMessageRef.current ?? '') // eslint-disable-line react-hooks/set-state-in-effect -- reset on dialog open focusMessageInput(inputRef) } }, [open]) useGeneratedCommitMessage({ generatedMessage, generatedMessageKey, inputRef, open, setMessage }) const handleSubmit = () => { const trimmed = message.trim() if (!trimmed) return onCommit(trimmed) } const handleKeyDown = (e: React.KeyboardEvent) => { if (isSubmitShortcut(e)) { e.preventDefault() handleSubmit() } else if (isCloseShortcut(e)) { onClose() } } const handleGeneratedMessage = (generated: string) => { setMessage(generated) focusMessageInput(inputRef) } return ( { if (!isOpen) onClose() }}>
{copy.title} {changedFilesLabel(modifiedCount)}
{copy.description}
{onRepositoryChange && selectedRepositoryPath && ( )}