diff --git a/app/src/lib/ai/anthropicClient.ts b/app/src/lib/ai/anthropicClient.ts index f68532ef9c..c4296ffacd 100644 --- a/app/src/lib/ai/anthropicClient.ts +++ b/app/src/lib/ai/anthropicClient.ts @@ -1,8 +1,12 @@ import { SHORT_DEFAULT_COMMIT_TEMPLATE, SHORT_DEFAULT_BRANCH_TEMPLATE } from '$lib/ai/prompts'; +import { err, ok, type Result } from '$lib/result'; import { fetch, Body } from '@tauri-apps/api/http'; import type { AIClient, AnthropicModelName, Prompt } from '$lib/ai/types'; -type AnthropicAPIResponse = { content: { text: string }[] }; +type AnthropicAPIResponse = { + content: { text: string }[]; + error: { type: string; message: string }; +}; export class AnthropicAIClient implements AIClient { defaultCommitTemplate = SHORT_DEFAULT_COMMIT_TEMPLATE; @@ -13,7 +17,7 @@ export class AnthropicAIClient implements AIClient { private modelName: AnthropicModelName ) {} - async evaluate(prompt: Prompt) { + async evaluate(prompt: Prompt): Promise> { const body = Body.json({ messages: prompt, max_tokens: 1024, @@ -30,6 +34,12 @@ export class AnthropicAIClient implements AIClient { body }); - return response.data.content[0].text; + if (response.ok && response.data?.content?.[0]?.text) { + return ok(response.data.content[0].text); + } else { + return err( + `Anthropic returned error code ${response.status} ${response.data?.error?.message}` + ); + } } } diff --git a/app/src/lib/ai/butlerClient.ts b/app/src/lib/ai/butlerClient.ts index 56454ef247..b917fb41cf 100644 --- a/app/src/lib/ai/butlerClient.ts +++ b/app/src/lib/ai/butlerClient.ts @@ -1,4 +1,5 @@ import { SHORT_DEFAULT_BRANCH_TEMPLATE, SHORT_DEFAULT_COMMIT_TEMPLATE } from '$lib/ai/prompts'; +import { err, ok, type Result } from '$lib/result'; import type { AIClient, ModelKind, Prompt } from '$lib/ai/types'; import type { HttpClient } from '$lib/backend/httpClient'; @@ -12,16 +13,24 @@ export class ButlerAIClient implements AIClient { private modelKind: ModelKind ) {} - async evaluate(prompt: Prompt) { - const response = await this.cloud.post<{ message: string }>('evaluate_prompt/predict.json', { - body: { - messages: prompt, - max_tokens: 400, - model_kind: this.modelKind - }, - token: this.userToken - }); + async evaluate(prompt: Prompt): Promise> { + try { + const response = await this.cloud.post<{ message: string }>('evaluate_prompt/predict.json', { + body: { + messages: prompt, + max_tokens: 400, + model_kind: this.modelKind + }, + token: this.userToken + }); - return response.message; + return ok(response.message); + } catch (e) { + if (e instanceof Error) { + return err(e.message); + } else { + return err('Failed to contant GitButler API'); + } + } } } diff --git a/app/src/lib/ai/ollamaClient.ts b/app/src/lib/ai/ollamaClient.ts index a9653b92fa..4950d60d20 100644 --- a/app/src/lib/ai/ollamaClient.ts +++ b/app/src/lib/ai/ollamaClient.ts @@ -1,5 +1,6 @@ import { LONG_DEFAULT_BRANCH_TEMPLATE, LONG_DEFAULT_COMMIT_TEMPLATE } from '$lib/ai/prompts'; import { MessageRole, type PromptMessage, type AIClient, type Prompt } from '$lib/ai/types'; +import { err, isError, ok, type Result } from '$lib/result'; import { isNonEmptyObject } from '$lib/utils/typeguards'; import { fetch, Body, Response } from '@tauri-apps/api/http'; @@ -81,15 +82,19 @@ export class OllamaClient implements AIClient { private modelName: string ) {} - async evaluate(prompt: Prompt) { + async evaluate(prompt: Prompt): Promise> { const messages = this.formatPrompt(prompt); - const response = await this.chat(messages); + + const responseResult = await this.chat(messages); + if (isError(responseResult)) return responseResult; + const response = responseResult.value; + const rawResponse = JSON.parse(response.message.content); if (!isOllamaChatMessageFormat(rawResponse)) { - throw new Error('Invalid response: ' + response.message.content); + err('Invalid response: ' + response.message.content); } - return rawResponse.result; + return ok(rawResponse.result); } /** @@ -142,13 +147,12 @@ ${JSON.stringify(OLLAMA_CHAT_MESSAGE_FORMAT_SCHEMA, null, 2)}` * * @param messages - An array of LLMChatMessage objects representing the chat messages. * @param options - Optional LLMRequestOptions object for specifying additional options. - * @throws Error if the response is invalid. * @returns A Promise that resolves to an LLMResponse object representing the response from the LLM model. */ private async chat( messages: Prompt, options?: OllamaRequestOptions - ): Promise { + ): Promise> { const result = await this.fetchChat({ model: this.modelName, stream: false, @@ -158,9 +162,9 @@ ${JSON.stringify(OLLAMA_CHAT_MESSAGE_FORMAT_SCHEMA, null, 2)}` }); if (!isOllamaChatResponse(result.data)) { - throw new Error('Invalid response\n' + JSON.stringify(result.data)); + return err('Invalid response\n' + JSON.stringify(result.data)); } - return result.data; + return ok(result.data); } } diff --git a/app/src/lib/ai/openAIClient.ts b/app/src/lib/ai/openAIClient.ts index 09ea39d88d..bcaa9e23a2 100644 --- a/app/src/lib/ai/openAIClient.ts +++ b/app/src/lib/ai/openAIClient.ts @@ -1,4 +1,5 @@ import { SHORT_DEFAULT_BRANCH_TEMPLATE, SHORT_DEFAULT_COMMIT_TEMPLATE } from '$lib/ai/prompts'; +import { err, ok, type Result } from '$lib/result'; import type { OpenAIModelName, Prompt, AIClient } from '$lib/ai/types'; import type OpenAI from 'openai'; @@ -11,13 +12,25 @@ export class OpenAIClient implements AIClient { private openAI: OpenAI ) {} - async evaluate(prompt: Prompt) { - const response = await this.openAI.chat.completions.create({ - messages: prompt, - model: this.modelName, - max_tokens: 400 - }); + async evaluate(prompt: Prompt): Promise> { + try { + const response = await this.openAI.chat.completions.create({ + messages: prompt, + model: this.modelName, + max_tokens: 400 + }); - return response.choices[0].message.content || ''; + if (response.choices[0]?.message.content) { + return ok(response.choices[0]?.message.content); + } else { + return err('Open AI generated an empty message'); + } + } catch (e) { + if (e instanceof Error) { + return err(e.message); + } else { + return err('Failed to contact Open AI'); + } + } } } diff --git a/app/src/lib/ai/service.test.ts b/app/src/lib/ai/service.test.ts index f25eea65dc..72335ead2e 100644 --- a/app/src/lib/ai/service.test.ts +++ b/app/src/lib/ai/service.test.ts @@ -11,7 +11,7 @@ import { type Prompt } from '$lib/ai/types'; import { HttpClient } from '$lib/backend/httpClient'; -import * as toasts from '$lib/utils/toasts'; +import { err, ok, unwrap, type Result } from '$lib/result'; import { Hunk } from '$lib/vbranches/types'; import { plainToInstance } from 'class-transformer'; import { expect, test, describe, vi } from 'vitest'; @@ -56,8 +56,8 @@ class DummyAIClient implements AIClient { defaultBranchTemplate = SHORT_DEFAULT_BRANCH_TEMPLATE; constructor(private response = 'lorem ipsum') {} - async evaluate(_prompt: Prompt) { - return this.response; + async evaluate(_prompt: Prompt): Promise> { + return ok(this.response); } } @@ -116,16 +116,14 @@ describe.concurrent('AIService', () => { test('With default configuration, When a user token is provided. It returns ButlerAIClient', async () => { const aiService = buildDefaultAIService(); - expect(await aiService.buildClient('token')).toBeInstanceOf(ButlerAIClient); + expect(unwrap(await aiService.buildClient('token'))).toBeInstanceOf(ButlerAIClient); }); test('With default configuration, When a user is undefined. It returns undefined', async () => { - const toastErrorSpy = vi.spyOn(toasts, 'error'); const aiService = buildDefaultAIService(); - expect(await aiService.buildClient()).toBe(undefined); - expect(toastErrorSpy).toHaveBeenLastCalledWith( - "When using GitButler's API to summarize code, you must be logged in" + expect(await aiService.buildClient()).toStrictEqual( + err("When using GitButler's API to summarize code, you must be logged in") ); }); @@ -137,11 +135,10 @@ describe.concurrent('AIService', () => { }); const aiService = new AIService(gitConfig, cloud); - expect(await aiService.buildClient()).toBeInstanceOf(OpenAIClient); + expect(unwrap(await aiService.buildClient())).toBeInstanceOf(OpenAIClient); }); test('When token is bring your own, When a openAI token is blank. It returns undefined', async () => { - const toastErrorSpy = vi.spyOn(toasts, 'error'); const gitConfig = new DummyGitConfigService({ ...defaultGitConfig, [GitAIConfigKey.OpenAIKeyOption]: KeyOption.BringYourOwn, @@ -149,9 +146,10 @@ describe.concurrent('AIService', () => { }); const aiService = new AIService(gitConfig, cloud); - expect(await aiService.buildClient()).toBe(undefined); - expect(toastErrorSpy).toHaveBeenLastCalledWith( - 'When using OpenAI in a bring your own key configuration, you must provide a valid token' + expect(await aiService.buildClient()).toStrictEqual( + err( + 'When using OpenAI in a bring your own key configuration, you must provide a valid token' + ) ); }); @@ -164,11 +162,10 @@ describe.concurrent('AIService', () => { }); const aiService = new AIService(gitConfig, cloud); - expect(await aiService.buildClient()).toBeInstanceOf(AnthropicAIClient); + expect(unwrap(await aiService.buildClient())).toBeInstanceOf(AnthropicAIClient); }); test('When ai provider is Anthropic, When token is bring your own, When an anthropic token is blank. It returns undefined', async () => { - const toastErrorSpy = vi.spyOn(toasts, 'error'); const gitConfig = new DummyGitConfigService({ ...defaultGitConfig, [GitAIConfigKey.ModelProvider]: ModelKind.Anthropic, @@ -177,9 +174,10 @@ describe.concurrent('AIService', () => { }); const aiService = new AIService(gitConfig, cloud); - expect(await aiService.buildClient()).toBe(undefined); - expect(toastErrorSpy).toHaveBeenLastCalledWith( - 'When using Anthropic in a bring your own key configuration, you must provide a valid token' + expect(await aiService.buildClient()).toStrictEqual( + err( + 'When using Anthropic in a bring your own key configuration, you must provide a valid token' + ) ); }); }); @@ -188,9 +186,13 @@ describe.concurrent('AIService', () => { test('When buildModel returns undefined, it returns undefined', async () => { const aiService = buildDefaultAIService(); - vi.spyOn(aiService, 'buildClient').mockReturnValue((async () => undefined)()); + vi.spyOn(aiService, 'buildClient').mockReturnValue( + (async () => err('Failed to build'))() + ); - expect(await aiService.summarizeCommit({ hunks: exampleHunks })).toBe(undefined); + expect(await aiService.summarizeCommit({ hunks: exampleHunks })).toStrictEqual( + err('Failed to build') + ); }); test('When the AI returns a single line commit message, it returns it unchanged', async () => { @@ -199,10 +201,12 @@ describe.concurrent('AIService', () => { const clientResponse = 'single line commit'; vi.spyOn(aiService, 'buildClient').mockReturnValue( - (async () => new DummyAIClient(clientResponse))() + (async () => ok(new DummyAIClient(clientResponse)))() ); - expect(await aiService.summarizeCommit({ hunks: exampleHunks })).toBe('single line commit'); + expect(await aiService.summarizeCommit({ hunks: exampleHunks })).toStrictEqual( + ok('single line commit') + ); }); test('When the AI returns a title and body that is split by a single new line, it replaces it with two', async () => { @@ -211,10 +215,12 @@ describe.concurrent('AIService', () => { const clientResponse = 'one\nnew line'; vi.spyOn(aiService, 'buildClient').mockReturnValue( - (async () => new DummyAIClient(clientResponse))() + (async () => ok(new DummyAIClient(clientResponse)))() ); - expect(await aiService.summarizeCommit({ hunks: exampleHunks })).toBe('one\n\nnew line'); + expect(await aiService.summarizeCommit({ hunks: exampleHunks })).toStrictEqual( + ok('one\n\nnew line') + ); }); test('When the commit is in brief mode, When the AI returns a title and body, it takes just the title', async () => { @@ -223,12 +229,12 @@ describe.concurrent('AIService', () => { const clientResponse = 'one\nnew line'; vi.spyOn(aiService, 'buildClient').mockReturnValue( - (async () => new DummyAIClient(clientResponse))() + (async () => ok(new DummyAIClient(clientResponse)))() ); - expect(await aiService.summarizeCommit({ hunks: exampleHunks, useBriefStyle: true })).toBe( - 'one' - ); + expect( + await aiService.summarizeCommit({ hunks: exampleHunks, useBriefStyle: true }) + ).toStrictEqual(ok('one')); }); }); @@ -236,9 +242,13 @@ describe.concurrent('AIService', () => { test('When buildModel returns undefined, it returns undefined', async () => { const aiService = buildDefaultAIService(); - vi.spyOn(aiService, 'buildClient').mockReturnValue((async () => undefined)()); + vi.spyOn(aiService, 'buildClient').mockReturnValue( + (async () => err('Failed to build client'))() + ); - expect(await aiService.summarizeBranch({ hunks: exampleHunks })).toBe(undefined); + expect(await aiService.summarizeBranch({ hunks: exampleHunks })).toStrictEqual( + err('Failed to build client') + ); }); test('When the AI client returns a string with spaces, it replaces them with hypens', async () => { @@ -247,10 +257,12 @@ describe.concurrent('AIService', () => { const clientResponse = 'with spaces included'; vi.spyOn(aiService, 'buildClient').mockReturnValue( - (async () => new DummyAIClient(clientResponse))() + (async () => ok(new DummyAIClient(clientResponse)))() ); - expect(await aiService.summarizeBranch({ hunks: exampleHunks })).toBe('with-spaces-included'); + expect(await aiService.summarizeBranch({ hunks: exampleHunks })).toStrictEqual( + ok('with-spaces-included') + ); }); test('When the AI client returns multiple lines, it replaces them with hypens', async () => { @@ -259,11 +271,11 @@ describe.concurrent('AIService', () => { const clientResponse = 'with\nnew\nlines\nincluded'; vi.spyOn(aiService, 'buildClient').mockReturnValue( - (async () => new DummyAIClient(clientResponse))() + (async () => ok(new DummyAIClient(clientResponse)))() ); - expect(await aiService.summarizeBranch({ hunks: exampleHunks })).toBe( - 'with-new-lines-included' + expect(await aiService.summarizeBranch({ hunks: exampleHunks })).toStrictEqual( + ok('with-new-lines-included') ); }); @@ -273,11 +285,11 @@ describe.concurrent('AIService', () => { const clientResponse = 'with\nnew lines\nincluded'; vi.spyOn(aiService, 'buildClient').mockReturnValue( - (async () => new DummyAIClient(clientResponse))() + (async () => ok(new DummyAIClient(clientResponse)))() ); - expect(await aiService.summarizeBranch({ hunks: exampleHunks })).toBe( - 'with-new-lines-included' + expect(await aiService.summarizeBranch({ hunks: exampleHunks })).toStrictEqual( + ok('with-new-lines-included') ); }); }); diff --git a/app/src/lib/ai/service.ts b/app/src/lib/ai/service.ts index 3e30bb112e..c8cef2b9ee 100644 --- a/app/src/lib/ai/service.ts +++ b/app/src/lib/ai/service.ts @@ -14,8 +14,8 @@ import { MessageRole, type Prompt } from '$lib/ai/types'; +import { err, isError, ok, type Result } from '$lib/result'; import { splitMessage } from '$lib/utils/commitMessage'; -import * as toasts from '$lib/utils/toasts'; import OpenAI from 'openai'; import type { GitConfigService } from '$lib/backend/gitConfigService'; import type { HttpClient } from '$lib/backend/httpClient'; @@ -189,21 +189,20 @@ export class AIService { // This optionally returns a summarizer. There are a few conditions for how this may occur // Firstly, if the user has opted to use the GB API and isn't logged in, it will return undefined // Secondly, if the user has opted to bring their own key but hasn't provided one, it will return undefined - async buildClient(userToken?: string): Promise { + async buildClient(userToken?: string): Promise> { const modelKind = await this.getModelKind(); if (await this.usingGitButlerAPI()) { if (!userToken) { - toasts.error("When using GitButler's API to summarize code, you must be logged in"); - return; + return err("When using GitButler's API to summarize code, you must be logged in"); } - return new ButlerAIClient(this.cloud, userToken, modelKind); + return ok(new ButlerAIClient(this.cloud, userToken, modelKind)); } if (modelKind === ModelKind.Ollama) { const ollamaEndpoint = await this.getOllamaEndpoint(); const ollamaModelName = await this.getOllamaModelName(); - return new OllamaClient(ollamaEndpoint, ollamaModelName); + return ok(new OllamaClient(ollamaEndpoint, ollamaModelName)); } if (modelKind === ModelKind.OpenAI) { @@ -211,14 +210,13 @@ export class AIService { const openAIKey = await this.getOpenAIKey(); if (!openAIKey) { - toasts.error( + return err( 'When using OpenAI in a bring your own key configuration, you must provide a valid token' ); - return; } const openAI = new OpenAI({ apiKey: openAIKey, dangerouslyAllowBrowser: true }); - return new OpenAIClient(openAIModelName, openAI); + return ok(new OpenAIClient(openAIModelName, openAI)); } if (modelKind === ModelKind.Anthropic) { @@ -226,14 +224,15 @@ export class AIService { const anthropicKey = await this.getAnthropicKey(); if (!anthropicKey) { - toasts.error( + return err( 'When using Anthropic in a bring your own key configuration, you must provide a valid token' ); - return; } - return new AnthropicAIClient(anthropicKey, anthropicModelName); + return ok(new AnthropicAIClient(anthropicKey, anthropicModelName)); } + + return err('Failed to build ai client'); } async summarizeCommit({ @@ -242,9 +241,10 @@ export class AIService { useBriefStyle = false, commitTemplate, userToken - }: SummarizeCommitOpts) { - const aiClient = await this.buildClient(userToken); - if (!aiClient) return; + }: SummarizeCommitOpts): Promise> { + const aiClientResult = await this.buildClient(userToken); + if (isError(aiClientResult)) return aiClientResult; + const aiClient = aiClientResult.value; const diffLengthLimit = await this.getDiffLengthLimitConsideringAPI(); const defaultedCommitTemplate = commitTemplate || aiClient.defaultCommitTemplate; @@ -272,19 +272,26 @@ export class AIService { }; }); - let message = await aiClient.evaluate(prompt); + const messageResult = await aiClient.evaluate(prompt); + if (isError(messageResult)) return messageResult; + let message = messageResult.value; if (useBriefStyle) { message = message.split('\n')[0]; } const { title, description } = splitMessage(message); - return description ? `${title}\n\n${description}` : title; + return ok(description ? `${title}\n\n${description}` : title); } - async summarizeBranch({ hunks, branchTemplate, userToken = undefined }: SummarizeBranchOpts) { - const aiClient = await this.buildClient(userToken); - if (!aiClient) return; + async summarizeBranch({ + hunks, + branchTemplate, + userToken = undefined + }: SummarizeBranchOpts): Promise> { + const aiClientResult = await this.buildClient(userToken); + if (isError(aiClientResult)) return aiClientResult; + const aiClient = aiClientResult.value; const diffLengthLimit = await this.getDiffLengthLimitConsideringAPI(); const defaultedBranchTemplate = branchTemplate || aiClient.defaultBranchTemplate; @@ -299,7 +306,10 @@ export class AIService { }; }); - const message = await aiClient.evaluate(prompt); - return message.replaceAll(' ', '-').replaceAll('\n', '-'); + const messageResult = await aiClient.evaluate(prompt); + if (isError(messageResult)) return messageResult; + const message = messageResult.value; + + return ok(message.replaceAll(' ', '-').replaceAll('\n', '-')); } } diff --git a/app/src/lib/ai/types.ts b/app/src/lib/ai/types.ts index 4ff924a41d..d115baaee0 100644 --- a/app/src/lib/ai/types.ts +++ b/app/src/lib/ai/types.ts @@ -1,4 +1,5 @@ import type { Persisted } from '$lib/persisted/persisted'; +import type { Result } from '$lib/result'; export enum ModelKind { OpenAI = 'openai', @@ -33,7 +34,7 @@ export interface PromptMessage { export type Prompt = PromptMessage[]; export interface AIClient { - evaluate(prompt: Prompt): Promise; + evaluate(prompt: Prompt): Promise>; defaultBranchTemplate: Prompt; defaultCommitTemplate: Prompt; diff --git a/app/src/lib/branch/BranchCard.svelte b/app/src/lib/branch/BranchCard.svelte index 388cba8e54..57fa6c99fe 100644 --- a/app/src/lib/branch/BranchCard.svelte +++ b/app/src/lib/branch/BranchCard.svelte @@ -17,6 +17,7 @@ import BranchFiles from '$lib/file/BranchFiles.svelte'; import { showError } from '$lib/notifications/toasts'; import { persisted } from '$lib/persisted/persisted'; + import { isError } from '$lib/result'; import { SETTINGS, type Settings } from '$lib/settings/userSettings'; import Resizer from '$lib/shared/Resizer.svelte'; import { User } from '$lib/stores/user'; @@ -59,26 +60,32 @@ $commitBoxOpen = false; } - async function generateBranchName() { + async function generateBranchName(shouldThrowErrors: boolean) { if (!aiGenEnabled) return; const hunks = branch.files.flatMap((f) => f.hunks); - try { - const prompt = promptService.selectedBranchPrompt(project.id); - const message = await aiService.summarizeBranch({ - hunks, - userToken: $user?.access_token, - branchTemplate: prompt - }); + const prompt = promptService.selectedBranchPrompt(project.id); + const messageResult = await aiService.summarizeBranch({ + hunks, + userToken: $user?.access_token, + branchTemplate: prompt + }); - if (message && message !== branch.name) { - branch.name = message; - branchController.updateBranchName(branch.id, branch.name); + if (isError(messageResult)) { + if (shouldThrowErrors) { + console.error(messageResult.error); + showError('Failed to generate branch name', messageResult.error); } - } catch (e) { - console.error(e); - showError('Failed to generate branch name', e); + + return; + } + + const message = messageResult.value; + + if (message && message !== branch.name) { + branch.name = message; + branchController.updateBranchName(branch.id, branch.name); } } @@ -95,7 +102,7 @@ bind:isLaneCollapsed on:action={(e) => { if (e.detail === 'generate-branch-name') { - generateBranchName(); + generateBranchName(true); } }} /> @@ -124,7 +131,7 @@ bind:isLaneCollapsed on:action={(e) => { if (e.detail === 'generate-branch-name') { - generateBranchName(); + generateBranchName(true); } if (e.detail === 'collapse') { $isLaneCollapsed = true; @@ -165,7 +172,7 @@ hasSectionsAfter={branch.commits.length > 0} on:action={(e) => { if (e.detail === 'generate-branch-name') { - generateBranchName(); + generateBranchName(true); } }} /> diff --git a/app/src/lib/commit/CommitMessageInput.svelte b/app/src/lib/commit/CommitMessageInput.svelte index b11f8d6c26..9f9e999bde 100644 --- a/app/src/lib/commit/CommitMessageInput.svelte +++ b/app/src/lib/commit/CommitMessageInput.svelte @@ -11,6 +11,7 @@ projectCommitGenerationUseEmojis } from '$lib/config/config'; import { showError } from '$lib/notifications/toasts'; + import { isError } from '$lib/result'; import Checkbox from '$lib/shared/Checkbox.svelte'; import DropDownButton from '$lib/shared/DropDownButton.svelte'; import Icon from '$lib/shared/Icon.svelte'; @@ -75,27 +76,35 @@ } aiLoading = true; - try { - const prompt = promptService.selectedCommitPrompt(project.id); - console.log(prompt); - const generatedMessage = await aiService.summarizeCommit({ - hunks, - useEmojiStyle: $commitGenerationUseEmojis, - useBriefStyle: $commitGenerationExtraConcise, - userToken: $user?.access_token, - commitTemplate: prompt - }); - - if (generatedMessage) { - commitMessage = generatedMessage; - } else { - throw new Error('Prompt generated no response'); - } - } catch (e: any) { - showError('Failed to generate commit message', e); - } finally { + + const prompt = promptService.selectedCommitPrompt(project.id); + + const generatedMessageResult = await aiService.summarizeCommit({ + hunks, + useEmojiStyle: $commitGenerationUseEmojis, + useBriefStyle: $commitGenerationExtraConcise, + userToken: $user?.access_token, + commitTemplate: prompt + }); + + if (isError(generatedMessageResult)) { + showError('Failed to generate commit message', generatedMessageResult.error); + aiLoading = false; + return; + } + + const generatedMessage = generatedMessageResult.value; + + if (generatedMessage) { + commitMessage = generatedMessage; + } else { + const errorMessage = 'Prompt generated no response'; + showError(errorMessage, undefined); aiLoading = false; + return; } + + aiLoading = false; } onMount(async () => {