Files
orca/src/shared/source-control-ai.ts
T
OrcaWinandOrcaWin 3b30779a1f fix(i18n): code Source Control AI settings and generation plan failures (#35697)
* fix(i18n): code AI text-generation failures for commit, PR, and Jira title generation

* fix(i18n): merge duplicate type import

* fix(i18n): code Source Control AI settings and generation plan failures

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 22:15:37 -07:00

271 lines
9.6 KiB
TypeScript

import {
CUSTOM_AGENT_ID,
getCommitMessageAgentSpec,
getCommitMessageModel,
isCustomAgentId,
listCommitMessageAgentCapabilities,
resolveCommitMessageAgentChoice
} from './commit-message-agent-spec'
import { LOCAL_COMMIT_MESSAGE_HOST_KEY } from './commit-message-host-key'
import {
textGenerationFailure,
type TextGenerationFailureCode,
type TextGenerationFailureFields
} from './text-generation-failure-copy'
import type { GlobalSettings } from './global-settings-types'
import type { Repo } from './repo-types'
import type { TuiAgent } from './tui-agent'
import { mergeLegacyCommitMessageAiIntoSourceControlAi as mergeLegacySettings } from './source-control-ai-legacy-reconciliation'
import { projectSourceControlAiToLegacyCommitMessageAi as projectLegacySettings } from './source-control-ai-legacy-projection'
import {
clearSourceControlAiModelChoiceForHost as clearModelChoice,
getDiscoveredModels,
readSourceControlAiModelChoiceForHost as readModelChoice,
resolveThinkingLevel,
selectPersistedModelId,
selectSourceControlAiModelChoiceForHost as selectModelChoice
} from './source-control-ai-model-selection'
import {
hasConfiguredSourceControlAiInstructions as hasConfiguredInstructions,
resolveActionRecipeForTextOperation,
resolveInstructionsFromNormalized,
resolvePrCreationDefaults,
resolveSourceControlActionRecipe as resolveActionRecipe,
resolveSourceControlAiEnabled as resolveEnabled,
resolveSourceControlAiInstructions as resolveInstructions,
resolveSourceControlAiPrCreationDefaults as resolvePrDefaults
} from './source-control-ai-operation-policy'
import { normalizeRepoSourceControlAiOverrides as normalizeRepoOverrides } from './source-control-ai-repo-settings'
import {
DEFAULT_SOURCE_CONTROL_AI_PR_CREATION_DEFAULTS as DEFAULT_PR_CREATION_DEFAULTS,
getDefaultSourceControlAiSettings as getDefaultSettings,
normalizeSourceControlAiSettings as normalizeSettings,
sourceControlAiSettingsFromLegacy as settingsFromLegacy
} from './source-control-ai-settings'
import { hasActionAgentRecipe } from './source-control-ai-command-template'
import type { AiTextOperation, SourceControlAiPrCreationDefaults } from './source-control-ai-types'
export const DEFAULT_SOURCE_CONTROL_AI_PR_CREATION_DEFAULTS = DEFAULT_PR_CREATION_DEFAULTS
export type ResolvedSourceControlAiGenerationParams = {
agentId: TuiAgent | 'custom'
model: string
thinkingLevel?: string
customPrompt?: string
commandInputTemplate?: string
agentArgs?: string
customAgentCommand?: string
agentCommandOverride?: string
}
export type ResolvedSourceControlAiOperation = {
enabled: boolean
params: ResolvedSourceControlAiGenerationParams
prCreationDefaults: Required<SourceControlAiPrCreationDefaults>
}
export type ResolveSourceControlAiResult =
| { ok: true; value: ResolvedSourceControlAiOperation }
| ({ ok: false } & TextGenerationFailureFields)
export type SourceControlAiSettingsInput = Partial<
Pick<
GlobalSettings,
| 'defaultTuiAgent'
| 'agentCmdOverrides'
| 'commitMessageAi'
| 'sourceControlAi'
| 'disabledTuiAgents'
>
>
type ResolveSourceControlAiInput = {
settings: SourceControlAiSettingsInput
repo?: Pick<Repo, 'sourceControlAi'> | null
operation: AiTextOperation
discoveryHostKey?: string
prCreationProductDefaults?: SourceControlAiPrCreationDefaults
}
export type ResolveSourceControlAiPrCreationDefaultsInput = {
settings: Pick<GlobalSettings, 'commitMessageAi' | 'sourceControlAi'>
repo?: Pick<Repo, 'sourceControlAi'> | null
prCreationProductDefaults?: SourceControlAiPrCreationDefaults
}
export const normalizeRepoSourceControlAiOverrides = normalizeRepoOverrides
export const getDefaultSourceControlAiSettings = getDefaultSettings
export const sourceControlAiSettingsFromLegacy = settingsFromLegacy
export const mergeLegacyCommitMessageAiIntoSourceControlAi = mergeLegacySettings
export const normalizeSourceControlAiSettings = normalizeSettings
export const readSourceControlAiModelChoiceForHost = readModelChoice
export const selectSourceControlAiModelChoiceForHost = selectModelChoice
export const clearSourceControlAiModelChoiceForHost = clearModelChoice
export const projectSourceControlAiToLegacyCommitMessageAi = projectLegacySettings
export const resolveSourceControlAiInstructions = resolveInstructions
export const hasConfiguredSourceControlAiInstructions = hasConfiguredInstructions
export const resolveSourceControlAiPrCreationDefaults = resolvePrDefaults
export const resolveSourceControlAiEnabled = resolveEnabled
export const resolveSourceControlActionRecipe = resolveActionRecipe
const COMMAND_TEMPLATE_EMPTY = {
commitMessage: 'commandTemplateEmptyCommitMessage',
pullRequest: 'commandTemplateEmptyPullRequest',
branchName: 'commandTemplateEmptyBranchName',
conversationName: 'commandTemplateEmptyChatName'
} as const satisfies Record<AiTextOperation, TextGenerationFailureCode>
const AGENT_CANNOT_GENERATE = {
commitMessage: 'agentCannotGenerateCommitMessage',
pullRequest: 'agentCannotGeneratePullRequest',
branchName: 'agentCannotGenerateBranchName',
conversationName: 'agentCannotGenerateChatName'
} as const satisfies Record<AiTextOperation, TextGenerationFailureCode>
function supportedAgentLabels(): string {
return listCommitMessageAgentCapabilities()
.map((capability) => capability.label)
.join(', ')
}
function resolveFailure(
failure: TextGenerationFailureFields
): { ok: false } & TextGenerationFailureFields {
return { ok: false, ...failure }
}
export function resolveSourceControlAiForOperation(
input: ResolveSourceControlAiInput
): ResolveSourceControlAiResult {
const legacy = input.settings.commitMessageAi
const source = normalizeSettings(input.settings.sourceControlAi, legacy)
const repoOverrides =
input.operation === 'conversationName'
? null
: normalizeRepoOverrides(input.repo?.sourceControlAi)
const forChatNames = input.operation === 'conversationName'
const agentUnsupported = (): { ok: false } & TextGenerationFailureFields =>
resolveFailure(
textGenerationFailure(
forChatNames ? 'chatNameAgentUnsupported' : 'sourceControlAgentUnsupported',
{ agents: supportedAgentLabels() }
)
)
const prCreationDefaults = resolvePrCreationDefaults(
source,
repoOverrides,
input.prCreationProductDefaults
)
const actionRecipe = resolveActionRecipeForTextOperation(source, repoOverrides, input.operation)
if (!actionRecipe.commandInputTemplate.trim()) {
return resolveFailure(textGenerationFailure(COMMAND_TEMPLATE_EMPTY[input.operation]))
}
const preferredAgent = hasActionAgentRecipe(actionRecipe) ? actionRecipe.agentId : source.agentId
const agentChoice = resolveCommitMessageAgentChoice(
preferredAgent,
input.settings.defaultTuiAgent,
input.settings.disabledTuiAgents
)
if (!agentChoice) {
return agentUnsupported()
}
const customAgentCommand =
repoOverrides?.customAgentCommand?.trim() || source.customAgentCommand.trim()
const commonParams = {
customPrompt: resolveInstructionsFromNormalized(
source,
repoOverrides,
input.operation,
legacy?.customPrompt
),
commandInputTemplate: actionRecipe.commandInputTemplate,
...(actionRecipe.agentArgs !== undefined ? { agentArgs: actionRecipe.agentArgs } : {})
}
if (isCustomAgentId(agentChoice)) {
if (!customAgentCommand) {
return resolveFailure(
textGenerationFailure(
forChatNames ? 'chatNameCustomCommandEmpty' : 'sourceControlCustomCommandEmpty'
)
)
}
return {
ok: true,
value: {
enabled: true,
params: {
agentId: CUSTOM_AGENT_ID,
model: '',
...commonParams,
customAgentCommand
},
prCreationDefaults
}
}
}
const actionAgentId = actionRecipe.agentId ?? agentChoice
const resolvedAgent =
actionAgentId === agentChoice
? agentChoice
: resolveCommitMessageAgentChoice(
actionAgentId,
input.settings.defaultTuiAgent,
input.settings.disabledTuiAgents
)
if (!resolvedAgent || isCustomAgentId(resolvedAgent)) {
return agentUnsupported()
}
const spec = getCommitMessageAgentSpec(resolvedAgent)
if (!spec) {
return resolveFailure(
textGenerationFailure(AGENT_CANNOT_GENERATE[input.operation], {
agent: resolvedAgent,
agents: supportedAgentLabels()
})
)
}
const hostKey = input.discoveryHostKey ?? LOCAL_COMMIT_MESSAGE_HOST_KEY
const persistedModelId = selectPersistedModelId({
source,
legacy,
repoOverrides,
operation: input.operation,
hostKey,
agentId: resolvedAgent,
defaultModelId: spec.defaultModelId
})
const discoveredModels = getDiscoveredModels(source, legacy, hostKey, resolvedAgent)
const model =
spec.models.find((candidate) => candidate.id === persistedModelId) ??
discoveredModels.find((candidate) => candidate.id === persistedModelId) ??
getCommitMessageModel(resolvedAgent, spec.defaultModelId)
if (!model) {
return resolveFailure(textGenerationFailure('noModelForAgent', { label: spec.label }))
}
const thinkingLevel = resolveThinkingLevel({
model,
source,
legacy,
repoOverrides,
operation: input.operation
})
const agentCommandOverride = input.settings.agentCmdOverrides?.[resolvedAgent]?.trim()
return {
ok: true,
value: {
enabled: true,
params: {
agentId: resolvedAgent,
model: model.id,
thinkingLevel,
...commonParams,
...(customAgentCommand ? { customAgentCommand } : {}),
...(agentCommandOverride ? { agentCommandOverride } : {})
},
prCreationDefaults
}
}
}