fix(naming): preserve branch prompt overrides

This commit is contained in:
Brennan Benson
2026-07-16 17:04:53 -07:00
parent 3874e62c34
commit 620805da45
6 changed files with 89 additions and 18 deletions
+1 -1
View File
@@ -2483,7 +2483,7 @@ describe('Store', () => {
})
expect(persisted.settings.sourceControlAi.actions.branchName).toEqual({
agentId: 'claude',
commandInputTemplate: '{basePrompt}\n\nRollback commit prompt'
commandInputTemplate: 'Rollback commit prompt\n\n{basePrompt}'
})
})
@@ -1853,7 +1853,7 @@ describe('generateBranchNameFromContext', () => {
})
})
it('includes the branch-name custom prompt in the generated prompt', async () => {
it('keeps branch-name guidance first without dropping the output contract', async () => {
let prompt = ''
await generateBranchNameFromContext(
{ firstPrompt: 'Fix login flow' },
@@ -1861,7 +1861,8 @@ describe('generateBranchNameFromContext', () => {
agentId: 'custom',
model: '',
customAgentCommand: 'agent',
customPrompt: 'Prefer auth terminology.'
customPrompt: 'Prefer auth terminology.',
commandInputTemplate: 'Prefer auth terminology.\n\n{basePrompt}'
},
{
kind: 'remote',
@@ -1882,6 +1883,8 @@ describe('generateBranchNameFromContext', () => {
expect(prompt.startsWith('Prefer auth terminology.')).toBe(true)
expect(prompt).toContain('Prefer auth terminology.')
expect(prompt).not.toContain('Additional user prompt:')
expect(prompt).toContain('Generate a short git branch name')
expect(prompt).toContain('Output ONLY the branch name on a single line')
})
})
+2
View File
@@ -127,6 +127,8 @@ describe('buildBranchNamePrompt', () => {
expect(prompt.startsWith('Prefer product nouns.')).toBe(true)
expect(prompt).not.toContain('Additional user prompt:')
expect(prompt).not.toContain('Rules:')
expect(prompt).toContain('Generate a git branch name')
expect(prompt).toContain('Output ONLY the branch name on a single line')
expect(prompt).toContain('Add a logout button')
})
})
+7 -6
View File
@@ -113,13 +113,14 @@ export function buildBranchNamePrompt(context: BranchNameWorkContext, customProm
// override owns style rather than sitting under a prescriptive default.
if (prompt) {
sections.push(prompt, '')
} else {
sections.push(
'Generate a short git branch name that summarizes the coding task described below.',
'Output ONLY the branch name on a single line, nothing else.',
''
)
}
sections.push(
prompt
? 'Generate a git branch name that summarizes the coding task described below.'
: 'Generate a short git branch name that summarizes the coding task described below.',
'Output ONLY the branch name on a single line, nothing else.',
''
)
sections.push('User request:', context.firstPrompt.trim())
const assistant = context.assistantMessage?.trim()
if (assistant) {
+15 -1
View File
@@ -313,11 +313,19 @@ describe('source-control AI resolution', () => {
}).params.customPrompt
).toBe('Repo commit style')
expect(resolve('branchName').params.customPrompt).toBe('Global branch style')
expect(resolve('branchName').params.commandInputTemplate).toBe(
'Global branch style\n\n{basePrompt}'
)
expect(
resolve('branchName', {
instructionsByOperation: { branchName: 'Repo branch style' }
}).params.customPrompt
).toBe('Repo branch style')
expect(
resolve('branchName', {
instructionsByOperation: { branchName: 'Repo branch style' }
}).params.commandInputTemplate
).toBe('Repo branch style\n\n{basePrompt}')
})
it('does not treat null repo instructions as configured overrides', () => {
@@ -368,6 +376,12 @@ describe('source-control AI resolution', () => {
expect(migrated.instructionsByOperation.commitMessage).toBe('Legacy commit prompt')
expect(migrated.instructionsByOperation.pullRequest).toBe('')
expect(migrated.instructionsByOperation.branchName).toBe('Legacy commit prompt')
expect(migrated.actions?.commitMessage?.commandInputTemplate).toBe(
'{basePrompt}\n\nLegacy commit prompt'
)
expect(migrated.actions?.branchName?.commandInputTemplate).toBe(
'Legacy commit prompt\n\n{basePrompt}'
)
})
it('merges legacy commit-message updates without wiping PR-only settings', () => {
@@ -702,7 +716,7 @@ describe('source-control AI resolution', () => {
commandInputTemplate: '{basePrompt}'
},
branchName: {
commandInputTemplate: '{basePrompt}\n\nbranch style'
commandInputTemplate: 'branch style\n\n{basePrompt}'
}
},
prCreationDefaults: {
+59 -8
View File
@@ -296,13 +296,15 @@ export function normalizeRepoSourceControlAiOverrides(
const migratedActionOverrides = { ...actionOverrides }
for (const operation of SOURCE_CONTROL_TEXT_ACTION_IDS) {
const instruction = instructionsByOperation?.[operation]
const existingTemplate = migratedActionOverrides[operation]?.commandInputTemplate
if (
typeof instruction === 'string' &&
migratedActionOverrides[operation]?.commandInputTemplate === undefined
(existingTemplate === undefined ||
isLegacyBranchInstructionTemplate(operation, instruction, existingTemplate))
) {
migratedActionOverrides[operation] = {
...migratedActionOverrides[operation],
commandInputTemplate: commandTemplateFromInstruction(instruction)
commandInputTemplate: commandTemplateFromOperationInstruction(operation, instruction)
}
}
}
@@ -324,6 +326,35 @@ function commandTemplateFromInstruction(instruction: string | null | undefined):
return ['{basePrompt}', '', trimmed].join('\n')
}
function commandTemplateFromOperationInstruction(
operation: SourceControlAiOperation,
instruction: string | null | undefined
): string {
const trimmed = instruction?.trim()
if (!trimmed) {
return '{basePrompt}'
}
// Why: branch naming instructions define naming style, so they must precede
// the general built-in prompt. Other operations retain their released order.
return operation === 'branchName'
? [trimmed, '', '{basePrompt}'].join('\n')
: commandTemplateFromInstruction(trimmed)
}
function isLegacyBranchInstructionTemplate(
operation: SourceControlAiOperation,
instruction: string | null | undefined,
template: string | null | undefined
): boolean {
// Why: reorder only the exact template older settings derived automatically;
// a user-authored command template remains authoritative.
return (
operation === 'branchName' &&
Boolean(instruction?.trim()) &&
template === commandTemplateFromInstruction(instruction)
)
}
function actionRecipeFromLegacyCommitMessageAi(legacy: CommitMessageAiSettings): {
agentId?: TuiAgent | CustomAgentId | null
commandInputTemplate: string
@@ -400,7 +431,10 @@ function shouldImportLegacyBranchPrompt(
projectedLegacy: CommitMessageAiSettings
): boolean {
const branchRecipe = readSourceControlActionDefault(base.actions, 'branchName')
const projectedTemplate = commandTemplateFromInstruction(projectedLegacy.customPrompt)
const projectedTemplate = commandTemplateFromOperationInstruction(
'branchName',
projectedLegacy.customPrompt
)
return (
branchRecipe.commandInputTemplate === undefined ||
branchRecipe.commandInputTemplate ===
@@ -473,7 +507,13 @@ export function sourceControlAiSettingsFromLegacy(
actions: {
...defaults.actions,
commitMessage: legacyActionRecipe,
branchName: legacyActionRecipe
branchName: {
...legacyActionRecipe,
commandInputTemplate: commandTemplateFromOperationInstruction(
'branchName',
legacy.customPrompt
)
}
}
}
}
@@ -640,7 +680,12 @@ export function mergeLegacyCommitMessageAiIntoSourceControlAi(
? applyLegacyAgentToActionRecipe(base.actions?.branchName, legacy.agentId)
: base.actions?.branchName),
...(shouldMergeBranchPrompt
? { commandInputTemplate: legacyActionRecipe.commandInputTemplate }
? {
commandInputTemplate: commandTemplateFromOperationInstruction(
'branchName',
legacy.customPrompt
)
}
: {})
}
}
@@ -693,15 +738,21 @@ export function normalizeSourceControlAiSettings(
const existing = readSourceControlActionDefault(normalizedActions, actionId)
const instruction = base.instructionsByOperation?.[actionId]
const legacyInstruction = actionId === 'commitMessage' ? legacy?.customPrompt : undefined
const resolvedInstruction = instruction ?? legacyInstruction
const instructionTemplate =
instruction || legacyInstruction
? commandTemplateFromInstruction(instruction ?? legacyInstruction)
? commandTemplateFromOperationInstruction(actionId, resolvedInstruction)
: undefined
const shouldApplyInstructionTemplate =
instructionTemplate !== undefined &&
(existing.commandInputTemplate === undefined ||
existing.commandInputTemplate ===
DEFAULT_SOURCE_CONTROL_ACTION_COMMAND_TEMPLATES[actionId])
DEFAULT_SOURCE_CONTROL_ACTION_COMMAND_TEMPLATES[actionId] ||
isLegacyBranchInstructionTemplate(
actionId,
resolvedInstruction,
existing.commandInputTemplate
))
return [
actionId,
{
@@ -1048,7 +1099,7 @@ function resolveActionRecipeForTextOperation(
)
const fallbackTemplate =
repoInstruction !== undefined
? commandTemplateFromInstruction(repoInstruction)
? commandTemplateFromOperationInstruction(operation, repoInstruction)
: resolveSourceControlActionCommandTemplate(source.actions, operation)
const repoTemplate =
typeof repoRecipe?.commandInputTemplate === 'string'