Enable AI commit messages by default (#2060)

* test commit

* fix: address review findings
This commit is contained in:
Jinjing 2026-05-15 21:33:59 -07:00 committed by GitHub
parent 6a960a855e
commit e9bf07643e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 206 additions and 113 deletions

View File

@ -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')
})
})

View File

@ -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)
}

View File

@ -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)
})

View File

@ -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"

View File

@ -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'

View File

@ -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}>

View File

@ -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')

View File

@ -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

View File

@ -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)'}`,
'',

View File

@ -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: {}
})
})
})

View File

@ -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: {},