feat: Add AI-generated commit messages

Integrate Claude Haiku to automatically generate commit messages when
committing worktree changes. Shows a sparkle animation while generating
and auto-populates the commit message field.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
anonymous
2026-01-10 21:10:33 -08:00
committed by Shirone
parent d2c7a9e05d
commit e56db2362c
7 changed files with 265 additions and 7 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
@@ -10,7 +10,7 @@ import {
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { GitCommit, Loader2 } from 'lucide-react';
import { GitCommit, Loader2, Sparkles } from 'lucide-react';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
@@ -37,6 +37,7 @@ export function CommitWorktreeDialog({
}: CommitWorktreeDialogProps) {
const [message, setMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleCommit = async () => {
@@ -82,6 +83,45 @@ export function CommitWorktreeDialog({
}
};
// Generate AI commit message when dialog opens
useEffect(() => {
if (open && worktree) {
// Reset state
setMessage('');
setError(null);
setIsGenerating(true);
const generateMessage = async () => {
try {
const api = getElectronAPI();
if (!api?.worktree?.generateCommitMessage) {
setError('AI commit message generation not available');
setIsGenerating(false);
return;
}
const result = await api.worktree.generateCommitMessage(worktree.path);
if (result.success && result.message) {
setMessage(result.message);
} else {
// Don't show error toast, just log it and leave message empty
console.warn('Failed to generate commit message:', result.error);
setMessage('');
}
} catch (err) {
// Don't show error toast for generation failures
console.warn('Error generating commit message:', err);
setMessage('');
} finally {
setIsGenerating(false);
}
};
generateMessage();
}
}, [open, worktree]);
if (!worktree) return null;
return (
@@ -106,10 +146,20 @@ export function CommitWorktreeDialog({
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="commit-message">Commit Message</Label>
<Label htmlFor="commit-message" className="flex items-center gap-2">
Commit Message
{isGenerating && (
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Sparkles className="w-3 h-3 animate-pulse" />
Generating...
</span>
)}
</Label>
<Textarea
id="commit-message"
placeholder="Describe your changes..."
placeholder={
isGenerating ? 'Generating commit message...' : 'Describe your changes...'
}
value={message}
onChange={(e) => {
setMessage(e.target.value);
@@ -118,6 +168,7 @@ export function CommitWorktreeDialog({
onKeyDown={handleKeyDown}
className="min-h-[100px] font-mono text-sm"
autoFocus
disabled={isGenerating}
/>
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
@@ -128,10 +179,14 @@ export function CommitWorktreeDialog({
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isLoading}>
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
disabled={isLoading || isGenerating}
>
Cancel
</Button>
<Button onClick={handleCommit} disabled={isLoading || !message.trim()}>
<Button onClick={handleCommit} disabled={isLoading || isGenerating || !message.trim()}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />

View File

@@ -1537,6 +1537,15 @@ function createMockWorktreeAPI(): WorktreeAPI {
};
},
generateCommitMessage: async (worktreePath: string) => {
console.log('[Mock] Generating commit message:', { worktreePath });
return {
success: true,
message:
'feat: Add new feature implementation\n\nThis is a mock AI-generated commit message.',
};
},
push: async (worktreePath: string, force?: boolean) => {
console.log('[Mock] Pushing worktree:', { worktreePath, force });
return {

View File

@@ -1681,6 +1681,8 @@ export class HttpApiClient implements ElectronAPI {
}),
commit: (worktreePath: string, message: string) =>
this.post('/api/worktree/commit', { worktreePath, message }),
generateCommitMessage: (worktreePath: string) =>
this.post('/api/worktree/generate-commit-message', { worktreePath }),
push: (worktreePath: string, force?: boolean) =>
this.post('/api/worktree/push', { worktreePath, force }),
createPR: (worktreePath: string, options?: any) =>

View File

@@ -770,6 +770,13 @@ export interface WorktreeAPI {
error?: string;
}>;
// Generate an AI commit message from git diff
generateCommitMessage: (worktreePath: string) => Promise<{
success: boolean;
message?: string;
error?: string;
}>;
// Push a worktree branch to remote
push: (
worktreePath: string,