diff --git a/src/main/text-generation/commit-message-text-generation.test.ts b/src/main/text-generation/commit-message-text-generation.test.ts index 77665b899..e9e5b5f42 100644 --- a/src/main/text-generation/commit-message-text-generation.test.ts +++ b/src/main/text-generation/commit-message-text-generation.test.ts @@ -6,9 +6,9 @@ import type * as ChildProcess from 'child_process' import { beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../shared/constants' import { - applyOrcaAttribution, generateCommitMessageFromContext, - resolveCommitMessageSettings + resolveCommitMessageSettings, + trimGeneratedCommitMessage } from './commit-message-text-generation' vi.mock('child_process', async (importOriginal) => { @@ -54,10 +54,25 @@ describe('resolveCommitMessageSettings', () => { ok: true, params: { agentId: 'codex', - model: 'gpt-5.4-mini', + model: 'gpt-5.5', thinkingLevel: 'low', - customPrompt: 'Use Conventional Commits.', - attributionEnabled: true + customPrompt: 'Use Conventional Commits.' + } + }) + }) + + it("uses the user's default agent when the AI setting has no explicit agent", () => { + const settings = getDefaultSettings('/tmp') + settings.defaultTuiAgent = 'codex' + + const result = resolveCommitMessageSettings(settings) + + expect(result).toMatchObject({ + ok: true, + params: { + agentId: 'codex', + model: 'gpt-5.5', + thinkingLevel: 'low' } }) }) @@ -67,8 +82,8 @@ describe('resolveCommitMessageSettings', () => { settings.commitMessageAi = { enabled: true, agentId: 'codex', - selectedModelByAgent: { codex: 'gpt-5.5' }, - selectedThinkingByModel: { 'gpt-5.5': 'turbo' }, + selectedModelByAgent: { codex: 'gpt-5.4-mini' }, + selectedThinkingByModel: { 'gpt-5.4-mini': 'turbo' }, customPrompt: '', customAgentCommand: '' } @@ -79,7 +94,7 @@ describe('resolveCommitMessageSettings', () => { ok: true, params: { agentId: 'codex', - model: 'gpt-5.5', + model: 'gpt-5.4-mini', thinkingLevel: 'low' } }) @@ -239,8 +254,7 @@ describe('generateCommitMessageFromContext', () => { { agentId: 'custom', model: '', - customAgentCommand: 'agent', - attributionEnabled: true + customAgentCommand: 'agent' }, { kind: 'remote', @@ -257,8 +271,7 @@ describe('generateCommitMessageFromContext', () => { expect(result).toEqual({ success: true, - message: - 'Update README\n\n- Explain the generated commit-message flow\n\nCo-authored-by: Orca ', + message: 'Update README\n\n- Explain the generated commit-message flow', agentLabel: 'agent' }) }) @@ -464,10 +477,10 @@ describe('generateCommitMessageFromContext', () => { }) }) -describe('applyOrcaAttribution', () => { - it('does not duplicate the Orca trailer', () => { - const message = applyOrcaAttribution('Update docs', true) +describe('trimGeneratedCommitMessage', () => { + it('removes trailing whitespace from generated messages', () => { + const message = trimGeneratedCommitMessage('Update docs\n\n') - expect(applyOrcaAttribution(message, true)).toBe(message) + expect(message).toBe('Update docs') }) }) diff --git a/src/main/text-generation/commit-message-text-generation.ts b/src/main/text-generation/commit-message-text-generation.ts index e056270f8..cb6aa1c31 100644 --- a/src/main/text-generation/commit-message-text-generation.ts +++ b/src/main/text-generation/commit-message-text-generation.ts @@ -15,16 +15,15 @@ import { } from '../../shared/commit-message-prompt' import { CUSTOM_AGENT_ID, - DEFAULT_COMMIT_MESSAGE_AGENT_ID, getCommitMessageAgentSpec, getCommitMessageModel, - isCustomAgentId + isCustomAgentId, + resolveCommitMessageAgentChoice } from '../../shared/commit-message-agent-spec' import { planCommitMessageGeneration, type CommitMessagePlan } from '../../shared/commit-message-plan' -import { ORCA_GIT_COMMIT_TRAILER } from '../../shared/orca-attribution' import { resolveCliCommand } from '../codex-cli/command' import { getSpawnArgsForWindows, @@ -42,8 +41,6 @@ export type GenerateCommitMessageParams = { customPrompt?: string customAgentCommand?: string agentCommandOverride?: string - /** When true, append `Co-authored-by: Orca ...` after the cleaned message. */ - attributionEnabled?: boolean } export type GenerateCommitMessageResult = @@ -80,33 +77,29 @@ type InternalCommitMessageGenerationResult = | { success: true; commitMessage: GeneratedCommitMessage; agentLabel?: string } | { success: false; error: string; canceled?: boolean } -/** Appends the Orca trailer if the message does not already include it. */ -export function applyOrcaAttribution(message: string, enabled: boolean): string { - if (!enabled) { - // Why: trim trailing whitespace even on the no-attribution path so a - // stray "\n" from the agent's output never reaches the textarea as a - // visible blank line. - return message.replace(/\s+$/, '') - } - const stripped = message.replace(/\s+$/, '') - if (stripped.includes(ORCA_GIT_COMMIT_TRAILER)) { - return stripped - } - // Why: a blank line separates the trailer block from the body so `git - // interpret-trailers` and most parsers treat it as a real trailer instead - // of a paragraph continuation. - return `${stripped}\n\n${ORCA_GIT_COMMIT_TRAILER}` +export function trimGeneratedCommitMessage(message: string): string { + return message.replace(/\s+$/, '') } export function resolveCommitMessageSettings( settings: GlobalSettings ): ResolveCommitMessageSettingsResult { const config = settings.commitMessageAi - if (!config?.enabled || !config.agentId) { - return { ok: false, error: 'Enable AI commit messages and choose an agent in Settings -> Git.' } + if (!config?.enabled) { + return { ok: false, error: 'Enable AI commit messages in Settings -> Git.' } } - if (isCustomAgentId(config.agentId)) { + const agentChoice = resolveCommitMessageAgentChoice(config.agentId, settings.defaultTuiAgent) + if (!agentChoice) { + return { + ok: false, + error: + `Default agent "${settings.defaultTuiAgent}" does not support AI commit messages. ` + + 'Choose Claude or Codex in Settings -> Git -> AI Commit Messages.' + } + } + + if (isCustomAgentId(agentChoice)) { const customAgentCommand = config.customAgentCommand.trim() if (!customAgentCommand) { return { @@ -120,13 +113,12 @@ export function resolveCommitMessageSettings( agentId: CUSTOM_AGENT_ID, model: '', customPrompt: config.customPrompt, - customAgentCommand, - attributionEnabled: settings.enableGitHubAttribution === true + customAgentCommand } } } - const agentId = config.agentId ?? DEFAULT_COMMIT_MESSAGE_AGENT_ID + const agentId = agentChoice const spec = getCommitMessageAgentSpec(agentId) if (!spec) { return { ok: false, error: `Agent "${agentId}" does not support AI commit messages.` } @@ -153,8 +145,7 @@ export function resolveCommitMessageSettings( model: model.id, thinkingLevel, customPrompt: config.customPrompt, - ...(agentCommandOverride ? { agentCommandOverride } : {}), - attributionEnabled: settings.enableGitHubAttribution === true + ...(agentCommandOverride ? { agentCommandOverride } : {}) } } } @@ -418,15 +409,14 @@ async function runRemotePlan( } function formatCommitMessageGenerationResult( - result: InternalCommitMessageGenerationResult, - attributionEnabled: boolean + result: InternalCommitMessageGenerationResult ): GenerateCommitMessageResult { if (!result.success) { return result } return { success: true, - message: applyOrcaAttribution(result.commitMessage.message, attributionEnabled), + message: trimGeneratedCommitMessage(result.commitMessage.message), agentLabel: result.agentLabel } } @@ -446,5 +436,5 @@ export async function generateCommitMessageFromContext( target.kind === 'remote' ? await runRemotePlan(planned.plan, target) : await runLocalPlan(planned.plan, target.cwd, target.env) - return formatCommitMessageGenerationResult(internalResult, params.attributionEnabled === true) + return formatCommitMessageGenerationResult(internalResult) } diff --git a/src/renderer/src/components/right-sidebar/CommitArea.generate.test.tsx b/src/renderer/src/components/right-sidebar/CommitArea.generate.test.tsx index cce1ecae3..83c9c0872 100644 --- a/src/renderer/src/components/right-sidebar/CommitArea.generate.test.tsx +++ b/src/renderer/src/components/right-sidebar/CommitArea.generate.test.tsx @@ -184,6 +184,7 @@ describe('CommitArea AI generation', () => { const button = findNativeButtonByAriaLabel(element, 'Stop generating commit message') expect(button.props.title).toBe('Stop generating') + expect(hasText(element, 'Generating commit message. Click to stop.')).toBe(true) ;(button.props.onClick as () => void)() expect(onCancelGenerate).toHaveBeenCalledTimes(1) }) diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index 35536944b..13b490239 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -132,6 +132,10 @@ import type { HostedReviewInfo } from '../../../../shared/hosted-review' import { STATUS_COLORS, STATUS_LABELS } from './status-display' +import { + isCustomAgentId, + resolveCommitMessageAgentChoice +} from '../../../../shared/commit-message-agent-spec' type SourceControlScope = 'all' | 'uncommitted' type SourceControlViewMode = 'list' | 'tree' @@ -485,6 +489,10 @@ function SourceControlInner(): React.JSX.Element { const [createPrDialogOpen, setCreatePrDialogOpen] = useState(false) const [createPrPushFirst, setCreatePrPushFirst] = useState(false) const commitMessageAi = useAppStore((s) => s.settings?.commitMessageAi) + const effectiveCommitMessageAgentId = useMemo( + () => resolveCommitMessageAgentChoice(commitMessageAi?.agentId, settings?.defaultTuiAgent), + [commitMessageAi?.agentId, settings?.defaultTuiAgent] + ) const filterInputRef = useRef(null) const commitMessage = readCommitDraftForWorktree(commitDrafts, activeWorktreeId) const commitError = commitErrors[activeWorktreeId ?? ''] ?? null @@ -982,11 +990,11 @@ function SourceControlInner(): React.JSX.Element { if (generateInFlightRef.current[activeWorktreeId]) { return } - if (!commitMessageAi?.enabled || !commitMessageAi.agentId) { + if (!commitMessageAi?.enabled || !effectiveCommitMessageAgentId) { return } - if (commitMessageAi.agentId === 'custom') { + if (isCustomAgentId(effectiveCommitMessageAgentId)) { const command = commitMessageAi.customAgentCommand?.trim() ?? '' if (!command) { setGenerateErrors((prev) => ({ @@ -1045,7 +1053,7 @@ function SourceControlInner(): React.JSX.Element { setGenerateInFlightByWorktree((prev) => ({ ...prev, [activeWorktreeId]: false })) generateInFlightRef.current[activeWorktreeId] = false } - }, [activeWorktreeId, commitMessageAi, worktreePath]) + }, [activeWorktreeId, commitMessageAi, effectiveCommitMessageAgentId, worktreePath]) const handleCancelGenerate = useCallback((): void => { if (!activeWorktreeId || !worktreePath) { @@ -2488,11 +2496,11 @@ function SourceControlInner(): React.JSX.Element { aiEnabled={commitMessageAi?.enabled === true} aiAgentConfigured={ commitMessageAi?.enabled === true && - commitMessageAi.agentId !== null && + effectiveCommitMessageAgentId !== null && // Why: 'custom' is configured only once the user types a command. // Without this guard, Generate would spawn an empty command and // fail with a confusing error. - (commitMessageAi.agentId !== 'custom' || + (!isCustomAgentId(effectiveCommitMessageAgentId) || (commitMessageAi.customAgentCommand ?? '').trim().length > 0) } isGenerating={isGenerating} @@ -3048,16 +3056,23 @@ export function CommitArea({ // swap to a Square ("stop") with a destructive tint so the user // sees that clicking will abort the run. Group/group-hover toggles // keep this stateless on the React side. - + + + + + + Generating commit message. Click to stop. + + ) : (