mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(server): suffix-fallback auto-named branches and rotate opencode waterfall
Auto-namer now probes refs/heads/<name> and appends -2, -3, … (up to 50) when the desired slug is taken, instead of failing with "branch already exists" and leaving the worktree on its placeholder name. Structured-generation waterfall replaces opencode/gpt-5-nano with opencode/minimax-m2.5-free and opencode/nemotron-3-super-free so users without nano configured fall through to free models they actually have.
This commit is contained in:
@@ -96,7 +96,8 @@ export interface StructuredAgentGenerationWithFallbackOptions<T> {
|
||||
export const DEFAULT_STRUCTURED_GENERATION_PROVIDERS: readonly StructuredGenerationProvider[] = [
|
||||
{ provider: "claude", model: "haiku" },
|
||||
{ provider: "codex", model: "gpt-5.4-mini", thinkingOptionId: "low" },
|
||||
{ provider: "opencode", model: "opencode/gpt-5-nano" },
|
||||
{ provider: "opencode", model: "opencode/minimax-m2.5-free" },
|
||||
{ provider: "opencode", model: "opencode/nemotron-3-super-free" },
|
||||
] as const;
|
||||
|
||||
interface SchemaValidator<T> {
|
||||
|
||||
@@ -173,6 +173,44 @@ test("renames an eligible unnamed branch-off worktree once on first agent contex
|
||||
expect(branchAfterSecond).toBe("renamed-from-agent-context");
|
||||
});
|
||||
|
||||
test("falls back to a numeric suffix when the desired branch name already exists", async () => {
|
||||
const { repoDir, tempDir } = createGitRepo();
|
||||
cleanupPaths.push(tempDir);
|
||||
|
||||
execFileSync("git", ["branch", "renamed-from-agent-context"], { cwd: repoDir, stdio: "pipe" });
|
||||
execFileSync("git", ["branch", "renamed-from-agent-context-2"], { cwd: repoDir, stdio: "pipe" });
|
||||
|
||||
const created = await createPaseoWorktree(
|
||||
{
|
||||
cwd: repoDir,
|
||||
worktreeSlug: "dazzling-yak",
|
||||
runSetup: false,
|
||||
paseoHome: path.join(tempDir, ".paseo"),
|
||||
},
|
||||
createDeps(),
|
||||
);
|
||||
|
||||
const result = await attemptFirstAgentBranchAutoName({
|
||||
cwd: created.worktree.worktreePath,
|
||||
firstAgentContext: { prompt: "Build the agent context name" },
|
||||
generateBranchNameFromContext: async () => "renamed-from-agent-context",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: true,
|
||||
renamed: true,
|
||||
branchName: "renamed-from-agent-context-3",
|
||||
});
|
||||
expect(
|
||||
execFileSync("git", ["branch", "--show-current"], {
|
||||
cwd: created.worktree.worktreePath,
|
||||
stdio: "pipe",
|
||||
})
|
||||
.toString()
|
||||
.trim(),
|
||||
).toBe("renamed-from-agent-context-3");
|
||||
});
|
||||
|
||||
test("renames the branch even when the app supplies a random placeholder slug", async () => {
|
||||
const { repoDir, tempDir } = createGitRepo();
|
||||
cleanupPaths.push(tempDir);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type CreateWorktreeCoreInput,
|
||||
} from "./worktree-core.js";
|
||||
import { validateBranchSlug, type WorktreeConfig } from "../utils/worktree.js";
|
||||
import { getCurrentBranch, renameCurrentBranch } from "../utils/checkout-git.js";
|
||||
import { getCurrentBranch, localBranchExists, renameCurrentBranch } from "../utils/checkout-git.js";
|
||||
import {
|
||||
markPaseoWorktreeFirstAgentBranchAutoNameAttempted,
|
||||
readPaseoWorktreeMetadata,
|
||||
@@ -85,6 +85,7 @@ export async function attemptFirstAgentBranchAutoName(options: {
|
||||
}) => Promise<string | null>;
|
||||
getCurrentBranch?: typeof getCurrentBranch;
|
||||
renameCurrentBranch?: typeof renameCurrentBranch;
|
||||
localBranchExists?: typeof localBranchExists;
|
||||
}): Promise<AttemptFirstAgentBranchAutoNameResult> {
|
||||
const firstAgentContext = options.firstAgentContext;
|
||||
if (!firstAgentContext || !buildAgentBranchNameSeed(firstAgentContext)) {
|
||||
@@ -129,15 +130,50 @@ export async function attemptFirstAgentBranchAutoName(options: {
|
||||
return { attempted: true, renamed: false, branchName: null };
|
||||
}
|
||||
|
||||
const localBranchExistsImpl = options.localBranchExists ?? localBranchExists;
|
||||
const targetName = await findAvailableBranchName({
|
||||
cwd: options.cwd,
|
||||
desiredName: branchName,
|
||||
placeholderBranchName,
|
||||
localBranchExists: localBranchExistsImpl,
|
||||
});
|
||||
if (!targetName) {
|
||||
return { attempted: true, renamed: false, branchName: null };
|
||||
}
|
||||
|
||||
const renameCurrentBranchImpl = options.renameCurrentBranch ?? renameCurrentBranch;
|
||||
const renamedBranch = await renameCurrentBranchImpl(options.cwd, branchName);
|
||||
const renamedBranch = await renameCurrentBranchImpl(options.cwd, targetName);
|
||||
return {
|
||||
attempted: true,
|
||||
renamed: true,
|
||||
branchName: renamedBranch.currentBranch ?? branchName,
|
||||
branchName: renamedBranch.currentBranch ?? targetName,
|
||||
};
|
||||
}
|
||||
|
||||
const MAX_BRANCH_NAME_SUFFIX_ATTEMPTS = 50;
|
||||
|
||||
async function findAvailableBranchName(options: {
|
||||
cwd: string;
|
||||
desiredName: string;
|
||||
placeholderBranchName: string;
|
||||
localBranchExists: (cwd: string, branchName: string) => Promise<boolean>;
|
||||
}): Promise<string | null> {
|
||||
const { cwd, desiredName, placeholderBranchName } = options;
|
||||
if (!(await options.localBranchExists(cwd, desiredName))) {
|
||||
return desiredName;
|
||||
}
|
||||
for (let suffix = 2; suffix <= MAX_BRANCH_NAME_SUFFIX_ATTEMPTS; suffix++) {
|
||||
const candidate = `${desiredName}-${suffix}`;
|
||||
if (candidate === placeholderBranchName) {
|
||||
continue;
|
||||
}
|
||||
if (!(await options.localBranchExists(cwd, candidate))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function maybeMarkFirstAgentBranchAutoNameEligible(options: {
|
||||
createdWorktree: Awaited<ReturnType<typeof createWorktreeCore>>;
|
||||
}): void {
|
||||
|
||||
@@ -870,6 +870,10 @@ async function getWorktreePathForBranch(cwd: string, branchName: string): Promis
|
||||
}
|
||||
}
|
||||
|
||||
export async function localBranchExists(cwd: string, branchName: string): Promise<boolean> {
|
||||
return doesGitRefExist(cwd, `refs/heads/${branchName}`);
|
||||
}
|
||||
|
||||
export async function renameCurrentBranch(
|
||||
cwd: string,
|
||||
newName: string,
|
||||
|
||||
Reference in New Issue
Block a user