Enable AI commit messages by default (#2060)
* test commit * fix: address review findings
This commit is contained in:
parent
6a960a855e
commit
e9bf07643e
|
|
@ -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 <help@stably.ai>',
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<HTMLInputElement>(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.
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onCancelGenerate()}
|
||||
title="Stop generating"
|
||||
aria-label="Stop generating commit message"
|
||||
className="group absolute right-1.5 top-1.5 inline-flex size-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive focus-visible:bg-destructive/10 focus-visible:text-destructive focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-destructive/40"
|
||||
>
|
||||
<RefreshCw className="size-3.5 animate-spin group-hover:hidden group-focus-visible:hidden" />
|
||||
<Square className="hidden size-3.5 fill-current group-hover:block group-focus-visible:block" />
|
||||
</button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onCancelGenerate()}
|
||||
title="Stop generating"
|
||||
aria-label="Stop generating commit message"
|
||||
className="group absolute right-1.5 top-1.5 inline-flex size-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive focus-visible:bg-destructive/10 focus-visible:text-destructive focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-destructive/40"
|
||||
>
|
||||
<RefreshCw className="size-3.5 animate-spin group-hover:hidden group-focus-visible:hidden" />
|
||||
<Square className="hidden size-3.5 fill-current group-hover:block group-focus-visible:block" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" sideOffset={6}>
|
||||
Generating commit message. Click to stop.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -87,6 +87,28 @@ describe('CommitMessageAiPane', () => {
|
|||
expect(markup).toContain('ollama run llama3.1 {prompt}')
|
||||
})
|
||||
|
||||
it('shows an unconfigured state when the default agent is unsupported', () => {
|
||||
const markup = renderPane(
|
||||
buildSettings({
|
||||
defaultTuiAgent: 'gemini',
|
||||
commitMessageAi: {
|
||||
enabled: true,
|
||||
agentId: null,
|
||||
selectedModelByAgent: {},
|
||||
selectedThinkingByModel: {},
|
||||
customPrompt: '',
|
||||
customAgentCommand: ''
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expect(markup).toContain('Not configured')
|
||||
expect(markup).toContain('Your default agent is Gemini')
|
||||
expect(markup).toContain('Choose Claude, Codex, or Custom')
|
||||
expect(markup).not.toContain('Which model the selected agent uses')
|
||||
expect(markup).not.toContain('Thinking effort')
|
||||
})
|
||||
|
||||
it('keeps custom command discoverable in settings search metadata', () => {
|
||||
const customCommandEntry = COMMIT_MESSAGE_AI_PANE_SEARCH_ENTRIES.find(
|
||||
(entry) => entry.title === 'Custom command'
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ import { Terminal } from 'lucide-react'
|
|||
import type { CommitMessageAiSettings, GlobalSettings, TuiAgent } from '../../../../shared/types'
|
||||
import {
|
||||
CUSTOM_AGENT_ID,
|
||||
DEFAULT_COMMIT_MESSAGE_AGENT_ID,
|
||||
getCommitMessageAgentCapability,
|
||||
isCustomAgentId,
|
||||
listCommitMessageAgentCapabilities,
|
||||
type CommitMessageAgentChoice,
|
||||
resolveCommitMessageAgentChoice,
|
||||
type CommitMessageAgentCapability,
|
||||
type CommitMessageModelCapability
|
||||
} from '../../../../shared/commit-message-agent-spec'
|
||||
|
|
@ -40,6 +39,8 @@ const EMPTY_SETTINGS: CommitMessageAiSettings = {
|
|||
customAgentCommand: ''
|
||||
}
|
||||
|
||||
const UNCONFIGURED_AGENT_SELECT_VALUE = ''
|
||||
|
||||
function readSettings(settings: GlobalSettings): CommitMessageAiSettings {
|
||||
return settings.commitMessageAi ?? EMPTY_SETTINGS
|
||||
}
|
||||
|
|
@ -119,9 +120,24 @@ export function CommitMessageAiPane({
|
|||
)
|
||||
|
||||
const agentCapabilities = useMemo(listCommitMessageAgentCapabilities, [])
|
||||
const activeAgentId: CommitMessageAgentChoice = config.agentId ?? DEFAULT_COMMIT_MESSAGE_AGENT_ID
|
||||
const isCustom = isCustomAgentId(activeAgentId)
|
||||
const activeCapability = isCustom ? undefined : getCommitMessageAgentCapability(activeAgentId)
|
||||
const resolvedAgentId = resolveCommitMessageAgentChoice(config.agentId, settings.defaultTuiAgent)
|
||||
const activeAgentSelectValue = resolvedAgentId ?? UNCONFIGURED_AGENT_SELECT_VALUE
|
||||
const unsupportedDefaultAgent =
|
||||
resolvedAgentId === null &&
|
||||
!config.agentId &&
|
||||
settings.defaultTuiAgent &&
|
||||
settings.defaultTuiAgent !== 'blank'
|
||||
? settings.defaultTuiAgent
|
||||
: null
|
||||
const unsupportedDefaultAgentLabel = unsupportedDefaultAgent
|
||||
? (AGENT_CATALOG.find((a) => a.id === unsupportedDefaultAgent)?.label ??
|
||||
unsupportedDefaultAgent)
|
||||
: null
|
||||
const isCustom = isCustomAgentId(resolvedAgentId)
|
||||
const activeCapability =
|
||||
resolvedAgentId && !isCustomAgentId(resolvedAgentId)
|
||||
? getCommitMessageAgentCapability(resolvedAgentId)
|
||||
: undefined
|
||||
const activeModel = activeCapability ? resolveSelectedModel(config, activeCapability) : null
|
||||
const activeThinking = activeModel ? resolveSelectedThinking(config, activeModel) : undefined
|
||||
|
||||
|
|
@ -136,11 +152,15 @@ export function CommitMessageAiPane({
|
|||
return
|
||||
}
|
||||
// Why: when the user enables the feature for the first time, hydrate the
|
||||
// agent / model / thinking choices from provider capabilities so the
|
||||
// Generate button works immediately without forcing them to pick first.
|
||||
// If the user previously persisted 'custom', we keep that and let them
|
||||
// re-edit the command — no implicit reset to a preset.
|
||||
const seedAgentId: TuiAgent | 'custom' = config.agentId ?? DEFAULT_COMMIT_MESSAGE_AGENT_ID
|
||||
// agent / model / thinking choices from their default agent when possible
|
||||
// so Generate works without maintaining a second agent preference. If the
|
||||
// user previously persisted 'custom', keep it and let them re-edit the
|
||||
// command — no implicit reset to a preset.
|
||||
const seedAgentId = resolveCommitMessageAgentChoice(config.agentId, settings.defaultTuiAgent)
|
||||
if (!seedAgentId) {
|
||||
writeConfig({ enabled: true, agentId: null })
|
||||
return
|
||||
}
|
||||
const seedCapability = isCustomAgentId(seedAgentId)
|
||||
? undefined
|
||||
: getCommitMessageAgentCapability(seedAgentId)
|
||||
|
|
@ -164,6 +184,9 @@ export function CommitMessageAiPane({
|
|||
}
|
||||
|
||||
const onAgentChange = (newAgentId: string): void => {
|
||||
if (newAgentId === UNCONFIGURED_AGENT_SELECT_VALUE) {
|
||||
return
|
||||
}
|
||||
if (isCustomAgentId(newAgentId)) {
|
||||
writeConfig({ agentId: CUSTOM_AGENT_ID })
|
||||
return
|
||||
|
|
@ -317,30 +340,38 @@ export function CommitMessageAiPane({
|
|||
worktrees, or the SSH host for remote ones.
|
||||
</p>
|
||||
</div>
|
||||
<Select value={activeAgentId} onValueChange={onAgentChange}>
|
||||
<SelectTrigger size="sm" className="h-8 text-xs w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{agentCapabilities.map((capability) => {
|
||||
const id = capability.id
|
||||
return (
|
||||
<SelectItem key={id} value={id} className="cursor-pointer">
|
||||
<span className="flex items-center gap-2">
|
||||
<AgentIcon agent={id} size={14} />
|
||||
<span>{agentLabel(id, capability)}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
<SelectItem value={CUSTOM_AGENT_ID} className="cursor-pointer">
|
||||
<span className="flex items-center gap-2">
|
||||
<Terminal className="size-3.5" />
|
||||
<span>Custom</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<Select value={activeAgentSelectValue} onValueChange={onAgentChange}>
|
||||
<SelectTrigger size="sm" className="h-8 text-xs w-[180px]">
|
||||
<SelectValue placeholder="Not configured" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{agentCapabilities.map((capability) => {
|
||||
const id = capability.id
|
||||
return (
|
||||
<SelectItem key={id} value={id} className="cursor-pointer">
|
||||
<span className="flex items-center gap-2">
|
||||
<AgentIcon agent={id} size={14} />
|
||||
<span>{agentLabel(id, capability)}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
<SelectItem value={CUSTOM_AGENT_ID} className="cursor-pointer">
|
||||
<span className="flex items-center gap-2">
|
||||
<Terminal className="size-3.5" />
|
||||
<span>Custom</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{unsupportedDefaultAgentLabel ? (
|
||||
<p className="max-w-[260px] text-right text-[11px] text-muted-foreground">
|
||||
Your default agent is {unsupportedDefaultAgentLabel}, which does not support commit
|
||||
message generation yet. Choose Claude, Codex, or Custom.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
)
|
||||
}
|
||||
|
|
@ -413,8 +444,8 @@ export function CommitMessageAiPane({
|
|||
<div className="space-y-0.5">
|
||||
<Label>Model</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Smaller models default to lower latency and cost. Pick a larger one if the diffs you
|
||||
review tend to need more reasoning.
|
||||
Defaults to the strongest available model for the selected agent. Pick a smaller one if
|
||||
you prefer lower latency or cost.
|
||||
</p>
|
||||
</div>
|
||||
<Select value={activeModel.id} onValueChange={onModelChange}>
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ describe('COMMIT_MESSAGE_AGENT_SPECS', () => {
|
|||
expect(ids).toEqual(['claude', 'codex'])
|
||||
})
|
||||
|
||||
it('uses the smallest model as the default for each agent', () => {
|
||||
expect(COMMIT_MESSAGE_AGENT_SPECS.claude?.defaultModelId).toBe('claude-haiku-4-5')
|
||||
expect(COMMIT_MESSAGE_AGENT_SPECS.codex?.defaultModelId).toBe('gpt-5.4-mini')
|
||||
it('uses the smartest model as the default for each agent', () => {
|
||||
expect(COMMIT_MESSAGE_AGENT_SPECS.claude?.defaultModelId).toBe('claude-opus-4-7')
|
||||
expect(COMMIT_MESSAGE_AGENT_SPECS.codex?.defaultModelId).toBe('gpt-5.5')
|
||||
})
|
||||
|
||||
it('defaults the agent picker to Claude', () => {
|
||||
|
|
@ -86,7 +86,7 @@ describe('COMMIT_MESSAGE_AGENT_SPECS', () => {
|
|||
expect(codex).toMatchObject({
|
||||
id: 'codex',
|
||||
label: 'Codex',
|
||||
defaultModelId: 'gpt-5.4-mini'
|
||||
defaultModelId: 'gpt-5.5'
|
||||
})
|
||||
expect(codex).not.toHaveProperty('binary')
|
||||
expect(codex).not.toHaveProperty('buildArgs')
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export const COMMIT_MESSAGE_AGENT_SPECS: Partial<Record<TuiAgent, CommitMessageA
|
|||
defaultThinkingLevel: 'low'
|
||||
}
|
||||
],
|
||||
defaultModelId: 'claude-haiku-4-5'
|
||||
defaultModelId: 'claude-opus-4-7'
|
||||
},
|
||||
codex: {
|
||||
id: 'codex',
|
||||
|
|
@ -118,7 +118,6 @@ export const COMMIT_MESSAGE_AGENT_SPECS: Partial<Record<TuiAgent, CommitMessageA
|
|||
],
|
||||
// Why: ordered to match the official `codex` model picker — descending
|
||||
// by version so the frontier model lands on top and legacy models trail.
|
||||
// Default still resolves by id (`gpt-5.4-mini`), independent of order.
|
||||
models: [
|
||||
{
|
||||
id: 'gpt-5.5',
|
||||
|
|
@ -191,7 +190,7 @@ export const COMMIT_MESSAGE_AGENT_SPECS: Partial<Record<TuiAgent, CommitMessageA
|
|||
defaultThinkingLevel: 'low'
|
||||
}
|
||||
],
|
||||
defaultModelId: 'gpt-5.4-mini'
|
||||
defaultModelId: 'gpt-5.5'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,6 +204,7 @@ export const DEFAULT_COMMIT_MESSAGE_AGENT_ID: TuiAgent = 'claude'
|
|||
export const CUSTOM_AGENT_ID = 'custom' as const
|
||||
export type CustomAgentId = typeof CUSTOM_AGENT_ID
|
||||
export type CommitMessageAgentChoice = TuiAgent | CustomAgentId
|
||||
export type DefaultTuiAgentPreference = TuiAgent | 'blank' | null | undefined
|
||||
|
||||
export function isCustomAgentId(id: string | null | undefined): id is CustomAgentId {
|
||||
return id === CUSTOM_AGENT_ID
|
||||
|
|
@ -214,6 +214,19 @@ export function getCommitMessageAgentSpec(agentId: TuiAgent): CommitMessageAgent
|
|||
return COMMIT_MESSAGE_AGENT_SPECS[agentId]
|
||||
}
|
||||
|
||||
export function resolveCommitMessageAgentChoice(
|
||||
configuredAgentId: CommitMessageAgentChoice | null | undefined,
|
||||
defaultTuiAgent: DefaultTuiAgentPreference
|
||||
): CommitMessageAgentChoice | null {
|
||||
if (configuredAgentId) {
|
||||
return configuredAgentId
|
||||
}
|
||||
if (defaultTuiAgent && defaultTuiAgent !== 'blank') {
|
||||
return getCommitMessageAgentSpec(defaultTuiAgent) ? defaultTuiAgent : null
|
||||
}
|
||||
return DEFAULT_COMMIT_MESSAGE_AGENT_ID
|
||||
}
|
||||
|
||||
export function getCommitMessageModel(
|
||||
agentId: TuiAgent,
|
||||
modelId: string
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export function buildCommitMessagePrompt(
|
|||
'- Optional body: blank line, then short wrapped bullet points or prose explaining WHY.',
|
||||
'- Capture the primary user-visible or developer-visible change.',
|
||||
'- Use only the staged changes below as context.',
|
||||
'- Do not include "Co-authored-by" trailers - Orca appends them after generation when configured.',
|
||||
'- Do not include "Co-authored-by" or other git trailers.',
|
||||
'',
|
||||
`Branch: ${context.branch ?? '(detached)'}`,
|
||||
'',
|
||||
|
|
|
|||
|
|
@ -9,4 +9,12 @@ describe('getDefaultSettings', () => {
|
|||
it('enables separate light terminal theme by default', () => {
|
||||
expect(getDefaultSettings('/tmp').terminalUseSeparateLightTheme).toBe(true)
|
||||
})
|
||||
|
||||
it('enables AI commit messages by default without pinning a separate agent', () => {
|
||||
expect(getDefaultSettings('/tmp').commitMessageAi).toMatchObject({
|
||||
enabled: true,
|
||||
agentId: null,
|
||||
selectedModelByAgent: {}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -274,12 +274,12 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
|
|||
lastViewByProject: {},
|
||||
activeProject: null
|
||||
},
|
||||
// Why: opt-in feature — `enabled: false` keeps the Generate button hidden
|
||||
// for existing users until they discover and turn it on in Settings. The
|
||||
// per-agent / per-model maps stay empty until the user activates the
|
||||
// toggle, at which point the pane fills them with the spec defaults.
|
||||
// Why: default-on uses the user's default agent when it supports
|
||||
// non-interactive commit-message generation. Keep agent/model maps empty
|
||||
// so first use follows the default agent's configured default model instead
|
||||
// of freezing a stale choice into new profiles.
|
||||
commitMessageAi: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
agentId: null,
|
||||
selectedModelByAgent: {},
|
||||
selectedThinkingByModel: {},
|
||||
|
|
|
|||
Loading…
Reference in New Issue