Files
orca/src/shared/source-control-ai-operation-policy.ts
T
Brennan Benson 6aa12c30ef Name native Claude and Codex chats after their first message (#25724)
* feat: generate chat names through configured text agents

* Project structured chat names across stored tabs and session rows

* Name native chats from their first live message

* Restore the journal test provider handle import

* Fix first-message naming and live Vault title updates

* Preserve Unicode characters in bounded chat naming prompts

* Read chat naming settings only when a turn needs them

* fix(chat): store only generated conversation names

* fix(chat): keep ordinary labels across unnamed chat surfaces

* fix(chat): preserve naming after fast first turns

* Preserve native chat names across command-first sends and Vault lifecycle

* Route native chat SQLite contracts through the existing Node test pool

* feat(settings): add chat naming controls to Chat page

* Preserve chat naming drafts and configure Custom commands through host settings

* Scope synthetic command output to its journal thread in naming test

* Keep naming test fixtures within their typed project boundaries

* Resolve the real Vault hook directly in its integration test

* Publish chat naming save refs after render commits

* fix: keep Japanese chat naming copy stable during repair

* Update test contracts for the naming integration

* Keep journal fixture reads on the host clock

* Give Chat names a separate settings section
2026-10-06 14:42:59 -07:00

221 lines
7.9 KiB
TypeScript

import type { GlobalSettings } from './global-settings-types'
import type { Repo } from './repo-types'
import {
readSourceControlActionDefault,
resolveSourceControlActionCommandTemplate,
type AiActionId,
type SourceControlTextActionId,
type SourceControlActionRecipe
} from './source-control-ai-actions'
import { commandTemplateFromOperationInstruction } from './source-control-ai-command-template'
import { normalizeRepoSourceControlAiOverrides } from './source-control-ai-repo-settings'
import {
DEFAULT_SOURCE_CONTROL_AI_PR_CREATION_DEFAULTS,
normalizeSourceControlAiSettings
} from './source-control-ai-settings'
import type {
RepoSourceControlAiOverrides,
AiTextOperation,
SourceControlAiPrCreationDefaults,
SourceControlAiSettings
} from './source-control-ai-types'
import type { CustomAgentId } from './commit-message-agent-spec'
import type { TuiAgent } from './tui-agent'
function readRepoInstructionOverride(
instructions: RepoSourceControlAiOverrides['instructionsByOperation'],
operation: SourceControlTextActionId
): string | undefined {
if (!Object.hasOwn(instructions ?? {}, operation)) {
return undefined
}
const instruction = instructions?.[operation]
return typeof instruction === 'string' ? instruction : undefined
}
export function resolveInstructionsFromNormalized(
source: SourceControlAiSettings,
repoOverrides: RepoSourceControlAiOverrides | null | undefined,
operation: AiTextOperation,
legacyCustomPrompt: string | undefined
): string {
const repoInstruction =
operation === 'conversationName'
? undefined
: readRepoInstructionOverride(repoOverrides?.instructionsByOperation, operation)
if (repoInstruction !== undefined) {
return repoInstruction.trim()
}
const globalInstruction = source.instructionsByOperation[operation]
if (typeof globalInstruction === 'string') {
return globalInstruction.trim()
}
return operation === 'commitMessage' ? (legacyCustomPrompt ?? '').trim() : ''
}
export function resolveSourceControlAiInstructions(args: {
settings: Pick<GlobalSettings, 'sourceControlAi' | 'commitMessageAi'>
repo?: Pick<Repo, 'sourceControlAi'> | null
operation: AiTextOperation
}): string {
const source = normalizeSourceControlAiSettings(
args.settings.sourceControlAi,
args.settings.commitMessageAi
)
return resolveInstructionsFromNormalized(
source,
normalizeRepoSourceControlAiOverrides(args.repo?.sourceControlAi),
args.operation,
args.settings.commitMessageAi?.customPrompt
)
}
export function hasConfiguredSourceControlAiInstructions(args: {
settings: Pick<GlobalSettings, 'sourceControlAi' | 'commitMessageAi'>
repo?: Pick<Repo, 'sourceControlAi'> | null
operation: AiTextOperation
}): boolean {
const repoInstruction =
args.operation === 'conversationName'
? undefined
: readRepoInstructionOverride(
normalizeRepoSourceControlAiOverrides(args.repo?.sourceControlAi)
?.instructionsByOperation,
args.operation
)
return repoInstruction !== undefined || resolveSourceControlAiInstructions(args).length > 0
}
export function resolvePrCreationDefaults(
source: SourceControlAiSettings,
repoOverrides: RepoSourceControlAiOverrides | null | undefined,
productDefaults: SourceControlAiPrCreationDefaults | undefined
): Required<SourceControlAiPrCreationDefaults> {
const base = {
...DEFAULT_SOURCE_CONTROL_AI_PR_CREATION_DEFAULTS,
...productDefaults,
...source.prCreationDefaults
}
const repoDefaults = repoOverrides?.prCreationDefaults
return repoDefaults
? {
draft: repoDefaults.draft ?? base.draft,
useTemplate: repoDefaults.useTemplate ?? base.useTemplate,
generateDetailsOnOpen: repoDefaults.generateDetailsOnOpen ?? base.generateDetailsOnOpen,
openAfterCreate: repoDefaults.openAfterCreate ?? base.openAfterCreate
}
: base
}
export function resolveActionRecipeForTextOperation(
source: SourceControlAiSettings,
repoOverrides: RepoSourceControlAiOverrides | null | undefined,
operation: AiTextOperation
): { agentId?: TuiAgent | CustomAgentId | null; commandInputTemplate: string; agentArgs?: string } {
const globalRecipe = readSourceControlActionDefault(source.actions, operation)
const repoRecipe =
operation === 'conversationName' ? undefined : repoOverrides?.actionOverrides?.[operation]
const repoInstruction =
operation === 'conversationName'
? undefined
: readRepoInstructionOverride(repoOverrides?.instructionsByOperation, operation)
const fallbackTemplate =
repoInstruction !== undefined
? commandTemplateFromOperationInstruction(operation, repoInstruction)
: resolveSourceControlActionCommandTemplate(source.actions, operation)
const repoTemplate =
typeof repoRecipe?.commandInputTemplate === 'string'
? repoRecipe.commandInputTemplate.trim()
: undefined
const repoAgentArgs =
typeof repoRecipe?.agentArgs === 'string'
? repoRecipe.agentArgs.trim()
: repoRecipe?.agentArgs === null
? ''
: undefined
return {
...(repoRecipe?.agentId !== undefined
? { agentId: repoRecipe.agentId }
: globalRecipe.agentId !== undefined
? { agentId: globalRecipe.agentId }
: {}),
...(repoAgentArgs !== undefined
? { agentArgs: repoAgentArgs }
: globalRecipe.agentArgs !== undefined
? { agentArgs: globalRecipe.agentArgs }
: {}),
commandInputTemplate:
repoTemplate !== undefined
? repoTemplate
: globalRecipe.commandInputTemplate !== undefined
? globalRecipe.commandInputTemplate
: fallbackTemplate
}
}
export function resolveSourceControlAiPrCreationDefaults(input: {
settings: Pick<GlobalSettings, 'commitMessageAi' | 'sourceControlAi'>
repo?: Pick<Repo, 'sourceControlAi'> | null
prCreationProductDefaults?: SourceControlAiPrCreationDefaults
}): Required<SourceControlAiPrCreationDefaults> {
const source = normalizeSourceControlAiSettings(
input.settings.sourceControlAi,
input.settings.commitMessageAi
)
return resolvePrCreationDefaults(
source,
normalizeRepoSourceControlAiOverrides(input.repo?.sourceControlAi),
input.prCreationProductDefaults
)
}
export function resolveSourceControlAiEnabled(input: {
settings: Pick<GlobalSettings, 'sourceControlAi' | 'commitMessageAi'> | null | undefined
repo?: Pick<Repo, 'sourceControlAi'> | null
}): boolean {
const source = normalizeSourceControlAiSettings(
input.settings?.sourceControlAi,
input.settings?.commitMessageAi
)
return (
normalizeRepoSourceControlAiOverrides(input.repo?.sourceControlAi)?.enabled ?? source.enabled
)
}
export function resolveSourceControlActionRecipe(input: {
settings: Pick<GlobalSettings, 'sourceControlAi' | 'commitMessageAi'> | null | undefined
repo?: Pick<Repo, 'sourceControlAi'> | null
actionId: AiActionId
}): SourceControlActionRecipe {
const source = normalizeSourceControlAiSettings(
input.settings?.sourceControlAi,
input.settings?.commitMessageAi
)
const globalRecipe = readSourceControlActionDefault(source.actions, input.actionId)
const repoRecipe =
input.actionId === 'conversationName'
? undefined
: normalizeRepoSourceControlAiOverrides(input.repo?.sourceControlAi)?.actionOverrides?.[
input.actionId
]
const commandInputTemplate = resolveSourceControlActionCommandTemplate(
source.actions,
input.actionId
)
return repoRecipe
? {
...globalRecipe,
commandInputTemplate,
...(repoRecipe.agentId !== undefined ? { agentId: repoRecipe.agentId } : {}),
...(typeof repoRecipe.commandInputTemplate === 'string'
? { commandInputTemplate: repoRecipe.commandInputTemplate.trim() }
: {}),
...(typeof repoRecipe.agentArgs === 'string'
? { agentArgs: repoRecipe.agentArgs.trim() }
: repoRecipe.agentArgs === null
? { agentArgs: '' }
: {})
}
: { ...globalRecipe, commandInputTemplate }
}