From e140802ff2ecf4b633fdd6ddc8a46e46262c5209 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 24 Feb 2026 15:30:03 +0100 Subject: [PATCH] fix(vscode): sanitize custom worktree names into valid git branch names (#6246) * fix(vscode): sanitize custom worktree names into valid git branch names * chore: revert unintended package.json reordering --- .../src/agent-manager/WorktreeManager.ts | 5 +- .../src/agent-manager/branch-name.ts | 15 ++++-- .../tests/unit/worktree-manager.test.ts | 46 ++++++++++++++++++- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts index 43406f3a54f..9feeaa9cf86 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts @@ -10,7 +10,7 @@ import * as path from "path" import * as fs from "fs" import * as cp from "child_process" import simpleGit, { type SimpleGit } from "simple-git" -import { generateBranchName } from "./branch-name" +import { generateBranchName, sanitizeBranchName } from "./branch-name" import { parsePRUrl, localBranchName, @@ -91,7 +91,8 @@ export class WorktreeManager { } } - let branch = params.existingBranch ?? params.branchName ?? generateBranchName(params.prompt || "agent-task") + const sanitized = params.branchName ? sanitizeBranchName(params.branchName) : undefined + let branch = params.existingBranch ?? (sanitized || undefined) ?? generateBranchName(params.prompt || "agent-task") if (params.existingBranch) { const exists = await this.branchExists(branch) diff --git a/packages/kilo-vscode/src/agent-manager/branch-name.ts b/packages/kilo-vscode/src/agent-manager/branch-name.ts index c0abce3dfc9..0b53b2e0b3c 100644 --- a/packages/kilo-vscode/src/agent-manager/branch-name.ts +++ b/packages/kilo-vscode/src/agent-manager/branch-name.ts @@ -1,14 +1,21 @@ /** - * Generate a valid git branch name from a prompt. + * Sanitize a string into a valid git branch name segment. + * Keeps lowercase alphanumeric chars and hyphens, collapses runs, strips edges. */ -export function generateBranchName(prompt: string): string { - const sanitized = prompt - .slice(0, 50) +export function sanitizeBranchName(name: string, maxLength = 50): string { + return name + .slice(0, maxLength) .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .replace(/-+/g, "-") +} +/** + * Generate a valid git branch name from a prompt. + */ +export function generateBranchName(prompt: string): string { + const sanitized = sanitizeBranchName(prompt) return `${sanitized || "kilo"}-${Date.now()}` } diff --git a/packages/kilo-vscode/tests/unit/worktree-manager.test.ts b/packages/kilo-vscode/tests/unit/worktree-manager.test.ts index a3236dc5fd1..6cb5dea05e9 100644 --- a/packages/kilo-vscode/tests/unit/worktree-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/worktree-manager.test.ts @@ -3,7 +3,7 @@ import os from "node:os" import path from "node:path" import fs from "node:fs/promises" import { WorktreeManager } from "../../src/agent-manager/WorktreeManager" -import { generateBranchName, versionedName } from "../../src/agent-manager/branch-name" +import { generateBranchName, sanitizeBranchName, versionedName } from "../../src/agent-manager/branch-name" import { WorktreeStateManager } from "../../src/agent-manager/WorktreeStateManager" import simpleGit from "simple-git" @@ -90,6 +90,50 @@ describe("generateBranchName", () => { }) }) +// --------------------------------------------------------------------------- +// sanitizeBranchName +// --------------------------------------------------------------------------- + +describe("sanitizeBranchName", () => { + it("replaces spaces with hyphens", () => { + expect(sanitizeBranchName("model comparison")).toBe("model-comparison") + }) + + it("lowercases input", () => { + expect(sanitizeBranchName("My Feature")).toBe("my-feature") + }) + + it("strips special characters", () => { + expect(sanitizeBranchName("fix bug #123 & add feature!")).toBe("fix-bug-123-add-feature") + }) + + it("collapses consecutive hyphens", () => { + expect(sanitizeBranchName("one two three")).toBe("one-two-three") + }) + + it("strips leading and trailing hyphens", () => { + expect(sanitizeBranchName("---hello---")).toBe("hello") + }) + + it("truncates to maxLength", () => { + const result = sanitizeBranchName("a".repeat(100)) + expect(result.length).toBeLessThanOrEqual(50) + }) + + it("returns empty string for whitespace-only input", () => { + expect(sanitizeBranchName(" ")).toBe("") + }) + + it("returns empty string for empty input", () => { + expect(sanitizeBranchName("")).toBe("") + }) + + it("handles custom maxLength", () => { + const result = sanitizeBranchName("abcdefghij", 5) + expect(result).toBe("abcde") + }) +}) + // --------------------------------------------------------------------------- // versionedName // ---------------------------------------------------------------------------