mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
* feat: rename worktree folder to match branch on first work When the first agent message auto-renames a freshly created creature branch to a short, work-derived name, also align the on-disk worktree folder and the sidebar display name with it. Re-key every worktree-scoped slice of state — renderer store maps plus the persisted main-process state — through the resulting id change so the live worktree survives the rename instead of being treated as a deletion (its tabs, terminals, browser panes, and git status all follow). The rename is best-effort and local-only: a skip or failure (remote runtime, Windows lock, destination taken) leaves the folder as-is and never undoes the branch/display rename that already landed. Squashed from the original PR #4743 commits, rebased onto upstream/main to drop accumulated merge commits and i18n formatting churn so the branch carries only the feature diff: - Rename worktree folder to match branch on first work - migrate renamed worktree session ids - keep the live worktree alive through a folder rename - address PR review feedback * Address PR review feedback (#4743) - orca-runtime: emit in-process worktreesChanged client event on folder rename, mirroring notifyBranchRenamed so onClientEvent listeners aren't left stale - worktrees: re-key rightSidebarExplorerViewByWorktree and activeWorkspaceKey through a worktree-identity rename (both were worktree-scoped but missed by buildWorktreeRenameState) - branch-name-from-work: treat prefix-only model output as an empty slug so the caller skips the rename instead of producing a doubled prefix - worktree-folder-rename-target: document why posix.dirname is safe (Windows filtered out earlier) - tests for each of the above * Extract branch rename test helpers to a separate harness file Move git responders, mock builders, and test event fixtures out of first-work-branch-rename.test.ts into a new test harness file. This reduces file length and removes the max-lines ESLint disable directive, complying with project style guidelines. * Wrap entire OnboardingFlow in TooltipProvider Enable the use of tooltips anywhere within the onboarding flow, rather than restricting them to the step indicators. --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
130 lines
4.6 KiB
TypeScript
130 lines
4.6 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('-')
|
|
}
|
|
|
|
/**
|
|
* Drop a leading prefix segment the generation model prepended despite the
|
|
* "no prefixes" instruction — e.g. with a `tmchow/` branch prefix the model
|
|
* emits `tmchow/worktree-spinner`, which `sanitizeBranchSlug` folds to
|
|
* `tmchow-worktree-spinner`. Left alone, that leaks the prefix into both the
|
|
* branch leaf (yielding a doubled `tmchow/tmchow-...`) and the humanized
|
|
* display name. Strips only when the leading segment matches the *configured*
|
|
* prefix, so a work-derived name that merely starts with a real word survives.
|
|
* Prefix-only output (the model echoed just `tmchow`) yields an empty slug so
|
|
* the caller skips the rename — otherwise it would double-prefix to
|
|
* `tmchow/tmchow`.
|
|
*/
|
|
export function stripConfiguredBranchPrefix(
|
|
slug: string,
|
|
prefix: string | null | undefined
|
|
): string {
|
|
if (!prefix) {
|
|
return slug
|
|
}
|
|
const prefixSlug = prefix
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
if (!prefixSlug) {
|
|
return slug
|
|
}
|
|
if (slug === prefixSlug) {
|
|
return ''
|
|
}
|
|
return slug.startsWith(`${prefixSlug}-`) ? slug.slice(prefixSlug.length + 1) : slug
|
|
}
|
|
|
|
/**
|
|
* 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')
|
|
}
|