From 620805da454f15d9560a05a30ee0763fdf0fed54 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:04:53 -0700 Subject: [PATCH] fix(naming): preserve branch prompt overrides --- src/main/persistence.test.ts | 2 +- .../commit-message-text-generation.test.ts | 7 +- src/shared/branch-name-from-work.test.ts | 2 + src/shared/branch-name-from-work.ts | 13 ++-- src/shared/source-control-ai.test.ts | 16 ++++- src/shared/source-control-ai.ts | 67 ++++++++++++++++--- 6 files changed, 89 insertions(+), 18 deletions(-) diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index acde5dd02c3..c6a3528a282 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -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}' }) }) 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 6bf37931a29..3b921daf65b 100644 --- a/src/main/text-generation/commit-message-text-generation.test.ts +++ b/src/main/text-generation/commit-message-text-generation.test.ts @@ -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') }) }) diff --git a/src/shared/branch-name-from-work.test.ts b/src/shared/branch-name-from-work.test.ts index 2c66923dd6c..9b754cd92d1 100644 --- a/src/shared/branch-name-from-work.test.ts +++ b/src/shared/branch-name-from-work.test.ts @@ -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') }) }) diff --git a/src/shared/branch-name-from-work.ts b/src/shared/branch-name-from-work.ts index eef0d338955..68880b38556 100644 --- a/src/shared/branch-name-from-work.ts +++ b/src/shared/branch-name-from-work.ts @@ -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) { diff --git a/src/shared/source-control-ai.test.ts b/src/shared/source-control-ai.test.ts index 2b560889b62..54a35115b87 100644 --- a/src/shared/source-control-ai.test.ts +++ b/src/shared/source-control-ai.test.ts @@ -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: { diff --git a/src/shared/source-control-ai.ts b/src/shared/source-control-ai.ts index 31adf5ac70d..2ca3541916a 100644 --- a/src/shared/source-control-ai.ts +++ b/src/shared/source-control-ai.ts @@ -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'