Files
orca/src/shared/branch-name-from-work.ts
2026-05-30 12:44:04 -07:00

97 lines
3.4 KiB
TypeScript

import { MARINE_CREATURES } from './marine-creatures'
// Why: the work-derived branch name stays short on purpose — long, sentence-like
// branches read worse than the creature name they replace. Two-to-four words is
// the sweet spot the feature targets.
export const MAX_BRANCH_NAME_WORDS = 4
const MIN_BRANCH_NAME_WORDS = 2
const CREATURE_NAMES_LOWER = new Set(MARINE_CREATURES.map((name) => name.toLowerCase()))
function normalizeName(name: string): string {
return name.trim().toLowerCase()
}
/**
* True when `branchLeaf` (the segment after any `prefix/`) is an Orca
* auto-generated creature name, e.g. `Nautilus` or `Octopus-2`. Used as the
* eligibility gate so auto-rename only ever overwrites a name Orca itself
* picked — never a branch the user named deliberately.
*/
export function isAutoGeneratedCreatureBranchName(branchLeaf: string): boolean {
const normalized = normalizeName(branchLeaf)
if (!normalized) {
return false
}
// Collision suffixes (`-2`, `-3`, …) are appended at creation; strip one
// before checking membership so numbered creatures still qualify.
const base = normalized.replace(/-\d+$/, '')
return CREATURE_NAMES_LOWER.has(base)
}
/**
* Turn raw model output into a safe, short kebab-case branch leaf. Returns ''
* when nothing usable remains, which callers treat as "skip the rename".
*/
export function sanitizeBranchSlug(raw: string, maxWords = MAX_BRANCH_NAME_WORDS): string {
const words = raw
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.split('-')
.filter(Boolean)
if (words.length === 0) {
return ''
}
return words.slice(0, maxWords).join('-')
}
/**
* Turn a branch slug into a readable workspace display label, e.g.
* `supported-models-list` → `Supported models list`. Used to update the
* sidebar name alongside the branch so the two stay in sync.
*/
export function humanizeBranchSlug(slug: string): string {
const joined = slug.split('-').filter(Boolean).join(' ')
if (!joined) {
return ''
}
return joined.charAt(0).toUpperCase() + joined.slice(1)
}
export type BranchNameWorkContext = {
/** The user's first prompt to the agent in this workspace. */
firstPrompt: string
/** The agent's first response, when it has already arrived. Optional because
* the rename fires as soon as work begins, before a reply may exist. */
assistantMessage?: string
}
/**
* Build the text-generation prompt that asks the configured agent to summarize
* the work into a branch name. Kept in shared so the prompt is identical across
* local and SSH generation targets.
*/
export function buildBranchNamePrompt(context: BranchNameWorkContext, customPrompt = ''): string {
const sections = [
'Generate a git branch name that summarizes the coding task described below.',
'Rules:',
`- Use between ${MIN_BRANCH_NAME_WORDS} and ${MAX_BRANCH_NAME_WORDS} words.`,
'- Lowercase kebab-case only (words joined by single hyphens).',
'- No slashes, no prefixes, no quotes, no trailing punctuation.',
'- Describe the work itself, not the agent or the repository.',
'- Output ONLY the branch name on a single line, nothing else.',
'',
'User request:',
context.firstPrompt.trim()
]
const assistant = context.assistantMessage?.trim()
if (assistant) {
sections.push('', "Agent's initial response:", assistant)
}
const prompt = customPrompt.trim()
if (prompt) {
sections.push('', 'Additional user prompt:', prompt)
}
return sections.join('\n')
}