diff --git a/apps/cli/package.json b/apps/cli/package.json index 700ff0c5069..6348bbe020a 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -30,6 +30,8 @@ "@trpc/client": "^11.8.1", "@vscode/ripgrep": "^1.15.9", "commander": "^12.1.0", + "cross-spawn": "^7.0.6", + "execa": "^9.5.2", "fuzzysort": "^3.1.0", "ink": "^6.6.0", "p-wait-for": "^5.0.2", diff --git a/packages/core/package.json b/packages/core/package.json index 95c6d793b35..25e6224e8ca 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,6 +18,7 @@ "@roo-code/types": "workspace:^", "esbuild": "^0.25.0", "execa": "^9.5.2", + "ignore": "^7.0.3", "openai": "^5.12.2", "zod": "^3.25.61" }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 937f71063bf..e5b42a07489 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,4 @@ export * from "./custom-tools/index.js" export * from "./debug-log/index.js" export * from "./message-utils/index.js" +export * from "./worktree/index.js" diff --git a/packages/core/src/worktree/__tests__/worktree-include.spec.ts b/packages/core/src/worktree/__tests__/worktree-include.spec.ts new file mode 100644 index 00000000000..7e9ce6557c3 --- /dev/null +++ b/packages/core/src/worktree/__tests__/worktree-include.spec.ts @@ -0,0 +1,268 @@ +import * as fs from "fs/promises" +import * as path from "path" +import * as os from "os" +import { execFile } from "child_process" +import { promisify } from "util" + +import { WorktreeIncludeService } from "../worktree-include.js" + +const execFileAsync = promisify(execFile) + +async function execGit(cwd: string, args: string[]): Promise { + const { stdout } = await execFileAsync("git", args, { cwd, encoding: "utf8" }) + return stdout +} + +describe("WorktreeIncludeService", () => { + let service: WorktreeIncludeService + let tempDir: string + + beforeEach(async () => { + service = new WorktreeIncludeService() + // Create a temp directory for each test + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "worktree-test-")) + }) + + afterEach(async () => { + // Clean up temp directory + try { + await fs.rm(tempDir, { recursive: true }) + } catch { + // Ignore cleanup errors + } + }) + + describe("hasWorktreeInclude", () => { + it("should return true when .worktreeinclude exists", async () => { + await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "node_modules") + + const result = await service.hasWorktreeInclude(tempDir) + + expect(result).toBe(true) + }) + + it("should return false when .worktreeinclude does not exist", async () => { + const result = await service.hasWorktreeInclude(tempDir) + + expect(result).toBe(false) + }) + + it("should return false for non-existent directory", async () => { + const result = await service.hasWorktreeInclude("/non/existent/path") + + expect(result).toBe(false) + }) + }) + + describe("branchHasWorktreeInclude", () => { + it("should detect .worktreeinclude on the specified branch", async () => { + const repoDir = path.join(tempDir, "repo") + await fs.mkdir(repoDir, { recursive: true }) + + await execGit(repoDir, ["init"]) + await execGit(repoDir, ["config", "user.name", "Test User"]) + await execGit(repoDir, ["config", "user.email", "test@example.com"]) + + await fs.writeFile(path.join(repoDir, "README.md"), "test") + await execGit(repoDir, ["add", "README.md"]) + await execGit(repoDir, ["commit", "-m", "init"]) + + const baseBranch = (await execGit(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"])).trim() + + expect(await service.branchHasWorktreeInclude(repoDir, baseBranch)).toBe(false) + + await execGit(repoDir, ["checkout", "-b", "with-include"]) + await fs.writeFile(path.join(repoDir, ".worktreeinclude"), "node_modules") + await execGit(repoDir, ["add", ".worktreeinclude"]) + await execGit(repoDir, ["commit", "-m", "add include"]) + + expect(await service.branchHasWorktreeInclude(repoDir, "with-include")).toBe(true) + }, 30_000) + }) + + describe("getStatus", () => { + it("should return correct status when both files exist", async () => { + const gitignoreContent = "node_modules\n.env\ndist" + await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "node_modules") + await fs.writeFile(path.join(tempDir, ".gitignore"), gitignoreContent) + + const result = await service.getStatus(tempDir) + + expect(result.exists).toBe(true) + expect(result.hasGitignore).toBe(true) + expect(result.gitignoreContent).toBe(gitignoreContent) + }) + + it("should return correct status when only .gitignore exists", async () => { + const gitignoreContent = "node_modules\n.env" + await fs.writeFile(path.join(tempDir, ".gitignore"), gitignoreContent) + + const result = await service.getStatus(tempDir) + + expect(result.exists).toBe(false) + expect(result.hasGitignore).toBe(true) + expect(result.gitignoreContent).toBe(gitignoreContent) + }) + + it("should return correct status when only .worktreeinclude exists", async () => { + await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "node_modules") + + const result = await service.getStatus(tempDir) + + expect(result.exists).toBe(true) + expect(result.hasGitignore).toBe(false) + expect(result.gitignoreContent).toBeUndefined() + }) + + it("should return correct status when neither file exists", async () => { + const result = await service.getStatus(tempDir) + + expect(result.exists).toBe(false) + expect(result.hasGitignore).toBe(false) + expect(result.gitignoreContent).toBeUndefined() + }) + }) + + describe("createWorktreeInclude", () => { + it("should create .worktreeinclude file with specified content", async () => { + const content = "node_modules\n.env\ndist" + + await service.createWorktreeInclude(tempDir, content) + + const fileContent = await fs.readFile(path.join(tempDir, ".worktreeinclude"), "utf-8") + expect(fileContent).toBe(content) + }) + + it("should overwrite existing .worktreeinclude file", async () => { + await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "old content") + const newContent = "new content" + + await service.createWorktreeInclude(tempDir, newContent) + + const fileContent = await fs.readFile(path.join(tempDir, ".worktreeinclude"), "utf-8") + expect(fileContent).toBe(newContent) + }) + }) + + describe("copyWorktreeIncludeFiles", () => { + let sourceDir: string + let targetDir: string + + beforeEach(async () => { + sourceDir = path.join(tempDir, "source") + targetDir = path.join(tempDir, "target") + await fs.mkdir(sourceDir, { recursive: true }) + await fs.mkdir(targetDir, { recursive: true }) + }) + + it("should return empty array when no .worktreeinclude exists", async () => { + await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules") + + const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir) + + expect(result).toEqual([]) + }) + + it("should return empty array when no .gitignore exists", async () => { + await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules") + + const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir) + + expect(result).toEqual([]) + }) + + it("should return empty array when patterns do not match", async () => { + // .worktreeinclude wants node_modules, .gitignore only ignores .env + await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules") + await fs.writeFile(path.join(sourceDir, ".gitignore"), ".env") + await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true }) + + const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir) + + expect(result).toEqual([]) + }) + + it("should copy files that match both patterns", async () => { + // Both files include node_modules + await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules") + await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules") + // Create a file in node_modules + await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true }) + await fs.writeFile(path.join(sourceDir, "node_modules", "package.json"), '{"name": "test"}') + + const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir) + + expect(result).toContain("node_modules") + // Verify the file was copied + const copiedContent = await fs.readFile(path.join(targetDir, "node_modules", "package.json"), "utf-8") + expect(copiedContent).toBe('{"name": "test"}') + }) + + it("should only copy intersection of patterns", async () => { + // .worktreeinclude: node_modules, dist + // .gitignore: node_modules, .env + // Only node_modules should be copied (intersection) + await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules\ndist") + await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules\n.env") + await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true }) + await fs.mkdir(path.join(sourceDir, "dist"), { recursive: true }) + await fs.writeFile(path.join(sourceDir, ".env"), "SECRET=123") + await fs.writeFile(path.join(sourceDir, "node_modules", "test.txt"), "test") + await fs.writeFile(path.join(sourceDir, "dist", "main.js"), "console.log('dist')") + + const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir) + + // Only node_modules should be in the result (matches both) + expect(result).toContain("node_modules") + expect(result).not.toContain("dist") // only in .worktreeinclude + expect(result).not.toContain(".env") // only in .gitignore + + // Verify node_modules was copied + const nodeModulesExists = await fs + .access(path.join(targetDir, "node_modules")) + .then(() => true) + .catch(() => false) + expect(nodeModulesExists).toBe(true) + + // Verify dist was NOT copied + const distExists = await fs + .access(path.join(targetDir, "dist")) + .then(() => true) + .catch(() => false) + expect(distExists).toBe(false) + }) + + it("should skip .git directory", async () => { + await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), ".git") + await fs.writeFile(path.join(sourceDir, ".gitignore"), ".git") + await fs.mkdir(path.join(sourceDir, ".git"), { recursive: true }) + await fs.writeFile(path.join(sourceDir, ".git", "config"), "[core]") + + const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir) + + expect(result).not.toContain(".git") + }) + + it("should copy single files", async () => { + await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), ".env.local") + await fs.writeFile(path.join(sourceDir, ".gitignore"), ".env.local") + await fs.writeFile(path.join(sourceDir, ".env.local"), "LOCAL_VAR=value") + + const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir) + + expect(result).toContain(".env.local") + const copiedContent = await fs.readFile(path.join(targetDir, ".env.local"), "utf-8") + expect(copiedContent).toBe("LOCAL_VAR=value") + }) + + it("should ignore comment lines in pattern files", async () => { + await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "# comment\nnode_modules\n# another comment") + await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules") + await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true }) + + const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir) + + expect(result).toContain("node_modules") + }) + }) +}) diff --git a/packages/core/src/worktree/__tests__/worktree-service.spec.ts b/packages/core/src/worktree/__tests__/worktree-service.spec.ts new file mode 100644 index 00000000000..5d0fbb848ff --- /dev/null +++ b/packages/core/src/worktree/__tests__/worktree-service.spec.ts @@ -0,0 +1,146 @@ +import * as path from "path" + +import { WorktreeService } from "../worktree-service.js" + +describe("WorktreeService", () => { + describe("normalizePath", () => { + let service: WorktreeService + + beforeEach(() => { + service = new WorktreeService() + }) + + // Access private method for testing + const callNormalizePath = (service: WorktreeService, p: string): string => { + // @ts-expect-error - accessing private method for testing + return service.normalizePath(p) + } + + it("should normalize paths with trailing slashes", () => { + const result = callNormalizePath(service, "/home/user/project/") + expect(result).toBe(path.normalize("/home/user/project")) + }) + + it("should normalize paths with multiple trailing slashes", () => { + const result = callNormalizePath(service, "/home/user/project///") + // path.normalize already handles multiple slashes + expect(result).toBe(path.normalize("/home/user/project")) + }) + + it("should preserve root path /", () => { + // This is a critical test - the old regex would turn "/" into "" + // On Windows, path.normalize("/") returns "\", on Unix it returns "/" + const result = callNormalizePath(service, "/") + expect(result).toBe(path.sep) + }) + + it("should handle paths without trailing slashes", () => { + const result = callNormalizePath(service, "/home/user/project") + expect(result).toBe(path.normalize("/home/user/project")) + }) + + it("should handle relative paths", () => { + const result = callNormalizePath(service, "./some/path/") + expect(result).toBe(path.normalize("./some/path")) + }) + + it("should handle empty string", () => { + const result = callNormalizePath(service, "") + expect(result).toBe(".") + }) + + it("should handle Windows-style paths on non-Windows", () => { + // path.normalize will convert separators appropriately + const result = callNormalizePath(service, "C:\\Users\\test\\project") + // On Unix, this stays as-is; on Windows it would normalize + expect(result).toBeTruthy() + }) + }) + + describe("parseWorktreeOutput", () => { + let service: WorktreeService + + beforeEach(() => { + service = new WorktreeService() + }) + + // Access private method for testing + const callParseWorktreeOutput = ( + service: WorktreeService, + output: string, + currentCwd: string, + ): ReturnType => { + // @ts-expect-error - accessing private method for testing + return service.parseWorktreeOutput(output, currentCwd) + } + + it("should parse porcelain output correctly", () => { + const output = `worktree /home/user/repo +HEAD abc123def456 +branch refs/heads/main + +worktree /home/user/repo-feature +HEAD def456abc123 +branch refs/heads/feature/test +` + const result = callParseWorktreeOutput(service, output, "/home/user/repo") + + expect(result).toHaveLength(2) + expect(result[0]).toMatchObject({ + path: "/home/user/repo", + branch: "main", + commitHash: "abc123def456", + isCurrent: true, + }) + expect(result[1]).toMatchObject({ + path: "/home/user/repo-feature", + branch: "feature/test", + commitHash: "def456abc123", + isCurrent: false, + }) + }) + + it("should handle detached HEAD worktrees", () => { + const output = `worktree /home/user/repo-detached +HEAD abc123def456 +detached +` + const result = callParseWorktreeOutput(service, output, "/home/user/other") + + expect(result).toHaveLength(1) + expect(result[0]).toMatchObject({ + path: "/home/user/repo-detached", + isDetached: true, + branch: "", + }) + }) + + it("should handle locked worktrees", () => { + const output = `worktree /home/user/repo-locked +HEAD abc123def456 +branch refs/heads/locked-branch +locked some reason here +` + const result = callParseWorktreeOutput(service, output, "/home/user/other") + + expect(result).toHaveLength(1) + expect(result[0]).toMatchObject({ + isLocked: true, + lockReason: "some reason here", + }) + }) + + it("should handle bare worktrees", () => { + const output = `worktree /home/user/repo.git +bare +` + const result = callParseWorktreeOutput(service, output, "/home/user/other") + + expect(result).toHaveLength(1) + expect(result[0]).toMatchObject({ + path: "/home/user/repo.git", + isBare: true, + }) + }) + }) +}) diff --git a/packages/core/src/worktree/index.ts b/packages/core/src/worktree/index.ts new file mode 100644 index 00000000000..5daaeebcc44 --- /dev/null +++ b/packages/core/src/worktree/index.ts @@ -0,0 +1,13 @@ +/** + * Worktree Module + * + * Platform-agnostic git worktree management functionality. + * These exports are decoupled from VSCode and can be used by any consumer. + */ + +// Types +export * from "./types.js" + +// Services +export { WorktreeService, worktreeService } from "./worktree-service.js" +export { WorktreeIncludeService, worktreeIncludeService } from "./worktree-include.js" diff --git a/packages/core/src/worktree/types.ts b/packages/core/src/worktree/types.ts new file mode 100644 index 00000000000..2f781705131 --- /dev/null +++ b/packages/core/src/worktree/types.ts @@ -0,0 +1,17 @@ +/** + * Worktree Types + * + * Re-exports platform-agnostic type definitions from @roo-code/types. + */ + +export type { + Worktree, + WorktreeResult, + BranchInfo, + CreateWorktreeOptions, + MergeWorktreeOptions, + MergeWorktreeResult, + WorktreeIncludeStatus, + WorktreeListResponse, + WorktreeDefaultsResponse, +} from "@roo-code/types" diff --git a/packages/core/src/worktree/worktree-include.ts b/packages/core/src/worktree/worktree-include.ts new file mode 100644 index 00000000000..40897468bfa --- /dev/null +++ b/packages/core/src/worktree/worktree-include.ts @@ -0,0 +1,256 @@ +/** + * WorktreeIncludeService + * + * Platform-agnostic service for handling .worktreeinclude files. + * Used to copy untracked files (like node_modules) when creating worktrees. + */ + +import { execFile } from "child_process" +import * as fs from "fs/promises" +import * as path from "path" +import { promisify } from "util" + +import ignore, { type Ignore } from "ignore" + +import type { WorktreeIncludeStatus } from "./types.js" + +const execFileAsync = promisify(execFile) + +/** + * Service for managing .worktreeinclude files and copying files to new worktrees. + * All methods are platform-agnostic and don't depend on VSCode APIs. + */ +export class WorktreeIncludeService { + /** + * Check if .worktreeinclude exists in a directory + */ + async hasWorktreeInclude(dir: string): Promise { + try { + await fs.access(path.join(dir, ".worktreeinclude")) + return true + } catch { + return false + } + } + + /** + * Check if a specific branch has .worktreeinclude file (in git, not local filesystem) + * @param cwd - Current working directory (git repo) + * @param branch - Branch name to check + */ + async branchHasWorktreeInclude(cwd: string, branch: string): Promise { + try { + const ref = `${branch}:.worktreeinclude` + // Use git cat-file -e to check if the file exists on the branch (without printing contents) + await execFileAsync("git", ["cat-file", "-e", "--", ref], { cwd }) + return true + } catch { + // File doesn't exist on this branch + return false + } + } + + /** + * Get the status of .worktreeinclude and .gitignore + */ + async getStatus(dir: string): Promise { + const worktreeIncludePath = path.join(dir, ".worktreeinclude") + const gitignorePath = path.join(dir, ".gitignore") + + let exists = false + let hasGitignore = false + let gitignoreContent: string | undefined + + try { + await fs.access(worktreeIncludePath) + exists = true + } catch { + exists = false + } + + try { + gitignoreContent = await fs.readFile(gitignorePath, "utf-8") + hasGitignore = true + } catch { + hasGitignore = false + } + + return { + exists, + hasGitignore, + gitignoreContent, + } + } + + /** + * Create a .worktreeinclude file with the specified content + */ + async createWorktreeInclude(dir: string, content: string): Promise { + await fs.writeFile(path.join(dir, ".worktreeinclude"), content, "utf-8") + } + + /** + * Copy files matching .worktreeinclude patterns from source to target. + * Only copies files that are ALSO in .gitignore (to avoid copying tracked files). + * + * @returns Array of copied file/directory paths + */ + async copyWorktreeIncludeFiles(sourceDir: string, targetDir: string): Promise { + const worktreeIncludePath = path.join(sourceDir, ".worktreeinclude") + const gitignorePath = path.join(sourceDir, ".gitignore") + + // Check if both files exist + let hasWorktreeInclude = false + let hasGitignore = false + + try { + await fs.access(worktreeIncludePath) + hasWorktreeInclude = true + } catch { + hasWorktreeInclude = false + } + + try { + await fs.access(gitignorePath) + hasGitignore = true + } catch { + hasGitignore = false + } + + if (!hasWorktreeInclude || !hasGitignore) { + return [] + } + + // Parse both files + const worktreeIncludePatterns = await this.parseIgnoreFile(worktreeIncludePath) + const gitignorePatterns = await this.parseIgnoreFile(gitignorePath) + + if (worktreeIncludePatterns.length === 0 || gitignorePatterns.length === 0) { + return [] + } + + // Create ignore matchers + const worktreeIncludeMatcher = ignore().add(worktreeIncludePatterns) + const gitignoreMatcher = ignore().add(gitignorePatterns) + + // Find items that match BOTH patterns (intersection) + const itemsToCopy = await this.findMatchingItems(sourceDir, worktreeIncludeMatcher, gitignoreMatcher) + + // Copy the items + const copiedItems: string[] = [] + for (const item of itemsToCopy) { + const sourcePath = path.join(sourceDir, item) + const targetPath = path.join(targetDir, item) + + try { + const stats = await fs.stat(sourcePath) + + if (stats.isDirectory()) { + // Use native cp for directories (much faster) + await this.copyDirectoryNative(sourcePath, targetPath) + } else { + // Ensure parent directory exists + await fs.mkdir(path.dirname(targetPath), { recursive: true }) + await fs.copyFile(sourcePath, targetPath) + } + copiedItems.push(item) + } catch (error) { + // Log but don't fail on individual copy errors + console.error(`Failed to copy ${item}:`, error) + } + } + + return copiedItems + } + + /** + * Parse a .gitignore-style file and return the patterns + */ + private async parseIgnoreFile(filePath: string): Promise { + try { + const content = await fs.readFile(filePath, "utf-8") + return content + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")) + } catch { + return [] + } + } + + /** + * Find items in sourceDir that match both matchers + */ + private async findMatchingItems( + sourceDir: string, + includeMatcher: Ignore, + gitignoreMatcher: Ignore, + ): Promise { + const matchingItems: string[] = [] + + try { + const entries = await fs.readdir(sourceDir, { withFileTypes: true }) + + for (const entry of entries) { + const relativePath = entry.name + + // Skip .git directory + if (relativePath === ".git") continue + + // Check if this path matches both patterns + // For .worktreeinclude, we want items that are "ignored" (matched) + // For .gitignore, we want items that are "ignored" (matched) + const matchesWorktreeInclude = includeMatcher.ignores(relativePath) + const matchesGitignore = gitignoreMatcher.ignores(relativePath) + + if (matchesWorktreeInclude && matchesGitignore) { + matchingItems.push(relativePath) + } + } + } catch { + return [] + } + + return matchingItems + } + + /** + * Copy directory using native cp command for performance. + * This is 10-20x faster than Node.js fs.cp for large directories like node_modules. + */ + private async copyDirectoryNative(source: string, target: string): Promise { + // Ensure parent directory exists + await fs.mkdir(path.dirname(target), { recursive: true }) + + // Use platform-appropriate copy command + const isWindows = process.platform === "win32" + + if (isWindows) { + // Use robocopy on Windows (more reliable than xcopy) + // robocopy returns non-zero for success, so we check the exit code + try { + await execFileAsync( + "robocopy", + [source, target, "/E", "/NFL", "/NDL", "/NJH", "/NJS", "/nc", "/ns", "/np"], + { windowsHide: true }, + ) + } catch (error) { + // robocopy returns non-zero for success (values < 8) + const exitCode = + typeof (error as { code?: unknown }).code === "number" + ? (error as { code: number }).code + : undefined + if (exitCode !== undefined && exitCode < 8) { + return // Success + } + throw error + } + } else { + // Use cp -r on Unix-like systems + await execFileAsync("cp", ["-r", "--", source, target]) + } + } +} + +// Export singleton instance for convenience +export const worktreeIncludeService = new WorktreeIncludeService() diff --git a/packages/core/src/worktree/worktree-service.ts b/packages/core/src/worktree/worktree-service.ts new file mode 100644 index 00000000000..34b16fed4c3 --- /dev/null +++ b/packages/core/src/worktree/worktree-service.ts @@ -0,0 +1,444 @@ +/** + * WorktreeService + * + * Platform-agnostic service for git worktree operations. + * Uses simple-git and native CLI commands - no VSCode dependencies. + */ + +import { exec, execFile } from "child_process" +import * as path from "path" +import { promisify } from "util" + +import type { + BranchInfo, + CreateWorktreeOptions, + MergeWorktreeOptions, + MergeWorktreeResult, + Worktree, + WorktreeResult, +} from "./types.js" + +const execAsync = promisify(exec) +const execFileAsync = promisify(execFile) + +/** + * Service for managing git worktrees. + * All methods are platform-agnostic and don't depend on VSCode APIs. + */ +export class WorktreeService { + /** + * Check if git is installed on the system + */ + async checkGitInstalled(): Promise { + try { + await execAsync("git --version") + return true + } catch { + return false + } + } + + /** + * Check if a directory is a git repository. + */ + async checkGitRepo(cwd: string): Promise { + try { + await execAsync("git rev-parse --git-dir", { cwd }) + return true + } catch { + return false + } + } + + /** + * Get the git repository root path. + */ + async getGitRootPath(cwd: string): Promise { + try { + const { stdout } = await execAsync("git rev-parse --show-toplevel", { cwd }) + return stdout.trim() + } catch { + return null + } + } + + /** + * Get the current worktree path. + */ + async getCurrentWorktreePath(cwd: string): Promise { + try { + const { stdout } = await execAsync("git rev-parse --show-toplevel", { cwd }) + return stdout.trim() + } catch { + return null + } + } + + /** + * Get the current branch name. + */ + async getCurrentBranch(cwd: string): Promise { + try { + const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd }) + const branch = stdout.trim() + return branch === "HEAD" ? null : branch + } catch { + return null + } + } + + /** + * List all worktrees in the repository + */ + async listWorktrees(cwd: string): Promise { + try { + const { stdout } = await execAsync("git worktree list --porcelain", { cwd }) + return this.parseWorktreeOutput(stdout, cwd) + } catch { + return [] + } + } + + /** + * Create a new worktree + */ + async createWorktree(cwd: string, options: CreateWorktreeOptions): Promise { + try { + const { path: worktreePath, branch, baseBranch, createNewBranch } = options + + // Build the git worktree add command arguments + const args: string[] = ["worktree", "add"] + + if (createNewBranch && branch) { + // Create new branch: git worktree add -b [] + args.push("-b", branch, worktreePath) + if (baseBranch) { + args.push(baseBranch) + } + } else if (branch) { + // Checkout existing branch: git worktree add + args.push(worktreePath, branch) + } else { + // Detached HEAD at current commit + args.push("--detach", worktreePath) + } + + await execFileAsync("git", args, { cwd }) + + // Get the created worktree info + const worktrees = await this.listWorktrees(cwd) + const createdWorktree = worktrees.find( + (wt) => this.normalizePath(wt.path) === this.normalizePath(worktreePath), + ) + + return { + success: true, + message: `Worktree created at ${worktreePath}`, + worktree: createdWorktree, + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + return { + success: false, + message: `Failed to create worktree: ${errorMessage}`, + } + } + } + + /** + * Delete a worktree + */ + async deleteWorktree(cwd: string, worktreePath: string, force = false): Promise { + try { + // Get worktree info BEFORE deletion to capture the branch name + const worktrees = await this.listWorktrees(cwd) + const worktreeToDelete = worktrees.find( + (wt) => this.normalizePath(wt.path) === this.normalizePath(worktreePath), + ) + + const args = ["worktree", "remove"] + if (force) { + args.push("--force") + } + args.push(worktreePath) + await execFileAsync("git", args, { cwd }) + + // Also try to delete the branch if it exists + if (worktreeToDelete?.branch) { + try { + await execFileAsync("git", ["branch", "-d", worktreeToDelete.branch], { cwd }) + } catch { + // Branch deletion is best-effort + } + } + + return { + success: true, + message: `Worktree removed from ${worktreePath}`, + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + return { + success: false, + message: `Failed to delete worktree: ${errorMessage}`, + } + } + } + + /** + * Get available branches + * @param cwd - Current working directory + * @param includeWorktreeBranches - If true, include branches already checked out in worktrees (useful for base branch selection) + */ + async getAvailableBranches(cwd: string, includeWorktreeBranches = false): Promise { + try { + // Run all git commands in parallel for better performance + const [worktrees, localResult, remoteResult, currentBranch] = await Promise.all([ + this.listWorktrees(cwd), + execAsync('git branch --format="%(refname:short)"', { cwd }), + execAsync('git branch -r --format="%(refname:short)"', { cwd }), + this.getCurrentBranch(cwd), + ]) + + const branchesInWorktrees = new Set(worktrees.map((wt) => wt.branch).filter(Boolean)) + + // Filter local branches + const localBranches = localResult.stdout + .trim() + .split("\n") + .filter((b) => b && (includeWorktreeBranches || !branchesInWorktrees.has(b))) + + // Filter remote branches + const remoteBranches = remoteResult.stdout + .trim() + .split("\n") + .filter( + (b) => + b && + !b.includes("HEAD") && + (includeWorktreeBranches || !branchesInWorktrees.has(b.replace(/^origin\//, ""))), + ) + + return { + localBranches, + remoteBranches, + currentBranch: currentBranch || "", + } + } catch { + return { + localBranches: [], + remoteBranches: [], + currentBranch: "", + } + } + } + + /** + * Merge a worktree branch into target branch + */ + async mergeWorktree(cwd: string, options: MergeWorktreeOptions): Promise { + const { worktreePath, targetBranch, deleteAfterMerge } = options + + try { + // Get the worktree info to find its branch + const worktrees = await this.listWorktrees(cwd) + const worktree = worktrees.find((wt) => this.normalizePath(wt.path) === this.normalizePath(worktreePath)) + + if (!worktree) { + return { + success: false, + message: "Worktree not found", + hasConflicts: false, + conflictingFiles: [], + } + } + + const sourceBranch = worktree.branch + if (!sourceBranch) { + return { + success: false, + message: "Worktree has detached HEAD - cannot merge", + hasConflicts: false, + conflictingFiles: [], + } + } + + // Find the worktree that has the target branch checked out + const targetWorktree = worktrees.find((wt) => wt.branch === targetBranch) + const mergeCwd = targetWorktree ? targetWorktree.path : cwd + + // Check for uncommitted changes in source worktree + try { + const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd: worktreePath }) + if (statusOutput.trim()) { + return { + success: false, + message: "Source worktree has uncommitted changes. Please commit or stash them first.", + hasConflicts: false, + conflictingFiles: [], + sourceBranch, + targetBranch, + } + } + } catch { + // Continue if status check fails + } + + // Ensure we're on the target branch + await execFileAsync("git", ["checkout", targetBranch], { cwd: mergeCwd }) + + // Attempt the merge + try { + await execFileAsync("git", ["merge", sourceBranch, "--no-edit"], { cwd: mergeCwd }) + + // Merge succeeded + if (deleteAfterMerge) { + await this.deleteWorktree(cwd, worktreePath, false) + } + + return { + success: true, + message: `Successfully merged ${sourceBranch} into ${targetBranch}`, + hasConflicts: false, + conflictingFiles: [], + sourceBranch, + targetBranch, + } + } catch (mergeError) { + // Check for merge conflicts + try { + const { stdout: conflictOutput } = await execAsync("git diff --name-only --diff-filter=U", { + cwd: mergeCwd, + }) + const conflictingFiles = conflictOutput.trim().split("\n").filter(Boolean) + + // Abort the merge to leave repo in clean state + await execAsync("git merge --abort", { cwd: mergeCwd }) + + return { + success: false, + message: `Merge conflicts detected in ${conflictingFiles.length} file(s)`, + hasConflicts: true, + conflictingFiles, + sourceBranch, + targetBranch, + } + } catch { + // If we can't get conflicts, just report the error + const errorMessage = mergeError instanceof Error ? mergeError.message : String(mergeError) + + // Try to abort any in-progress merge + try { + await execAsync("git merge --abort", { cwd: mergeCwd }) + } catch { + // Ignore abort errors + } + + return { + success: false, + message: `Merge failed: ${errorMessage}`, + hasConflicts: false, + conflictingFiles: [], + sourceBranch, + targetBranch, + } + } + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + return { + success: false, + message: `Merge failed: ${errorMessage}`, + hasConflicts: false, + conflictingFiles: [], + } + } + } + + /** + * Checkout a branch in the current worktree + */ + async checkoutBranch(cwd: string, branch: string): Promise { + try { + await execFileAsync("git", ["checkout", branch], { cwd }) + return { + success: true, + message: `Checked out branch ${branch}`, + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + return { + success: false, + message: `Failed to checkout branch: ${errorMessage}`, + } + } + } + + /** + * Parse git worktree list --porcelain output + */ + private parseWorktreeOutput(output: string, currentCwd: string): Worktree[] { + const worktrees: Worktree[] = [] + const entries = output.trim().split("\n\n") + + for (const entry of entries) { + if (!entry.trim()) continue + + const lines = entry.trim().split("\n") + const worktree: Partial = { + path: "", + branch: "", + commitHash: "", + isCurrent: false, + isBare: false, + isDetached: false, + isLocked: false, + } + + for (const line of lines) { + if (line.startsWith("worktree ")) { + worktree.path = line.substring(9).trim() + } else if (line.startsWith("HEAD ")) { + worktree.commitHash = line.substring(5).trim() + } else if (line.startsWith("branch ")) { + // branch refs/heads/main -> main + const branchRef = line.substring(7).trim() + worktree.branch = branchRef.replace(/^refs\/heads\//, "") + } else if (line === "bare") { + worktree.isBare = true + } else if (line === "detached") { + worktree.isDetached = true + } else if (line === "locked") { + worktree.isLocked = true + } else if (line.startsWith("locked ")) { + worktree.isLocked = true + worktree.lockReason = line.substring(7).trim() + } + } + + if (worktree.path) { + worktree.isCurrent = this.normalizePath(worktree.path) === this.normalizePath(currentCwd) + worktrees.push(worktree as Worktree) + } + } + + return worktrees + } + + /** + * Normalize a path for comparison (handle trailing slashes, etc.) + */ + private normalizePath(p: string): string { + // normalize resolves ./.. segments, removes duplicate slashes, and standardizes path separators + let normalized = path.normalize(p) + // however it doesn't remove trailing slashes + // remove trailing slash, except for root paths (handles both / and \) + if (normalized.length > 1 && (normalized.endsWith("/") || normalized.endsWith("\\"))) { + normalized = normalized.slice(0, -1) + } + return normalized + } +} + +// Export singleton instance for convenience +export const worktreeService = new WorktreeService() diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 9a17834ced7..2eaf5f59816 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -197,6 +197,12 @@ export const globalSettingsSchema = z.object({ hasOpenedModeSelector: z.boolean().optional(), lastModeExportPath: z.string().optional(), lastModeImportPath: z.string().optional(), + + /** + * Path to worktree to auto-open after switching workspaces. + * Used by the worktree feature to open the Roo Code sidebar in a new window. + */ + worktreeAutoOpenPath: z.string().optional(), }) export type GlobalSettings = z.infer diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 2ed3b00ac99..996ee781b28 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -28,5 +28,6 @@ export * from "./tool-params.js" export * from "./type-fu.js" export * from "./vscode-extension-host.js" export * from "./vscode.js" +export * from "./worktree.js" export * from "./providers/index.js" diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 01610ab9b3a..cd36b081576 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -20,6 +20,7 @@ import type { GitCommit } from "./git.js" import type { McpServer } from "./mcp.js" import type { ModelRecord, RouterModels } from "./model.js" import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js" +import type { WorktreeIncludeStatus } from "./worktree.js" /** * ExtensionMessage @@ -99,6 +100,14 @@ export interface ExtensionMessage { | "modes" | "taskWithAggregatedCosts" | "openAiCodexRateLimits" + // Worktree response types + | "worktreeList" + | "worktreeResult" + | "branchList" + | "worktreeDefaults" + | "worktreeIncludeStatus" + | "branchWorktreeIncludeResult" + | "mergeWorktreeResult" text?: string payload?: any // eslint-disable-line @typescript-eslint/no-explicit-any checkpointWarning?: { @@ -111,6 +120,7 @@ export interface ExtensionMessage { | "historyButtonClicked" | "marketplaceButtonClicked" | "cloudButtonClicked" + | "worktreesButtonClicked" | "didBecomeVisible" | "focusInput" | "switchTab" @@ -203,6 +213,51 @@ export interface ExtensionMessage { taskHistory?: HistoryItem[] // For taskHistoryUpdated: full sorted task history /** For taskHistoryItemUpdated: single updated/added history item */ taskHistoryItem?: HistoryItem + // Worktree response properties + worktrees?: Array<{ + path: string + branch: string + commitHash: string + isCurrent: boolean + isBare: boolean + isDetached: boolean + isLocked: boolean + lockReason?: string + }> + isGitRepo?: boolean + isMultiRoot?: boolean + isSubfolder?: boolean + gitRootPath?: string + worktreeResult?: { + success: boolean + message: string + worktree?: { + path: string + branch: string + commitHash: string + isCurrent: boolean + isBare: boolean + isDetached: boolean + isLocked: boolean + lockReason?: string + } + } + localBranches?: string[] + remoteBranches?: string[] + currentBranch?: string + suggestedBranch?: string + suggestedPath?: string + worktreeIncludeExists?: boolean + worktreeIncludeStatus?: WorktreeIncludeStatus + hasGitignore?: boolean + gitignoreContent?: string + hasConflicts?: boolean + conflictingFiles?: string[] + sourceBranch?: string + targetBranch?: string + // branchWorktreeIncludeResult + branch?: string + hasWorktreeInclude?: boolean } export interface OpenAiCodexRateLimitsMessage { @@ -542,6 +597,18 @@ export interface WebviewMessage { | "requestModes" | "switchMode" | "debugSetting" + // Worktree messages + | "listWorktrees" + | "createWorktree" + | "deleteWorktree" + | "switchWorktree" + | "getAvailableBranches" + | "getWorktreeDefaults" + | "getWorktreeIncludeStatus" + | "checkBranchWorktreeInclude" + | "createWorktreeInclude" + | "checkoutBranch" + | "mergeWorktree" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" @@ -631,6 +698,16 @@ export interface WebviewMessage { codebaseIndexOpenRouterApiKey?: string } updatedSettings?: RooCodeSettings + // Worktree properties + worktreePath?: string + worktreeBranch?: string + worktreeBaseBranch?: string + worktreeCreateNewBranch?: boolean + worktreeForce?: boolean + worktreeNewWindow?: boolean + worktreeTargetBranch?: string + worktreeDeleteAfterMerge?: boolean + worktreeIncludeContent?: string } export interface RequestOpenAiCodexRateLimitsMessage { diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index fd28f2e9945..cb581baf83c 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -35,6 +35,7 @@ export const commandIds = [ "popoutButtonClicked", "cloudButtonClicked", "settingsButtonClicked", + "worktreesButtonClicked", "openInNewTab", diff --git a/packages/types/src/worktree.ts b/packages/types/src/worktree.ts new file mode 100644 index 00000000000..ca98f2d5609 --- /dev/null +++ b/packages/types/src/worktree.ts @@ -0,0 +1,129 @@ +/** + * Worktree Types + * + * Platform-agnostic type definitions for git worktree operations. + * These types are decoupled from VSCode and can be used by any consumer. + */ + +/** + * Represents a git worktree + */ +export interface Worktree { + /** Absolute path to the worktree directory */ + path: string + /** Branch name - empty string if detached HEAD */ + branch: string + /** Current commit hash */ + commitHash: string + /** Whether this is the current worktree (matches cwd) */ + isCurrent: boolean + /** Whether this is the bare/main repository */ + isBare: boolean + /** Whether HEAD is detached (not on a branch) */ + isDetached: boolean + /** Whether the worktree is locked */ + isLocked: boolean + /** Reason for lock if locked */ + lockReason?: string +} + +/** + * Result of a worktree operation (create, delete, etc.) + */ +export interface WorktreeResult { + /** Whether the operation succeeded */ + success: boolean + /** Human-readable message describing the result */ + message: string + /** The worktree that was affected (if applicable) */ + worktree?: Worktree +} + +/** + * Branch information for worktree creation + */ +export interface BranchInfo { + /** Local branches available */ + localBranches: string[] + /** Remote branches available */ + remoteBranches: string[] + /** Currently checked out branch */ + currentBranch: string +} + +/** + * Options for creating a worktree + */ +export interface CreateWorktreeOptions { + /** Path where the worktree will be created */ + path: string + /** Branch name to checkout or create */ + branch?: string + /** Base branch to create new branch from */ + baseBranch?: string + /** If true, create a new branch; if false, checkout existing branch */ + createNewBranch?: boolean +} + +/** + * Options for merging a worktree branch + */ +export interface MergeWorktreeOptions { + /** Path to the worktree being merged */ + worktreePath: string + /** Target branch to merge into */ + targetBranch: string + /** If true, delete the worktree after successful merge */ + deleteAfterMerge?: boolean +} + +/** + * Result of a merge operation + */ +export interface MergeWorktreeResult { + /** Whether the merge succeeded */ + success: boolean + /** Human-readable message describing the result */ + message: string + /** Whether there are merge conflicts */ + hasConflicts: boolean + /** List of files with conflicts */ + conflictingFiles: string[] + /** Source branch that was merged */ + sourceBranch?: string + /** Target branch that was merged into */ + targetBranch?: string +} + +/** + * Status of .worktreeinclude file + */ +export interface WorktreeIncludeStatus { + /** Whether .worktreeinclude exists in the directory */ + exists: boolean + /** Whether .gitignore exists in the directory */ + hasGitignore: boolean + /** Content of .gitignore (for creating .worktreeinclude) */ + gitignoreContent?: string +} + +/** + * Response for listWorktrees handler + */ +export interface WorktreeListResponse { + worktrees: Worktree[] + isGitRepo: boolean + error?: string + isMultiRoot: boolean + isSubfolder: boolean + gitRootPath: string +} + +/** + * Response for worktree defaults + */ +export interface WorktreeDefaultsResponse { + suggestedBranch: string + suggestedPath: string + error?: string +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 177d0b3e5ab..501f170b62f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,6 +103,12 @@ importers: commander: specifier: ^12.1.0 version: 12.1.0 + cross-spawn: + specifier: ^7.0.6 + version: 7.0.6 + execa: + specifier: ^9.5.2 + version: 9.6.0 fuzzysort: specifier: ^3.1.0 version: 3.1.0 @@ -551,6 +557,9 @@ importers: execa: specifier: ^9.5.2 version: 9.6.0 + ignore: + specifier: ^7.0.3 + version: 7.0.5 openai: specifier: ^5.12.2 version: 5.12.2(ws@8.18.3)(zod@3.25.76) @@ -1115,6 +1124,9 @@ importers: '@radix-ui/react-progress': specifier: ^1.1.2 version: 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: ^1.3.8 + version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-select': specifier: ^2.1.6 version: 2.2.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3074,6 +3086,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': ^18.3.23 + '@types/react-dom': ^18.3.5 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.10': resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} peerDependencies: @@ -3087,6 +3112,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': ^18.3.23 + '@types/react-dom': ^18.3.5 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.9': resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==} peerDependencies: @@ -12862,6 +12900,24 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -12879,6 +12935,23 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-roving-focus@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index f02ee8309a3..51368dd9fc7 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -134,6 +134,12 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt if (!visibleProvider) return visibleProvider.postMessageToWebview({ type: "action", action: "marketplaceButtonClicked" }) }, + worktreesButtonClicked: () => { + const visibleProvider = getVisibleProviderOrLog(outputChannel) + if (!visibleProvider) return + TelemetryService.instance.captureTitleButtonClicked("worktrees") + visibleProvider.postMessageToWebview({ type: "action", action: "worktreesButtonClicked" }) + }, newTask: handleNewTask, setCustomStoragePath: async () => { const { promptForCustomStoragePath } = await import("../utils/storage") diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index ce4646418bf..2af791b93ee 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -66,6 +66,19 @@ const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"]) import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace" import { setPendingTodoList } from "../tools/UpdateTodoListTool" +import { + handleListWorktrees, + handleCreateWorktree, + handleDeleteWorktree, + handleSwitchWorktree, + handleGetAvailableBranches, + handleGetWorktreeDefaults, + handleGetWorktreeIncludeStatus, + handleCheckBranchWorktreeInclude, + handleCreateWorktreeInclude, + handleCheckoutBranch, + handleMergeWorktree, +} from "./worktree" export const webviewMessageHandler = async ( provider: ClineProvider, @@ -3389,6 +3402,247 @@ export const webviewMessageHandler = async ( break } + /** + * Git Worktree Management + */ + + case "listWorktrees": { + try { + const { worktrees, isGitRepo, isMultiRoot, isSubfolder, gitRootPath, error } = + await handleListWorktrees(provider) + + await provider.postMessageToWebview({ + type: "worktreeList", + worktrees, + isGitRepo, + isMultiRoot, + isSubfolder, + gitRootPath, + error, + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + await provider.postMessageToWebview({ + type: "worktreeList", + worktrees: [], + isGitRepo: false, + isMultiRoot: false, + isSubfolder: false, + gitRootPath: "", + error: errorMessage, + }) + } + + break + } + + case "createWorktree": { + try { + const { success, message: text } = await handleCreateWorktree(provider, { + path: message.worktreePath!, + branch: message.worktreeBranch, + baseBranch: message.worktreeBaseBranch, + createNewBranch: message.worktreeCreateNewBranch, + }) + + await provider.postMessageToWebview({ type: "worktreeResult", success, text }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + await provider.postMessageToWebview({ type: "worktreeResult", success: false, text: errorMessage }) + } + + break + } + + case "deleteWorktree": { + try { + const { success, message: text } = await handleDeleteWorktree( + provider, + message.worktreePath!, + message.worktreeForce ?? false, + ) + + await provider.postMessageToWebview({ type: "worktreeResult", success, text }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + await provider.postMessageToWebview({ type: "worktreeResult", success: false, text: errorMessage }) + } + + break + } + + case "switchWorktree": { + try { + const { success, message: text } = await handleSwitchWorktree( + provider, + message.worktreePath!, + message.worktreeNewWindow ?? true, + ) + + await provider.postMessageToWebview({ type: "worktreeResult", success, text }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + await provider.postMessageToWebview({ type: "worktreeResult", success: false, text: errorMessage }) + } + + break + } + + case "getAvailableBranches": { + try { + const { localBranches, remoteBranches, currentBranch } = await handleGetAvailableBranches(provider) + + await provider.postMessageToWebview({ + type: "branchList", + localBranches, + remoteBranches, + currentBranch, + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + await provider.postMessageToWebview({ + type: "branchList", + localBranches: [], + remoteBranches: [], + currentBranch: "", + error: errorMessage, + }) + } + + break + } + + case "getWorktreeDefaults": { + try { + const { suggestedBranch, suggestedPath } = await handleGetWorktreeDefaults(provider) + await provider.postMessageToWebview({ type: "worktreeDefaults", suggestedBranch, suggestedPath }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + await provider.postMessageToWebview({ + type: "worktreeDefaults", + suggestedBranch: "", + suggestedPath: "", + error: errorMessage, + }) + } + + break + } + + case "getWorktreeIncludeStatus": { + try { + const worktreeIncludeStatus = await handleGetWorktreeIncludeStatus(provider) + await provider.postMessageToWebview({ type: "worktreeIncludeStatus", worktreeIncludeStatus }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + await provider.postMessageToWebview({ + type: "worktreeIncludeStatus", + worktreeIncludeStatus: { + exists: false, + hasGitignore: false, + gitignoreContent: undefined, + }, + error: errorMessage, + }) + } + + break + } + + case "checkBranchWorktreeInclude": { + try { + const branch = message.worktreeBranch + if (!branch) { + await provider.postMessageToWebview({ + type: "branchWorktreeIncludeResult", + hasWorktreeInclude: false, + error: "No branch specified", + }) + break + } + const hasWorktreeInclude = await handleCheckBranchWorktreeInclude(provider, branch) + await provider.postMessageToWebview({ + type: "branchWorktreeIncludeResult", + branch, + hasWorktreeInclude, + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + await provider.postMessageToWebview({ + type: "branchWorktreeIncludeResult", + hasWorktreeInclude: false, + error: errorMessage, + }) + } + + break + } + + case "createWorktreeInclude": { + try { + const { success, message: text } = await handleCreateWorktreeInclude( + provider, + message.worktreeIncludeContent ?? "", + ) + + await provider.postMessageToWebview({ type: "worktreeResult", success, text }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + provider.log(`Error creating worktree include: ${errorMessage}`) + await provider.postMessageToWebview({ type: "worktreeResult", success: false, text: errorMessage }) + } + + break + } + + case "checkoutBranch": { + try { + const { success, message: text } = await handleCheckoutBranch(provider, message.worktreeBranch!) + await provider.postMessageToWebview({ type: "worktreeResult", success, text }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + await provider.postMessageToWebview({ type: "worktreeResult", success: false, text: errorMessage }) + } + + break + } + + case "mergeWorktree": { + try { + const result = await handleMergeWorktree(provider, { + worktreePath: message.worktreePath!, + targetBranch: message.worktreeTargetBranch!, + deleteAfterMerge: message.worktreeDeleteAfterMerge, + }) + + await provider.postMessageToWebview({ + type: "mergeWorktreeResult", + success: result.success, + text: result.message, + hasConflicts: result.hasConflicts, + conflictingFiles: result.conflictingFiles, + sourceBranch: result.sourceBranch, + targetBranch: result.targetBranch, + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + await provider.postMessageToWebview({ + type: "mergeWorktreeResult", + success: false, + text: errorMessage, + hasConflicts: false, + conflictingFiles: [], + }) + } + + break + } + default: { // console.log(`Unhandled message type: ${message.type}`) // diff --git a/src/core/webview/worktree/handlers.ts b/src/core/webview/worktree/handlers.ts new file mode 100644 index 00000000000..6910bda0695 --- /dev/null +++ b/src/core/webview/worktree/handlers.ts @@ -0,0 +1,277 @@ +/** + * Worktree Handlers + * + * VSCode-specific handlers that bridge webview messages to the core worktree services. + * These handlers handle VSCode-specific logic like opening folders and managing state. + */ + +import * as vscode from "vscode" +import * as path from "path" +import * as os from "os" + +import type { + WorktreeResult, + BranchInfo, + MergeWorktreeResult, + WorktreeIncludeStatus, + WorktreeListResponse, + WorktreeDefaultsResponse, +} from "@roo-code/types" +import { worktreeService, worktreeIncludeService } from "@roo-code/core" + +import type { ClineProvider } from "../ClineProvider" + +/** + * Generate a random alphanumeric suffix for branch/folder names. + */ +function generateRandomSuffix(length = 5): string { + const chars = "abcdefghijklmnopqrstuvwxyz0123456789" + let result = "" + + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)) + } + + return result +} + +async function isWorkspaceSubfolder(cwd: string): Promise { + const gitRoot = await worktreeService.getGitRootPath(cwd) + + if (!gitRoot) { + return false + } + + // Normalize paths for comparison. + const normalizedCwd = path.normalize(cwd) + const normalizedGitRoot = path.normalize(gitRoot) + + // If cwd is deeper than git root, it's a subfolder. + return normalizedCwd !== normalizedGitRoot && normalizedCwd.startsWith(normalizedGitRoot) +} + +export async function handleListWorktrees(provider: ClineProvider): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders + const isMultiRoot = workspaceFolders ? workspaceFolders.length > 1 : false + + if (!workspaceFolders || workspaceFolders.length === 0) { + return { + worktrees: [], + isGitRepo: false, + isMultiRoot: false, + isSubfolder: false, + gitRootPath: "", + error: "No workspace folder open", + } + } + + // Multi-root workspaces not supported for worktrees. + if (isMultiRoot) { + return { + worktrees: [], + isGitRepo: false, + isMultiRoot: true, + isSubfolder: false, + gitRootPath: "", + error: "Worktrees are not supported in multi-root workspaces", + } + } + + const cwd = provider.cwd + const isGitRepo = await worktreeService.checkGitRepo(cwd) + + if (!isGitRepo) { + return { + worktrees: [], + isGitRepo: false, + isMultiRoot: false, + isSubfolder: false, + gitRootPath: "", + error: "Not a git repository", + } + } + + const isSubfolder = await isWorkspaceSubfolder(cwd) + const gitRootPath = (await worktreeService.getGitRootPath(cwd)) || "" + + if (isSubfolder) { + return { + worktrees: [], + isGitRepo: true, + isMultiRoot: false, + isSubfolder: true, + gitRootPath, + error: "Worktrees are not supported when workspace is a subfolder of a git repository", + } + } + + try { + const worktrees = await worktreeService.listWorktrees(cwd) + + return { + worktrees, + isGitRepo: true, + isMultiRoot: false, + isSubfolder: false, + gitRootPath, + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + return { + worktrees: [], + isGitRepo: true, + isMultiRoot: false, + isSubfolder: false, + gitRootPath, + error: `Failed to list worktrees: ${errorMessage}`, + } + } +} + +export async function handleCreateWorktree( + provider: ClineProvider, + options: { + path: string + branch?: string + baseBranch?: string + createNewBranch?: boolean + }, +): Promise { + const cwd = provider.cwd + + const isGitRepo = await worktreeService.checkGitRepo(cwd) + + if (!isGitRepo) { + return { + success: false, + message: "Not a git repository", + } + } + + const result = await worktreeService.createWorktree(cwd, options) + + // If successful and .worktreeinclude exists, copy the files. + if (result.success && result.worktree) { + try { + const copiedItems = await worktreeIncludeService.copyWorktreeIncludeFiles(cwd, result.worktree.path) + if (copiedItems.length > 0) { + result.message += ` (copied ${copiedItems.length} item(s) from .worktreeinclude)` + } + } catch (error) { + // Log but don't fail the worktree creation. + provider.log(`Warning: Failed to copy .worktreeinclude files: ${error}`) + } + } + + return result +} + +export async function handleDeleteWorktree( + provider: ClineProvider, + worktreePath: string, + force = false, +): Promise { + const cwd = provider.cwd + return worktreeService.deleteWorktree(cwd, worktreePath, force) +} + +export async function handleSwitchWorktree( + provider: ClineProvider, + worktreePath: string, + newWindow: boolean, +): Promise { + try { + const worktreeUri = vscode.Uri.file(worktreePath) + + if (newWindow) { + // Set the auto-open path so the new window opens Roo Code sidebar. + await provider.contextProxy.setValue("worktreeAutoOpenPath", worktreePath) + + // Open in new window. + await vscode.commands.executeCommand("vscode.openFolder", worktreeUri, { forceNewWindow: true }) + } else { + // For current window, we need to flush pending state first since window will reload. + await provider.contextProxy.setValue("worktreeAutoOpenPath", worktreePath) + + // Open in current window (this will reload the window). + await vscode.commands.executeCommand("vscode.openFolder", worktreeUri, { forceNewWindow: false }) + } + + return { + success: true, + message: `Opened worktree at ${worktreePath}`, + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + return { + success: false, + message: `Failed to switch worktree: ${errorMessage}`, + } + } +} + +export async function handleGetAvailableBranches(provider: ClineProvider): Promise { + const cwd = provider.cwd + // Include branches already in worktrees since we use this for base branch selection + return worktreeService.getAvailableBranches(cwd, true) +} + +export async function handleGetWorktreeDefaults(provider: ClineProvider): Promise { + const suffix = generateRandomSuffix() + const workspaceFolders = vscode.workspace.workspaceFolders + const projectName = workspaceFolders?.[0]?.name || "project" + + const dotRooPath = path.join(os.homedir(), ".roo") + const suggestedPath = path.join(dotRooPath, "worktrees", `${projectName}-${suffix}`) + + return { + suggestedBranch: `worktree/roo-${suffix}`, + suggestedPath, + } +} + +export async function handleGetWorktreeIncludeStatus(provider: ClineProvider): Promise { + const cwd = provider.cwd + return worktreeIncludeService.getStatus(cwd) +} + +export async function handleCheckBranchWorktreeInclude(provider: ClineProvider, branch: string): Promise { + const cwd = provider.cwd + return worktreeIncludeService.branchHasWorktreeInclude(cwd, branch) +} + +export async function handleCreateWorktreeInclude(provider: ClineProvider, content: string): Promise { + const cwd = provider.cwd + + try { + await worktreeIncludeService.createWorktreeInclude(cwd, content) + return { + success: true, + message: ".worktreeinclude file created", + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + return { + success: false, + message: `Failed to create .worktreeinclude: ${errorMessage}`, + } + } +} + +export async function handleCheckoutBranch(provider: ClineProvider, branch: string): Promise { + const cwd = provider.cwd + return worktreeService.checkoutBranch(cwd, branch) +} + +export async function handleMergeWorktree( + provider: ClineProvider, + options: { + worktreePath: string + targetBranch: string + deleteAfterMerge?: boolean + }, +): Promise { + const cwd = provider.cwd + return worktreeService.mergeWorktree(cwd, options) +} diff --git a/src/core/webview/worktree/index.ts b/src/core/webview/worktree/index.ts new file mode 100644 index 00000000000..33627d64691 --- /dev/null +++ b/src/core/webview/worktree/index.ts @@ -0,0 +1,23 @@ +/** + * Worktree Module + * + * VSCode-specific handlers for git worktree management. + * Bridges webview messages to the platform-agnostic core services. + */ + +export { + handleListWorktrees, + handleCreateWorktree, + handleDeleteWorktree, + handleSwitchWorktree, + handleGetAvailableBranches, + handleGetWorktreeDefaults, + handleGetWorktreeIncludeStatus, + handleCheckBranchWorktreeInclude, + handleCreateWorktreeInclude, + handleCheckoutBranch, + handleMergeWorktree, +} from "./handlers" + +// Re-export types from @roo-code/types for convenience +export type { WorktreeListResponse, WorktreeDefaultsResponse } from "@roo-code/types" diff --git a/src/extension.ts b/src/extension.ts index c12f223f954..b58f3ce0fa0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -62,6 +62,55 @@ let authStateChangedHandler: ((data: { state: AuthState; previousState: AuthStat let settingsUpdatedHandler: (() => void) | undefined let userInfoHandler: ((data: { userInfo: CloudUserInfo }) => Promise) | undefined +/** + * Check if we should auto-open the Roo Code sidebar after switching to a worktree. + * This is called during extension activation to handle the worktree auto-open flow. + */ +async function checkWorktreeAutoOpen( + context: vscode.ExtensionContext, + outputChannel: vscode.OutputChannel, +): Promise { + try { + const worktreeAutoOpenPath = context.globalState.get("worktreeAutoOpenPath") + if (!worktreeAutoOpenPath) { + return + } + + const workspaceFolders = vscode.workspace.workspaceFolders + if (!workspaceFolders || workspaceFolders.length === 0) { + return + } + + const currentPath = workspaceFolders[0].uri.fsPath + + // Normalize paths for comparison + const normalizePath = (p: string) => p.replace(/\/+$/, "").replace(/\\+/g, "/").toLowerCase() + + // Check if current workspace matches the worktree path + if (normalizePath(currentPath) === normalizePath(worktreeAutoOpenPath)) { + // Clear the state first to prevent re-triggering + await context.globalState.update("worktreeAutoOpenPath", undefined) + + outputChannel.appendLine(`[Worktree] Auto-opening Roo Code sidebar for worktree: ${worktreeAutoOpenPath}`) + + // Open the Roo Code sidebar with a slight delay to ensure UI is ready + setTimeout(async () => { + try { + await vscode.commands.executeCommand("roo-cline.plusButtonClicked") + } catch (error) { + outputChannel.appendLine( + `[Worktree] Error auto-opening sidebar: ${error instanceof Error ? error.message : String(error)}`, + ) + } + }, 500) + } + } catch (error) { + outputChannel.appendLine( + `[Worktree] Error checking worktree auto-open: ${error instanceof Error ? error.message : String(error)}`, + ) + } +} + // This method is called when your extension is activated. // Your extension is activated the very first time the command is executed. export async function activate(context: vscode.ExtensionContext) { @@ -284,6 +333,9 @@ export async function activate(context: vscode.ExtensionContext) { }), ) + // Check for worktree auto-open path (set when switching to a worktree) + await checkWorktreeAutoOpen(context, outputChannel) + // Auto-import configuration if specified in settings. try { await autoImportSettings(outputChannel, { diff --git a/src/package.json b/src/package.json index 8f1ad0da09c..54205de1c06 100644 --- a/src/package.json +++ b/src/package.json @@ -100,6 +100,11 @@ "title": "%command.settings.title%", "icon": "$(settings-gear)" }, + { + "command": "roo-cline.worktreesButtonClicked", + "title": "%command.worktrees.title%", + "icon": "$(git-branch)" + }, { "command": "roo-cline.openInNewTab", "title": "%command.openInNewTab.title%", @@ -239,9 +244,14 @@ "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.popoutButtonClicked", + "command": "roo-cline.worktreesButtonClicked", "group": "overflow@2", "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.popoutButtonClicked", + "group": "overflow@3", + "when": "view == roo-cline.SidebarProvider" } ], "editor/title": [ @@ -271,9 +281,14 @@ "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.popoutButtonClicked", + "command": "roo-cline.worktreesButtonClicked", "group": "overflow@2", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" + }, + { + "command": "roo-cline.popoutButtonClicked", + "group": "overflow@3", + "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" } ] }, diff --git a/src/package.nls.ca.json b/src/package.nls.ca.json index 2781ed169cf..61cf128d7f6 100644 --- a/src/package.nls.ca.json +++ b/src/package.nls.ca.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "Obrir a l'Editor", "command.cloud.title": "Cloud", "command.settings.title": "Configuració", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Documentació", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Ordres que es poden executar automàticament quan 'Aprova sempre les operacions d'execució' està activat", diff --git a/src/package.nls.de.json b/src/package.nls.de.json index a77a253ef06..7e9ce2cda0e 100644 --- a/src/package.nls.de.json +++ b/src/package.nls.de.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "Im Editor Öffnen", "command.cloud.title": "Cloud", "command.settings.title": "Einstellungen", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Dokumentation", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Befehle, die automatisch ausgeführt werden können, wenn 'Ausführungsoperationen immer genehmigen' aktiviert ist", diff --git a/src/package.nls.es.json b/src/package.nls.es.json index a1c729080e2..a4af6dd715e 100644 --- a/src/package.nls.es.json +++ b/src/package.nls.es.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "Abrir en Editor", "command.cloud.title": "Cloud", "command.settings.title": "Configuración", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Documentación", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Comandos que pueden ejecutarse automáticamente cuando 'Aprobar siempre operaciones de ejecución' está activado", diff --git a/src/package.nls.fr.json b/src/package.nls.fr.json index 2d009c0038d..835f9fe3c82 100644 --- a/src/package.nls.fr.json +++ b/src/package.nls.fr.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "Ouvrir dans l'Éditeur", "command.cloud.title": "Cloud", "command.settings.title": "Paramètres", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Documentation", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Commandes pouvant être exécutées automatiquement lorsque 'Toujours approuver les opérations d'exécution' est activé", diff --git a/src/package.nls.hi.json b/src/package.nls.hi.json index c51f3ee95ee..eae3e253dd0 100644 --- a/src/package.nls.hi.json +++ b/src/package.nls.hi.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "एडिटर में खोलें", "command.cloud.title": "Cloud", "command.settings.title": "सेटिंग्स", + "command.worktrees.title": "Worktrees", "command.documentation.title": "दस्तावेज़ीकरण", "configuration.title": "Roo Code", "commands.allowedCommands.description": "वे कमांड जो स्वचालित रूप से निष्पादित की जा सकती हैं जब 'हमेशा निष्पादन संचालन को स्वीकृत करें' सक्रिय हो", diff --git a/src/package.nls.id.json b/src/package.nls.id.json index 2a7607f3e7c..8a9e6a0dc68 100644 --- a/src/package.nls.id.json +++ b/src/package.nls.id.json @@ -11,6 +11,7 @@ "command.openInEditor.title": "Buka di Editor", "command.cloud.title": "Cloud", "command.settings.title": "Pengaturan", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Dokumentasi", "command.openInNewTab.title": "Buka di Tab Baru", "command.explainCode.title": "Jelaskan Kode", diff --git a/src/package.nls.it.json b/src/package.nls.it.json index c94471355d4..0c05597f7ca 100644 --- a/src/package.nls.it.json +++ b/src/package.nls.it.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "Apri nell'Editor", "command.cloud.title": "Cloud", "command.settings.title": "Impostazioni", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Documentazione", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Comandi che possono essere eseguiti automaticamente quando 'Approva sempre le operazioni di esecuzione' è attivato", diff --git a/src/package.nls.ja.json b/src/package.nls.ja.json index ff6040d7734..9d2c07aec01 100644 --- a/src/package.nls.ja.json +++ b/src/package.nls.ja.json @@ -11,6 +11,7 @@ "command.openInEditor.title": "エディタで開く", "command.cloud.title": "Cloud", "command.settings.title": "設定", + "command.worktrees.title": "Worktrees", "command.documentation.title": "ドキュメント", "command.openInNewTab.title": "新しいタブで開く", "command.explainCode.title": "コードの説明", diff --git a/src/package.nls.json b/src/package.nls.json index 177b392f775..483574408c6 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -11,6 +11,7 @@ "command.openInEditor.title": "Open in Editor", "command.cloud.title": "Cloud", "command.settings.title": "Settings", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Documentation", "command.openInNewTab.title": "Open In New Tab", "command.explainCode.title": "Explain Code", diff --git a/src/package.nls.ko.json b/src/package.nls.ko.json index f0912835b8b..07a0a4bc074 100644 --- a/src/package.nls.ko.json +++ b/src/package.nls.ko.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "에디터에서 열기", "command.cloud.title": "Cloud", "command.settings.title": "설정", + "command.worktrees.title": "Worktrees", "command.documentation.title": "문서", "configuration.title": "Roo Code", "commands.allowedCommands.description": "'항상 실행 작업 승인' 이 활성화되어 있을 때 자동으로 실행할 수 있는 명령어", diff --git a/src/package.nls.nl.json b/src/package.nls.nl.json index fef3ca7219c..00f31a5368f 100644 --- a/src/package.nls.nl.json +++ b/src/package.nls.nl.json @@ -11,6 +11,7 @@ "command.openInEditor.title": "Openen in Editor", "command.cloud.title": "Cloud", "command.settings.title": "Instellingen", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Documentatie", "command.openInNewTab.title": "Openen in Nieuw Tabblad", "command.explainCode.title": "Leg Code Uit", diff --git a/src/package.nls.pl.json b/src/package.nls.pl.json index 8c1f66450d1..af838c6e5d4 100644 --- a/src/package.nls.pl.json +++ b/src/package.nls.pl.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "Otwórz w Edytorze", "command.cloud.title": "Cloud", "command.settings.title": "Ustawienia", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Dokumentacja", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Polecenia, które mogą być wykonywane automatycznie, gdy włączona jest opcja 'Zawsze zatwierdzaj operacje wykonania'", diff --git a/src/package.nls.pt-BR.json b/src/package.nls.pt-BR.json index 84cbf42c097..4db94b82c0a 100644 --- a/src/package.nls.pt-BR.json +++ b/src/package.nls.pt-BR.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "Abrir no Editor", "command.cloud.title": "Cloud", "command.settings.title": "Configurações", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Documentação", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Comandos que podem ser executados automaticamente quando 'Sempre aprovar operações de execução' está ativado", diff --git a/src/package.nls.ru.json b/src/package.nls.ru.json index be8df040323..379b99bdf01 100644 --- a/src/package.nls.ru.json +++ b/src/package.nls.ru.json @@ -11,6 +11,7 @@ "command.openInEditor.title": "Открыть в редакторе", "command.cloud.title": "Cloud", "command.settings.title": "Настройки", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Документация", "command.openInNewTab.title": "Открыть в новой вкладке", "command.explainCode.title": "Объяснить код", diff --git a/src/package.nls.tr.json b/src/package.nls.tr.json index a815188e8aa..ad018116231 100644 --- a/src/package.nls.tr.json +++ b/src/package.nls.tr.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "Düzenleyicide Aç", "command.cloud.title": "Cloud", "command.settings.title": "Ayarlar", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Dokümantasyon", "configuration.title": "Roo Code", "commands.allowedCommands.description": "'Her zaman yürütme işlemlerini onayla' etkinleştirildiğinde otomatik olarak yürütülebilen komutlar", diff --git a/src/package.nls.vi.json b/src/package.nls.vi.json index 6052080dfa3..99157668e25 100644 --- a/src/package.nls.vi.json +++ b/src/package.nls.vi.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "Mở trong Trình Soạn Thảo", "command.cloud.title": "Cloud", "command.settings.title": "Cài Đặt", + "command.worktrees.title": "Worktrees", "command.documentation.title": "Tài Liệu", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Các lệnh có thể được thực thi tự động khi 'Luôn phê duyệt các thao tác thực thi' được bật", diff --git a/src/package.nls.zh-CN.json b/src/package.nls.zh-CN.json index 9254d494d9b..f973934db1f 100644 --- a/src/package.nls.zh-CN.json +++ b/src/package.nls.zh-CN.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "在编辑器中打开", "command.cloud.title": "Cloud", "command.settings.title": "设置", + "command.worktrees.title": "Worktrees", "command.documentation.title": "文档", "configuration.title": "Roo Code", "commands.allowedCommands.description": "当启用'始终批准执行操作'时可以自动执行的命令", diff --git a/src/package.nls.zh-TW.json b/src/package.nls.zh-TW.json index a8030d69141..84d50befd9a 100644 --- a/src/package.nls.zh-TW.json +++ b/src/package.nls.zh-TW.json @@ -24,6 +24,7 @@ "command.openInEditor.title": "在編輯器中開啟", "command.cloud.title": "Cloud", "command.settings.title": "設定", + "command.worktrees.title": "Worktrees", "command.documentation.title": "文件", "configuration.title": "Roo Code", "commands.allowedCommands.description": "當啟用'始終批准執行操作'時可以自動執行的命令", diff --git a/webview-ui/package.json b/webview-ui/package.json index a316861389b..1ffc514a74d 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-portal": "^1.1.5", "@radix-ui/react-progress": "^1.1.2", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slider": "^1.2.3", diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index cccb0422ca8..b5de22978ba 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -20,11 +20,12 @@ import { CheckpointRestoreDialog } from "./components/chat/CheckpointRestoreDial import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog" import ErrorBoundary from "./components/ErrorBoundary" import { CloudView } from "./components/cloud/CloudView" +import { WorktreesView } from "./components/worktrees" import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick" import { TooltipProvider } from "./components/ui/tooltip" import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip" -type Tab = "settings" | "history" | "chat" | "marketplace" | "cloud" +type Tab = "settings" | "history" | "chat" | "marketplace" | "cloud" | "worktrees" interface DeleteMessageDialogState { isOpen: boolean @@ -50,6 +51,7 @@ const tabsByMessageAction: Partial { @@ -245,6 +247,7 @@ const App = () => { organizations={cloudOrganizations} /> )} + {tab === "worktrees" && switchTab("chat")} />} , React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) +>(({ className, onWheel, ...props }, ref) => { + const handleWheel = React.useCallback( + (e: React.WheelEvent) => { + // Manually handle scroll to work around VSCode webview scroll issues + const target = e.currentTarget + e.preventDefault() + target.scrollTop += e.deltaY + e.stopPropagation() + onWheel?.(e) + }, + [onWheel], + ) + + return ( + + ) +}) CommandList.displayName = CommandPrimitive.List.displayName diff --git a/webview-ui/src/components/ui/index.ts b/webview-ui/src/components/ui/index.ts index ee28b964c54..f1ed90e07eb 100644 --- a/webview-ui/src/components/ui/index.ts +++ b/webview-ui/src/components/ui/index.ts @@ -10,6 +10,7 @@ export * from "./dropdown-menu" export * from "./input" export * from "./popover" export * from "./progress" +export * from "./radio-group" export * from "./searchable-select" export * from "./separator" export * from "./slider" diff --git a/webview-ui/src/components/ui/radio-group.tsx b/webview-ui/src/components/ui/radio-group.tsx new file mode 100644 index 00000000000..02acf7e5e96 --- /dev/null +++ b/webview-ui/src/components/ui/radio-group.tsx @@ -0,0 +1,35 @@ +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/webview-ui/src/components/ui/searchable-select.tsx b/webview-ui/src/components/ui/searchable-select.tsx index ec4067c6ef8..6e6b30b2b3e 100644 --- a/webview-ui/src/components/ui/searchable-select.tsx +++ b/webview-ui/src/components/ui/searchable-select.tsx @@ -31,6 +31,8 @@ interface SearchableSelectProps { emptyMessage: string className?: string disabled?: boolean + /** Maximum items to display when not searching. Defaults to 50 for performance. */ + maxDisplayItems?: number "data-testid"?: string } @@ -43,6 +45,7 @@ export function SearchableSelect({ emptyMessage, className, disabled, + maxDisplayItems = 50, "data-testid": dataTestId, }: SearchableSelectProps) { const [open, setOpen] = React.useState(false) @@ -54,11 +57,34 @@ export function SearchableSelect({ // Find the selected option const selectedOption = options.find((option) => option.value === value) - // Filter options based on search + // Filter options based on search, always limit for performance. + // Ensure the selected option remains visible even when truncating. const filteredOptions = React.useMemo(() => { - if (!searchValue) return options - return options.filter((option) => option.label.toLowerCase().includes(searchValue.toLowerCase())) - }, [options, searchValue]) + const normalizedSearch = searchValue.trim().toLowerCase() + const matchingOptions = + normalizedSearch.length === 0 + ? options + : options.filter((option) => option.label.toLowerCase().includes(normalizedSearch)) + + if (matchingOptions.length <= maxDisplayItems) { + return matchingOptions + } + + const limitedOptions = matchingOptions.slice(0, maxDisplayItems) + if (!selectedOption) { + return limitedOptions + } + + // If the selected option would be truncated away, prepend it (but only if it matches the current filter). + if ( + matchingOptions.includes(selectedOption) && + !limitedOptions.some((option) => option.value === selectedOption.value) + ) { + return [selectedOption, ...limitedOptions.slice(0, maxDisplayItems - 1)] + } + + return limitedOptions + }, [options, searchValue, maxDisplayItems, selectedOption]) // Cleanup timeout on unmount React.useEffect(() => { @@ -129,7 +155,7 @@ export function SearchableSelect({ - +
) diff --git a/webview-ui/src/components/worktrees/CreateWorktreeModal.tsx b/webview-ui/src/components/worktrees/CreateWorktreeModal.tsx new file mode 100644 index 00000000000..2e54403b854 --- /dev/null +++ b/webview-ui/src/components/worktrees/CreateWorktreeModal.tsx @@ -0,0 +1,230 @@ +import { useState, useEffect, useCallback, useMemo } from "react" + +import type { WorktreeDefaultsResponse, BranchInfo, WorktreeIncludeStatus } from "@roo-code/types" + +import { vscode } from "@/utils/vscode" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Button, + Input, +} from "@/components/ui" +import { SearchableSelect, type SearchableSelectOption } from "@/components/ui/searchable-select" + +interface CreateWorktreeModalProps { + open: boolean + onClose: () => void + openAfterCreate?: boolean + onSuccess?: () => void +} + +export const CreateWorktreeModal = ({ + open, + onClose, + openAfterCreate = false, + onSuccess, +}: CreateWorktreeModalProps) => { + const { t } = useAppTranslation() + + // Form state + const [branchName, setBranchName] = useState("") + const [worktreePath, setWorktreePath] = useState("") + const [baseBranch, setBaseBranch] = useState("") + + // Data state + const [defaults, setDefaults] = useState(null) + const [branches, setBranches] = useState(null) + const [includeStatus, setIncludeStatus] = useState(null) + + // UI state + const [isCreating, setIsCreating] = useState(false) + const [error, setError] = useState(null) + + // Fetch defaults and branches on open + useEffect(() => { + if (open) { + vscode.postMessage({ type: "getWorktreeDefaults" }) + vscode.postMessage({ type: "getAvailableBranches" }) + vscode.postMessage({ type: "getWorktreeIncludeStatus" }) + } + }, [open]) + + // Handle messages from extension + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + switch (message.type) { + case "worktreeDefaults": { + const data = message as WorktreeDefaultsResponse + setDefaults(data) + setBranchName(data.suggestedBranch) + setWorktreePath(data.suggestedPath) + break + } + case "branchList": { + const data = message as BranchInfo + setBranches(data) + setBaseBranch(data.currentBranch || "main") + break + } + case "worktreeIncludeStatus": { + setIncludeStatus(message.worktreeIncludeStatus) + break + } + case "worktreeResult": { + setIsCreating(false) + if (message.success) { + if (openAfterCreate) { + vscode.postMessage({ + type: "switchWorktree", + worktreePath: worktreePath, + worktreeNewWindow: true, + }) + } + onSuccess?.() + onClose() + } else { + setError(message.text || "Unknown error") + } + break + } + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, [openAfterCreate, worktreePath, onSuccess, onClose]) + + const handleCreate = useCallback(() => { + setError(null) + setIsCreating(true) + + vscode.postMessage({ + type: "createWorktree", + worktreePath: worktreePath, + worktreeBranch: branchName, + worktreeBaseBranch: baseBranch, + worktreeCreateNewBranch: true, + }) + }, [worktreePath, branchName, baseBranch]) + + const isValid = branchName.trim() && worktreePath.trim() && baseBranch.trim() + + // Convert branches to SearchableSelect options format + const branchOptions = useMemo((): SearchableSelectOption[] => { + if (!branches) return [] + + const localOptions: SearchableSelectOption[] = branches.localBranches.map((branch) => ({ + value: branch, + label: branch, + icon: , + })) + + const remoteOptions: SearchableSelectOption[] = branches.remoteBranches.map((branch) => ({ + value: branch, + label: branch, + icon: , + })) + + return [...localOptions, ...remoteOptions] + }, [branches]) + + return ( + !isOpen && onClose()}> + + + {t("worktrees:createWorktree")} + {t("worktrees:createWorktreeDescription")} + + +
+ {/* No .worktreeinclude warning - shows when the current worktree doesn't have .worktreeinclude */} + {includeStatus?.exists === false && ( +
+ + + {t("worktrees:noIncludeFileWarning")} + {" — "} + + {t("worktrees:noIncludeFileHint")} + + +
+ )} + + {/* Branch name */} +
+ + setBranchName(e.target.value)} + placeholder={defaults?.suggestedBranch || "worktree/feature-name"} + className="rounded-full" + /> +
+ + {/* Base branch selector */} +
+ + {!branches ? ( +
+ + {t("worktrees:loadingBranches")} +
+ ) : ( + + )} +
+ + {/* Worktree path */} +
+ + setWorktreePath(e.target.value)} + placeholder={defaults?.suggestedPath || "/path/to/worktree"} + className="rounded-full" + /> +

{t("worktrees:pathHint")}

+
+ + {/* Error message */} + {error && ( +
+ +

{error}

+
+ )} +
+ + + + + +
+
+ ) +} diff --git a/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx b/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx new file mode 100644 index 00000000000..0df8541e111 --- /dev/null +++ b/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx @@ -0,0 +1,142 @@ +import { useState, useEffect, useCallback } from "react" + +import type { Worktree } from "@roo-code/types" + +import { vscode } from "@/utils/vscode" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Button, + Checkbox, +} from "@/components/ui" + +interface DeleteWorktreeModalProps { + open: boolean + onClose: () => void + worktree: Worktree + onSuccess?: () => void +} + +export const DeleteWorktreeModal = ({ open, onClose, worktree, onSuccess }: DeleteWorktreeModalProps) => { + const { t } = useAppTranslation() + + const [isDeleting, setIsDeleting] = useState(false) + const [forceDelete, setForceDelete] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + + if (message.type === "worktreeResult") { + setIsDeleting(false) + if (message.success) { + onSuccess?.() + onClose() + } else { + setError(message.text || "Unknown error") + } + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, [onSuccess, onClose]) + + const handleDelete = useCallback(() => { + setError(null) + setIsDeleting(true) + + vscode.postMessage({ + type: "deleteWorktree", + worktreePath: worktree.path, + worktreeForce: forceDelete, + }) + }, [worktree.path, forceDelete]) + + return ( + !isOpen && onClose()}> + + +
+ + {t("worktrees:deleteWorktree")} +
+ {t("worktrees:deleteWorktreeDescription")} +
+ +
+ {/* Worktree info */} +
+
+ + + {worktree.branch || + (worktree.isDetached ? t("worktrees:detachedHead") : t("worktrees:noBranch"))} + +
+
{worktree.path}
+
+ + {/* Warning message */} +
+ +
+

{t("worktrees:deleteWarning")}

+
    +
  • • {t("worktrees:deleteWarningBranch", { branch: worktree.branch || "HEAD" })}
  • +
  • • {t("worktrees:deleteWarningFiles")}
  • +
+
+
+ + {/* Force delete option (if worktree is locked) */} + {worktree.isLocked && ( +
+ setForceDelete(checked === true)} + /> + +
+ )} + + {/* Error message */} + {error && ( +
+ +

{error}

+
+ )} +
+ + + + + +
+
+ ) +} diff --git a/webview-ui/src/components/worktrees/WorktreesView.tsx b/webview-ui/src/components/worktrees/WorktreesView.tsx new file mode 100644 index 00000000000..f435969b0dc --- /dev/null +++ b/webview-ui/src/components/worktrees/WorktreesView.tsx @@ -0,0 +1,526 @@ +import { useState, useEffect, useCallback } from "react" + +import type { Worktree, WorktreeListResponse, MergeWorktreeResult, WorktreeIncludeStatus } from "@roo-code/types" + +import { Badge, Button, StandardTooltip } from "@/components/ui" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { vscode } from "@/utils/vscode" + +import { Tab, TabContent, TabHeader } from "../common/Tab" + +import { CreateWorktreeModal } from "./CreateWorktreeModal" +import { DeleteWorktreeModal } from "./DeleteWorktreeModal" + +type WorktreesViewProps = { + onDone: () => void +} + +export const WorktreesView = ({ onDone }: WorktreesViewProps) => { + const { t } = useAppTranslation() + + // State + const [worktrees, setWorktrees] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [isGitRepo, setIsGitRepo] = useState(true) + const [isMultiRoot, setIsMultiRoot] = useState(false) + const [isSubfolder, setIsSubfolder] = useState(false) + const [gitRootPath, setGitRootPath] = useState("") + + // Worktree include status + const [includeStatus, setIncludeStatus] = useState(null) + const [isCreatingInclude, setIsCreatingInclude] = useState(false) + + // Modals + const [showCreateModal, setShowCreateModal] = useState(false) + const [deleteWorktree, setDeleteWorktree] = useState(null) + + // Merge state + const [mergeWorktree, setMergeWorktree] = useState(null) + const [mergeTargetBranch, setMergeTargetBranch] = useState("") + const [mergeDeleteAfter, setMergeDeleteAfter] = useState(false) + const [isMerging, setIsMerging] = useState(false) + const [mergeResult, setMergeResult] = useState(null) + + // Fetch worktrees list + const fetchWorktrees = useCallback(() => { + vscode.postMessage({ type: "listWorktrees" }) + }, []) + + // Fetch worktree include status + const fetchIncludeStatus = useCallback(() => { + vscode.postMessage({ type: "getWorktreeIncludeStatus" }) + }, []) + + // Handle messages from extension + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + switch (message.type) { + case "worktreeList": { + const response: WorktreeListResponse = message + setWorktrees(response.worktrees || []) + setIsGitRepo(response.isGitRepo) + setIsMultiRoot(response.isMultiRoot) + setIsSubfolder(response.isSubfolder) + setGitRootPath(response.gitRootPath) + setError(response.error || null) + setIsLoading(false) + break + } + case "worktreeIncludeStatus": { + console.log("[WorktreesView] Received worktreeIncludeStatus:", message) + setIncludeStatus(message.worktreeIncludeStatus) + break + } + case "worktreeResult": { + console.log("[WorktreesView] Received worktreeResult:", message) + // Refresh list and include status after any worktree operation + fetchWorktrees() + fetchIncludeStatus() + setIsCreatingInclude(false) + break + } + case "mergeWorktreeResult": { + setIsMerging(false) + // Map ExtensionMessage format (text) to MergeWorktreeResult format (message) + setMergeResult({ + success: message.success, + message: message.text || "", + hasConflicts: message.hasConflicts || false, + conflictingFiles: message.conflictingFiles || [], + sourceBranch: message.sourceBranch, + targetBranch: message.targetBranch, + }) + if (message.success) { + fetchWorktrees() + } + break + } + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, [fetchWorktrees, fetchIncludeStatus]) + + // Initial fetch and polling + useEffect(() => { + fetchWorktrees() + fetchIncludeStatus() + + // Poll every 3 seconds for updates + const interval = setInterval(fetchWorktrees, 3000) + return () => clearInterval(interval) + }, [fetchWorktrees, fetchIncludeStatus]) + + // Handle create worktree include file + const handleCreateWorktreeInclude = useCallback(() => { + console.log("[WorktreesView] handleCreateWorktreeInclude called, includeStatus:", includeStatus) + if (!includeStatus?.gitignoreContent) { + console.log("[WorktreesView] No gitignoreContent, returning early") + return + } + setIsCreatingInclude(true) + console.log( + "[WorktreesView] Sending createWorktreeInclude with content length:", + includeStatus.gitignoreContent.length, + ) + vscode.postMessage({ + type: "createWorktreeInclude", + worktreeIncludeContent: includeStatus.gitignoreContent, + } as const) + // Refresh status after a short delay + setTimeout(() => { + fetchIncludeStatus() + setIsCreatingInclude(false) + }, 500) + }, [includeStatus, fetchIncludeStatus]) + + // Handle switch worktree + const handleSwitchWorktree = useCallback((worktreePath: string, newWindow: boolean) => { + vscode.postMessage({ + type: "switchWorktree", + worktreePath: worktreePath, + worktreeNewWindow: newWindow, + }) + }, []) + + // Handle merge + const handleMerge = useCallback(() => { + if (!mergeWorktree) return + setIsMerging(true) + vscode.postMessage({ + type: "mergeWorktree", + worktreePath: mergeWorktree.path, + worktreeTargetBranch: mergeTargetBranch, + worktreeDeleteAfterMerge: mergeDeleteAfter, + }) + }, [mergeWorktree, mergeTargetBranch, mergeDeleteAfter]) + + // Handle "Ask Roo to resolve conflicts" + const handleAskRooResolve = useCallback(() => { + if (!mergeResult) return + // Create a new task with conflict resolution instructions + const conflictMessage = `Please help me resolve merge conflicts in the following files:\n\n${mergeResult.conflictingFiles.map((f) => `- ${f}`).join("\n")}\n\nThe merge was from branch "${mergeResult.sourceBranch}" into "${mergeResult.targetBranch}".` + vscode.postMessage({ + type: "newTask", + text: conflictMessage, + }) + setMergeWorktree(null) + setMergeResult(null) + }, [mergeResult]) + + // Render error states + if (!isGitRepo) { + return ( + + +

{t("worktrees:title")}

+ +
+ +
+ +

{t("worktrees:notGitRepo")}

+
+
+
+ ) + } + + if (isMultiRoot) { + return ( + + +

{t("worktrees:title")}

+ +
+ +
+ +

{t("worktrees:multiRootNotSupported")}

+
+
+
+ ) + } + + if (isSubfolder) { + return ( + + +

{t("worktrees:title")}

+ +
+ +
+ +

{t("worktrees:subfolderNotSupported")}

+

+ {t("worktrees:gitRoot")}:{" "} + {gitRootPath} +

+
+
+
+ ) + } + + // Find the primary (bare/main) worktree for merge target. + const primaryWorktree = worktrees.find((w) => w.isBare || worktrees.indexOf(w) === 0) + + return ( + + +
+

{t("worktrees:title")}

+ +
+

{t("worktrees:description")}

+ + {/* Worktree include status */} + {includeStatus && ( +
+ {includeStatus.exists ? ( + <> + + + {t("worktrees:includeFileExists")} + + + ) : ( + <> + + + {t("worktrees:noIncludeFile")} + + {includeStatus.hasGitignore && ( + + )} + + )} +
+ )} +
+ + + {isLoading ? ( +
+ +
+ ) : error ? ( +
+ +

{error}

+
+ ) : ( +
+ {worktrees.map((worktree) => ( +
+
+
+
+ + + {worktree.branch || + (worktree.isDetached + ? t("worktrees:detachedHead") + : t("worktrees:noBranch"))} + + {worktree.isBare && {t("worktrees:primary")}} + {worktree.isCurrent && ( + {t("worktrees:current")} + )} + {worktree.isLocked && ( + + + + )} +
+
+ {worktree.path} +
+
+ +
+ {!worktree.isCurrent && ( + <> + + + + + + + + )} + {!worktree.isBare && + worktree.branch && + primaryWorktree && + worktree.branch !== primaryWorktree.branch && ( + + + + )} + {!worktree.isBare && !worktree.isCurrent && ( + + + + )} +
+
+
+ ))} + + {/* New Worktree button */} + +
+ )} +
+ + {/* Create Modal */} + {showCreateModal && ( + setShowCreateModal(false)} + onSuccess={() => { + setShowCreateModal(false) + fetchWorktrees() + }} + /> + )} + + {/* Delete Modal */} + {deleteWorktree && ( + setDeleteWorktree(null)} + worktree={deleteWorktree} + onSuccess={() => { + setDeleteWorktree(null) + fetchWorktrees() + }} + /> + )} + + {/* Merge Modal */} + {mergeWorktree && !mergeResult && ( +
+
+

+ {t("worktrees:mergeBranch")} +

+

+ {t("worktrees:mergeDescription", { + source: mergeWorktree.branch, + target: mergeTargetBranch, + })} +

+ +
+ +
+ +
+ + +
+
+
+ )} + + {/* Merge Result Modal */} + {mergeResult && ( +
+
+ {mergeResult.success ? ( + <> +
+ +

+ {t("worktrees:mergeSuccess")} +

+
+

{mergeResult.message}

+
+ +
+ + ) : mergeResult.hasConflicts ? ( + <> +
+ +

+ {t("worktrees:mergeConflicts")} +

+
+

+ {t("worktrees:conflictsDescription")} +

+
+ {mergeResult.conflictingFiles.map((file) => ( +
+ {file} +
+ ))} +
+
+ + +
+ + ) : ( + <> +
+ +

+ {t("worktrees:mergeFailed")} +

+
+

{mergeResult.message}

+
+ +
+ + )} +
+
+ )} +
+ ) +} diff --git a/webview-ui/src/components/worktrees/index.ts b/webview-ui/src/components/worktrees/index.ts new file mode 100644 index 00000000000..cdf20f972e6 --- /dev/null +++ b/webview-ui/src/components/worktrees/index.ts @@ -0,0 +1,3 @@ +export { WorktreesView } from "./WorktreesView" +export { CreateWorktreeModal } from "./CreateWorktreeModal" +export { DeleteWorktreeModal } from "./DeleteWorktreeModal" diff --git a/webview-ui/src/i18n/locales/ca/worktrees.json b/webview-ui/src/i18n/locales/ca/worktrees.json new file mode 100644 index 00000000000..e608771a65c --- /dev/null +++ b/webview-ui/src/i18n/locales/ca/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Fet", + "description": "Els worktrees de Git et permeten treballar en diverses branques alhora en directoris separats. Cada worktree té la seva pròpia finestra de VS Code amb Roo Code.", + + "notGitRepo": "Aquest espai de treball no és un repositori Git. Els worktrees necessiten un repositori Git per funcionar.", + "multiRootNotSupported": "Els worktrees no són compatibles amb espais de treball de múltiples arrels. Obre una sola carpeta per fer servir worktrees.", + "subfolderNotSupported": "Aquest espai de treball és una subcarpeta d'un repositori Git. Obre l'arrel del repositori per fer servir worktrees.", + "gitRoot": "Arrel de Git", + + "includeFileExists": "S'ha trobat el fitxer .worktreeinclude: es copiaran fitxers als worktrees nous", + "noIncludeFile": "No s'ha trobat cap fitxer .worktreeinclude", + "createFromGitignore": "Crea a partir de .gitignore", + + "primary": "Principal", + "current": "Actual", + "locked": "Bloquejat", + "detachedHead": "HEAD separat", + "noBranch": "Sense branca", + + "openInCurrentWindow": "Obre a la finestra actual", + "openInNewWindow": "Obre en una finestra nova", + "merge": "Fusiona", + "delete": "Suprimeix", + "newWorktree": "Worktree nou", + + "createWorktree": "Crea worktree", + "createWorktreeDescription": "Crea un worktree nou per treballar en una branca diferent dins del seu propi directori.", + "branchName": "Nom de la branca", + "createNewBranch": "Crea una branca nova", + "checkoutExisting": "Fes checkout d'una branca existent", + "baseBranch": "Branca base", + "loadingBranches": "Carregant branques...", + "selectBranch": "Selecciona una branca", + "searchBranch": "Cercar branques...", + "noBranchFound": "No s'ha trobat cap branca", + "localBranches": "Branques locals", + "remoteBranches": "Branques remotes", + "worktreePath": "Camí del worktree", + "pathHint": "El camí on es crearà el worktree", + "noIncludeFileWarning": "No hi ha cap fitxer .worktreeinclude", + "noIncludeFileHint": "Sense un fitxer .worktreeinclude, fitxers com node_modules no es copiaran al worktree nou. Potser hauràs d'executar npm install després de crear-lo.", + "create": "Crea", + "creating": "S'està creant...", + "cancel": "Cancel·la", + + "deleteWorktree": "Suprimeix worktree", + "deleteWorktreeDescription": "Això eliminarà el worktree i tots els seus fitxers.", + "deleteWarning": "Aquesta acció no es pot desfer. S'eliminarà:", + "deleteWarningBranch": "La branca '{{branch}}' i tots els canvis sense confirmar", + "deleteWarningFiles": "Tots els fitxers del directori del worktree", + "forceDelete": "Força la supressió", + "worktreeIsLocked": "el worktree està bloquejat", + "deleting": "S'està suprimint...", + + "mergeBranch": "Fusiona branca", + "mergeDescription": "Fusiona '{{source}}' a '{{target}}'", + "deleteAfterMerge": "Suprimeix el worktree després d'una fusió correcta", + "merging": "S'està fusionant...", + "mergeSuccess": "Fusió correcta", + "mergeConflicts": "Conflictes de fusió", + "conflictsDescription": "Els fitxers següents tenen conflictes que s'han de resoldre:", + "mergeFailed": "Fusió fallida", + "resolveManually": "Ho resoldré manualment", + "askRooResolve": "Demana a Roo que ho resolgui", + "close": "Tanca" +} diff --git a/webview-ui/src/i18n/locales/de/worktrees.json b/webview-ui/src/i18n/locales/de/worktrees.json new file mode 100644 index 00000000000..5a5349a3152 --- /dev/null +++ b/webview-ui/src/i18n/locales/de/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Fertig", + "description": "Git-Worktrees ermöglichen dir, gleichzeitig an mehreren Branches in separaten Verzeichnissen zu arbeiten. Jeder Worktree bekommt sein eigenes VS Code-Fenster mit Roo Code.", + + "notGitRepo": "Dieser Workspace ist kein Git-Repository. Worktrees benötigen ein Git-Repository, um zu funktionieren.", + "multiRootNotSupported": "Worktrees werden in Multi-Root-Workspaces nicht unterstützt. Öffne einen einzelnen Ordner, um Worktrees zu verwenden.", + "subfolderNotSupported": "Dieser Workspace ist ein Unterordner eines Git-Repositories. Öffne das Repository-Root, um Worktrees zu verwenden.", + "gitRoot": "Git-Root", + + "includeFileExists": ".worktreeinclude-Datei gefunden – Dateien werden in neue Worktrees kopiert", + "noIncludeFile": "Keine .worktreeinclude-Datei gefunden", + "createFromGitignore": "Aus .gitignore erstellen", + + "primary": "Primär", + "current": "Aktuell", + "locked": "Gesperrt", + "detachedHead": "Detached HEAD", + "noBranch": "Kein Branch", + + "openInCurrentWindow": "Im aktuellen Fenster öffnen", + "openInNewWindow": "In neuem Fenster öffnen", + "merge": "Mergen", + "delete": "Löschen", + "newWorktree": "Neuer Worktree", + + "createWorktree": "Worktree erstellen", + "createWorktreeDescription": "Erstelle einen neuen Worktree, um in einem eigenen Verzeichnis an einem separaten Branch zu arbeiten.", + "branchName": "Branch-Name", + "createNewBranch": "Neuen Branch erstellen", + "checkoutExisting": "Vorhandenen Branch auschecken", + "baseBranch": "Basis-Branch", + "loadingBranches": "Branches werden geladen...", + "selectBranch": "Branch auswählen", + "searchBranch": "Branches durchsuchen...", + "noBranchFound": "Kein Branch gefunden", + "localBranches": "Lokale Branches", + "remoteBranches": "Remote-Branches", + "worktreePath": "Worktree-Pfad", + "pathHint": "Der Pfad, in dem der Worktree erstellt wird", + "noIncludeFileWarning": "Keine .worktreeinclude-Datei", + "noIncludeFileHint": "Ohne eine .worktreeinclude-Datei werden Dateien wie node_modules nicht in den neuen Worktree kopiert. Möglicherweise musst du nach dem Erstellen npm install ausführen.", + "create": "Erstellen", + "creating": "Wird erstellt...", + "cancel": "Abbrechen", + + "deleteWorktree": "Worktree löschen", + "deleteWorktreeDescription": "Dadurch wird der Worktree und alle seine Dateien entfernt.", + "deleteWarning": "Diese Aktion kann nicht rückgängig gemacht werden. Folgendes wird gelöscht:", + "deleteWarningBranch": "Der Branch '{{branch}}' und alle nicht committeten Änderungen", + "deleteWarningFiles": "Alle Dateien im Worktree-Verzeichnis", + "forceDelete": "Löschen erzwingen", + "worktreeIsLocked": "Worktree ist gesperrt", + "deleting": "Wird gelöscht...", + + "mergeBranch": "Branch mergen", + "mergeDescription": "'{{source}}' in '{{target}}' mergen", + "deleteAfterMerge": "Worktree nach erfolgreichem Merge löschen", + "merging": "Wird gemergt...", + "mergeSuccess": "Merge erfolgreich", + "mergeConflicts": "Merge-Konflikte", + "conflictsDescription": "Die folgenden Dateien haben Konflikte, die gelöst werden müssen:", + "mergeFailed": "Merge fehlgeschlagen", + "resolveManually": "Ich löse das manuell", + "askRooResolve": "Roo zum Lösen fragen", + "close": "Schließen" +} diff --git a/webview-ui/src/i18n/locales/en/worktrees.json b/webview-ui/src/i18n/locales/en/worktrees.json new file mode 100644 index 00000000000..0b68eb21c71 --- /dev/null +++ b/webview-ui/src/i18n/locales/en/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Done", + "description": "Git worktrees allow you to work on multiple branches simultaneously in separate directories. Each worktree gets its own VS Code window with Roo Code.", + + "notGitRepo": "This workspace is not a Git repository. Worktrees require a Git repository to function.", + "multiRootNotSupported": "Worktrees are not supported in multi-root workspaces. Please open a single folder to use worktrees.", + "subfolderNotSupported": "This workspace is a subfolder of a Git repository. Please open the repository root to use worktrees.", + "gitRoot": "Git root", + + "includeFileExists": ".worktreeinclude file found - files will be copied to new worktrees", + "noIncludeFile": "No .worktreeinclude file found", + "createFromGitignore": "Create from .gitignore", + + "primary": "Primary", + "current": "Current", + "locked": "Locked", + "detachedHead": "Detached HEAD", + "noBranch": "No branch", + + "openInCurrentWindow": "Open in current window", + "openInNewWindow": "Open in new window", + "merge": "Merge", + "delete": "Delete", + "newWorktree": "New Worktree", + + "createWorktree": "Create Worktree", + "createWorktreeDescription": "Create a new worktree to work on a separate branch in its own directory.", + "branchName": "Branch name", + "createNewBranch": "Create new branch", + "checkoutExisting": "Checkout existing branch", + "baseBranch": "Base branch", + "loadingBranches": "Loading branches...", + "selectBranch": "Select branch", + "searchBranch": "Search branches...", + "noBranchFound": "No branch found", + "localBranches": "Local branches", + "remoteBranches": "Remote branches", + "worktreePath": "Worktree path", + "pathHint": "The path where the worktree will be created", + "noIncludeFileWarning": "No .worktreeinclude file", + "noIncludeFileHint": "Without a .worktreeinclude file, files like node_modules won't be copied to the new worktree. You may need to run npm install after creating it.", + "create": "Create", + "creating": "Creating...", + "cancel": "Cancel", + + "deleteWorktree": "Delete Worktree", + "deleteWorktreeDescription": "This will remove the worktree and all its files.", + "deleteWarning": "This action cannot be undone. The following will be deleted:", + "deleteWarningBranch": "The branch '{{branch}}' and all uncommitted changes", + "deleteWarningFiles": "All files in the worktree directory", + "forceDelete": "Force delete", + "worktreeIsLocked": "worktree is locked", + "deleting": "Deleting...", + + "mergeBranch": "Merge Branch", + "mergeDescription": "Merge '{{source}}' into '{{target}}'", + "deleteAfterMerge": "Delete worktree after successful merge", + "merging": "Merging...", + "mergeSuccess": "Merge Successful", + "mergeConflicts": "Merge Conflicts", + "conflictsDescription": "The following files have conflicts that need to be resolved:", + "mergeFailed": "Merge Failed", + "resolveManually": "I'll Resolve Manually", + "askRooResolve": "Ask Roo to Resolve", + "close": "Close" +} diff --git a/webview-ui/src/i18n/locales/es/worktrees.json b/webview-ui/src/i18n/locales/es/worktrees.json new file mode 100644 index 00000000000..8db749f797a --- /dev/null +++ b/webview-ui/src/i18n/locales/es/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Listo", + "description": "Los worktrees de Git te permiten trabajar en varias ramas a la vez en directorios separados. Cada worktree tiene su propia ventana de VS Code con Roo Code.", + + "notGitRepo": "Este espacio de trabajo no es un repositorio Git. Los worktrees requieren un repositorio Git para funcionar.", + "multiRootNotSupported": "Los worktrees no son compatibles con espacios de trabajo de múltiples raíces. Abre una sola carpeta para usar worktrees.", + "subfolderNotSupported": "Este espacio de trabajo es una subcarpeta de un repositorio Git. Abre la raíz del repositorio para usar worktrees.", + "gitRoot": "Raíz de Git", + + "includeFileExists": "Se encontró el archivo .worktreeinclude: se copiarán archivos a los worktrees nuevos", + "noIncludeFile": "No se encontró el archivo .worktreeinclude", + "createFromGitignore": "Crear a partir de .gitignore", + + "primary": "Principal", + "current": "Actual", + "locked": "Bloqueado", + "detachedHead": "HEAD separado", + "noBranch": "Sin rama", + + "openInCurrentWindow": "Abrir en la ventana actual", + "openInNewWindow": "Abrir en una ventana nueva", + "merge": "Fusionar", + "delete": "Eliminar", + "newWorktree": "Nuevo Worktree", + + "createWorktree": "Crear Worktree", + "createWorktreeDescription": "Crea un worktree nuevo para trabajar en una rama separada en su propio directorio.", + "branchName": "Nombre de la rama", + "createNewBranch": "Crear rama nueva", + "checkoutExisting": "Hacer checkout de una rama existente", + "baseBranch": "Rama base", + "loadingBranches": "Cargando ramas...", + "selectBranch": "Seleccionar rama", + "searchBranch": "Buscar ramas...", + "noBranchFound": "No se encontró ninguna rama", + "localBranches": "Ramas locales", + "remoteBranches": "Ramas remotas", + "worktreePath": "Ruta del worktree", + "pathHint": "La ruta donde se creará el worktree", + "noIncludeFileWarning": "No hay archivo .worktreeinclude", + "noIncludeFileHint": "Sin un archivo .worktreeinclude, archivos como node_modules no se copiarán al worktree nuevo. Puede que tengas que ejecutar npm install después de crearlo.", + "create": "Crear", + "creating": "Creando...", + "cancel": "Cancelar", + + "deleteWorktree": "Eliminar Worktree", + "deleteWorktreeDescription": "Esto eliminará el worktree y todos sus archivos.", + "deleteWarning": "Esta acción no se puede deshacer. Se eliminará lo siguiente:", + "deleteWarningBranch": "La rama '{{branch}}' y todos los cambios sin confirmar", + "deleteWarningFiles": "Todos los archivos del directorio del worktree", + "forceDelete": "Forzar eliminación", + "worktreeIsLocked": "el worktree está bloqueado", + "deleting": "Eliminando...", + + "mergeBranch": "Fusionar rama", + "mergeDescription": "Fusionar '{{source}}' en '{{target}}'", + "deleteAfterMerge": "Eliminar el worktree después de una fusión exitosa", + "merging": "Fusionando...", + "mergeSuccess": "Fusión exitosa", + "mergeConflicts": "Conflictos de fusión", + "conflictsDescription": "Los siguientes archivos tienen conflictos que deben resolverse:", + "mergeFailed": "Fusión fallida", + "resolveManually": "Lo resolveré manualmente", + "askRooResolve": "Pedirle a Roo que lo resuelva", + "close": "Cerrar" +} diff --git a/webview-ui/src/i18n/locales/fr/worktrees.json b/webview-ui/src/i18n/locales/fr/worktrees.json new file mode 100644 index 00000000000..089fb0ccb0d --- /dev/null +++ b/webview-ui/src/i18n/locales/fr/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Terminé", + "description": "Les worktrees Git te permettent de travailler sur plusieurs branches en même temps dans des répertoires séparés. Chaque worktree a sa propre fenêtre VS Code avec Roo Code.", + + "notGitRepo": "Cet espace de travail n'est pas un dépôt Git. Les worktrees nécessitent un dépôt Git pour fonctionner.", + "multiRootNotSupported": "Les worktrees ne sont pas pris en charge dans les espaces de travail multi-racine. Ouvre un seul dossier pour utiliser les worktrees.", + "subfolderNotSupported": "Cet espace de travail est un sous-dossier d'un dépôt Git. Ouvre la racine du dépôt pour utiliser les worktrees.", + "gitRoot": "Racine Git", + + "includeFileExists": "Fichier .worktreeinclude trouvé — les fichiers seront copiés vers les nouveaux worktrees", + "noIncludeFile": "Aucun fichier .worktreeinclude trouvé", + "createFromGitignore": "Créer depuis .gitignore", + + "primary": "Principal", + "current": "Actuel", + "locked": "Verrouillé", + "detachedHead": "HEAD détachée", + "noBranch": "Aucune branche", + + "openInCurrentWindow": "Ouvrir dans la fenêtre actuelle", + "openInNewWindow": "Ouvrir dans une nouvelle fenêtre", + "merge": "Fusionner", + "delete": "Supprimer", + "newWorktree": "Nouveau Worktree", + + "createWorktree": "Créer un worktree", + "createWorktreeDescription": "Crée un worktree pour travailler sur une autre branche dans son propre répertoire.", + "branchName": "Nom de la branche", + "createNewBranch": "Créer une nouvelle branche", + "checkoutExisting": "Checkout d'une branche existante", + "baseBranch": "Branche de base", + "loadingBranches": "Chargement des branches...", + "selectBranch": "Sélectionner une branche", + "searchBranch": "Rechercher des branches...", + "noBranchFound": "Aucune branche trouvée", + "localBranches": "Branches locales", + "remoteBranches": "Branches distantes", + "worktreePath": "Chemin du worktree", + "pathHint": "Le chemin où le worktree sera créé", + "noIncludeFileWarning": "Aucun fichier .worktreeinclude", + "noIncludeFileHint": "Sans fichier .worktreeinclude, des fichiers comme node_modules ne seront pas copiés dans le nouveau worktree. Tu devras peut-être exécuter npm install après l'avoir créé.", + "create": "Créer", + "creating": "Création...", + "cancel": "Annuler", + + "deleteWorktree": "Supprimer le worktree", + "deleteWorktreeDescription": "Cela supprimera le worktree et tous ses fichiers.", + "deleteWarning": "Cette action est irréversible. Les éléments suivants seront supprimés :", + "deleteWarningBranch": "La branche '{{branch}}' et toutes les modifications non validées", + "deleteWarningFiles": "Tous les fichiers dans le répertoire du worktree", + "forceDelete": "Forcer la suppression", + "worktreeIsLocked": "le worktree est verrouillé", + "deleting": "Suppression...", + + "mergeBranch": "Fusionner la branche", + "mergeDescription": "Fusionner '{{source}}' dans '{{target}}'", + "deleteAfterMerge": "Supprimer le worktree après une fusion réussie", + "merging": "Fusion...", + "mergeSuccess": "Fusion réussie", + "mergeConflicts": "Conflits de fusion", + "conflictsDescription": "Les fichiers suivants ont des conflits à résoudre :", + "mergeFailed": "Échec de la fusion", + "resolveManually": "Je vais le résoudre manuellement", + "askRooResolve": "Demander à Roo de résoudre", + "close": "Fermer" +} diff --git a/webview-ui/src/i18n/locales/hi/worktrees.json b/webview-ui/src/i18n/locales/hi/worktrees.json new file mode 100644 index 00000000000..c58f79854f2 --- /dev/null +++ b/webview-ui/src/i18n/locales/hi/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "हो गया", + "description": "Git worktrees आपको अलग-अलग डायरेक्टरी में एक साथ कई ब्रांच पर काम करने देते हैं। हर worktree को Roo Code के साथ अपनी अलग VS Code विंडो मिलती है।", + + "notGitRepo": "यह workspace Git repository नहीं है। worktrees को काम करने के लिए Git repository चाहिए।", + "multiRootNotSupported": "Multi-root workspaces में worktrees समर्थित नहीं हैं। worktrees उपयोग करने के लिए एक ही फ़ोल्डर खोलें।", + "subfolderNotSupported": "यह workspace Git repository का subfolder है। worktrees उपयोग करने के लिए repository root खोलें।", + "gitRoot": "Git root", + + "includeFileExists": ".worktreeinclude फ़ाइल मिली — नए worktrees में फ़ाइलें कॉपी की जाएँगी", + "noIncludeFile": ".worktreeinclude फ़ाइल नहीं मिली", + "createFromGitignore": ".gitignore से बनाएँ", + + "primary": "मुख्य", + "current": "वर्तमान", + "locked": "लॉक्ड", + "detachedHead": "Detached HEAD", + "noBranch": "कोई ब्रांच नहीं", + + "openInCurrentWindow": "वर्तमान विंडो में खोलें", + "openInNewWindow": "नई विंडो में खोलें", + "merge": "मर्ज", + "delete": "हटाएँ", + "newWorktree": "नया Worktree", + + "createWorktree": "Worktree बनाएँ", + "createWorktreeDescription": "अपने अलग डायरेक्टरी में किसी अलग ब्रांच पर काम करने के लिए नया worktree बनाएँ।", + "branchName": "ब्रांच नाम", + "createNewBranch": "नई ब्रांच बनाएँ", + "checkoutExisting": "मौजूदा ब्रांच checkout करें", + "baseBranch": "बेस ब्रांच", + "loadingBranches": "ब्रांच लोड हो रहे हैं...", + "selectBranch": "ब्रांच चुनें", + "searchBranch": "ब्रांच खोजें...", + "noBranchFound": "कोई ब्रांच नहीं मिली", + "localBranches": "लोकल ब्रांच", + "remoteBranches": "रिमोट ब्रांच", + "worktreePath": "Worktree पाथ", + "pathHint": "वह पाथ जहाँ worktree बनाया जाएगा", + "noIncludeFileWarning": ".worktreeinclude फ़ाइल नहीं है", + "noIncludeFileHint": ".worktreeinclude फ़ाइल के बिना, node_modules जैसी फ़ाइलें नए worktree में कॉपी नहीं होंगी। इसे बनाने के बाद आपको npm install चलाना पड़ सकता है।", + "create": "बनाएँ", + "creating": "बनाया जा रहा है...", + "cancel": "रद्द करें", + + "deleteWorktree": "Worktree हटाएँ", + "deleteWorktreeDescription": "यह worktree और उसकी सभी फ़ाइलें हटा देगा।", + "deleteWarning": "यह कार्रवाई वापस नहीं ली जा सकती। निम्नलिखित हटाया जाएगा:", + "deleteWarningBranch": "ब्रांच '{{branch}}' और सभी uncommitted बदलाव", + "deleteWarningFiles": "worktree डायरेक्टरी की सभी फ़ाइलें", + "forceDelete": "जबरन हटाएँ", + "worktreeIsLocked": "worktree लॉक्ड है", + "deleting": "हटाया जा रहा है...", + + "mergeBranch": "ब्रांच मर्ज करें", + "mergeDescription": "'{{source}}' को '{{target}}' में मर्ज करें", + "deleteAfterMerge": "सफल मर्ज के बाद worktree हटाएँ", + "merging": "मर्ज किया जा रहा है...", + "mergeSuccess": "मर्ज सफल", + "mergeConflicts": "मर्ज कॉन्फ्लिक्ट", + "conflictsDescription": "निम्नलिखित फ़ाइलों में कॉन्फ्लिक्ट हैं जिन्हें हल करना होगा:", + "mergeFailed": "मर्ज विफल", + "resolveManually": "मैं मैन्युअली हल करूँगा", + "askRooResolve": "Roo से हल करवाएँ", + "close": "बंद करें" +} diff --git a/webview-ui/src/i18n/locales/id/worktrees.json b/webview-ui/src/i18n/locales/id/worktrees.json new file mode 100644 index 00000000000..50fba4e8ed3 --- /dev/null +++ b/webview-ui/src/i18n/locales/id/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Selesai", + "description": "Git worktrees memungkinkan kamu bekerja di beberapa branch sekaligus dalam direktori terpisah. Setiap worktree mendapatkan jendela VS Code sendiri dengan Roo Code.", + + "notGitRepo": "Workspace ini bukan repository Git. Worktrees memerlukan repository Git untuk berfungsi.", + "multiRootNotSupported": "Worktrees tidak didukung di workspace multi-root. Buka satu folder untuk menggunakan worktrees.", + "subfolderNotSupported": "Workspace ini adalah subfolder dari repository Git. Buka root repository untuk menggunakan worktrees.", + "gitRoot": "Root Git", + + "includeFileExists": "File .worktreeinclude ditemukan — file akan disalin ke worktrees baru", + "noIncludeFile": "File .worktreeinclude tidak ditemukan", + "createFromGitignore": "Buat dari .gitignore", + + "primary": "Utama", + "current": "Saat ini", + "locked": "Terkunci", + "detachedHead": "Detached HEAD", + "noBranch": "Tidak ada branch", + + "openInCurrentWindow": "Buka di jendela saat ini", + "openInNewWindow": "Buka di jendela baru", + "merge": "Merge", + "delete": "Hapus", + "newWorktree": "Worktree Baru", + + "createWorktree": "Buat Worktree", + "createWorktreeDescription": "Buat worktree baru untuk bekerja pada branch terpisah di direktori sendiri.", + "branchName": "Nama branch", + "createNewBranch": "Buat branch baru", + "checkoutExisting": "Checkout branch yang sudah ada", + "baseBranch": "Branch dasar", + "loadingBranches": "Memuat branch...", + "selectBranch": "Pilih branch", + "searchBranch": "Cari branch...", + "noBranchFound": "Branch tidak ditemukan", + "localBranches": "Branch lokal", + "remoteBranches": "Branch remote", + "worktreePath": "Path worktree", + "pathHint": "Path tempat worktree akan dibuat", + "noIncludeFileWarning": "Tidak ada file .worktreeinclude", + "noIncludeFileHint": "Tanpa file .worktreeinclude, file seperti node_modules tidak akan disalin ke worktree baru. Kamu mungkin perlu menjalankan npm install setelah membuatnya.", + "create": "Buat", + "creating": "Membuat...", + "cancel": "Batal", + + "deleteWorktree": "Hapus Worktree", + "deleteWorktreeDescription": "Ini akan menghapus worktree dan semua filenya.", + "deleteWarning": "Tindakan ini tidak dapat dibatalkan. Yang berikut akan dihapus:", + "deleteWarningBranch": "Branch '{{branch}}' dan semua perubahan yang belum di-commit", + "deleteWarningFiles": "Semua file di direktori worktree", + "forceDelete": "Paksa hapus", + "worktreeIsLocked": "worktree terkunci", + "deleting": "Menghapus...", + + "mergeBranch": "Merge Branch", + "mergeDescription": "Merge '{{source}}' ke '{{target}}'", + "deleteAfterMerge": "Hapus worktree setelah merge berhasil", + "merging": "Menggabungkan...", + "mergeSuccess": "Merge Berhasil", + "mergeConflicts": "Konflik Merge", + "conflictsDescription": "File berikut memiliki konflik yang perlu diselesaikan:", + "mergeFailed": "Merge Gagal", + "resolveManually": "Aku akan menyelesaikannya manual", + "askRooResolve": "Minta Roo untuk menyelesaikan", + "close": "Tutup" +} diff --git a/webview-ui/src/i18n/locales/it/worktrees.json b/webview-ui/src/i18n/locales/it/worktrees.json new file mode 100644 index 00000000000..4408757306a --- /dev/null +++ b/webview-ui/src/i18n/locales/it/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Fatto", + "description": "I worktree di Git ti permettono di lavorare su più branch contemporaneamente in directory separate. Ogni worktree ha la propria finestra di VS Code con Roo Code.", + + "notGitRepo": "Questo workspace non è un repository Git. I worktree richiedono un repository Git per funzionare.", + "multiRootNotSupported": "I worktree non sono supportati nei workspace multi-root. Apri una singola cartella per usare i worktree.", + "subfolderNotSupported": "Questo workspace è una sottocartella di un repository Git. Apri la root del repository per usare i worktree.", + "gitRoot": "Root di Git", + + "includeFileExists": "File .worktreeinclude trovato — i file verranno copiati nei nuovi worktree", + "noIncludeFile": "Nessun file .worktreeinclude trovato", + "createFromGitignore": "Crea da .gitignore", + + "primary": "Primario", + "current": "Attuale", + "locked": "Bloccato", + "detachedHead": "HEAD staccata", + "noBranch": "Nessun branch", + + "openInCurrentWindow": "Apri nella finestra corrente", + "openInNewWindow": "Apri in una nuova finestra", + "merge": "Unisci", + "delete": "Elimina", + "newWorktree": "Nuovo Worktree", + + "createWorktree": "Crea Worktree", + "createWorktreeDescription": "Crea un nuovo worktree per lavorare su un branch separato nella sua directory.", + "branchName": "Nome del branch", + "createNewBranch": "Crea nuovo branch", + "checkoutExisting": "Fai checkout di un branch esistente", + "baseBranch": "Branch di base", + "loadingBranches": "Caricamento branch...", + "selectBranch": "Seleziona branch", + "searchBranch": "Cerca branch...", + "noBranchFound": "Nessun branch trovato", + "localBranches": "Branch locali", + "remoteBranches": "Branch remoti", + "worktreePath": "Percorso del worktree", + "pathHint": "Il percorso in cui verrà creato il worktree", + "noIncludeFileWarning": "Nessun file .worktreeinclude", + "noIncludeFileHint": "Senza un file .worktreeinclude, file come node_modules non verranno copiati nel nuovo worktree. Potresti dover eseguire npm install dopo averlo creato.", + "create": "Crea", + "creating": "Creazione...", + "cancel": "Annulla", + + "deleteWorktree": "Elimina Worktree", + "deleteWorktreeDescription": "Questo rimuoverà il worktree e tutti i suoi file.", + "deleteWarning": "Questa azione non può essere annullata. Verrà eliminato quanto segue:", + "deleteWarningBranch": "Il branch '{{branch}}' e tutte le modifiche non committate", + "deleteWarningFiles": "Tutti i file nella directory del worktree", + "forceDelete": "Forza eliminazione", + "worktreeIsLocked": "il worktree è bloccato", + "deleting": "Eliminazione...", + + "mergeBranch": "Unisci Branch", + "mergeDescription": "Unisci '{{source}}' in '{{target}}'", + "deleteAfterMerge": "Elimina il worktree dopo un merge riuscito", + "merging": "Merge in corso...", + "mergeSuccess": "Merge riuscito", + "mergeConflicts": "Conflitti di merge", + "conflictsDescription": "I seguenti file hanno conflitti che devono essere risolti:", + "mergeFailed": "Merge fallito", + "resolveManually": "Lo risolverò manualmente", + "askRooResolve": "Chiedi a Roo di risolvere", + "close": "Chiudi" +} diff --git a/webview-ui/src/i18n/locales/ja/worktrees.json b/webview-ui/src/i18n/locales/ja/worktrees.json new file mode 100644 index 00000000000..b7c459a4988 --- /dev/null +++ b/webview-ui/src/i18n/locales/ja/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "完了", + "description": "Git worktrees を使うと、別々のディレクトリで複数のブランチを同時に作業できます。各 worktree には Roo Code 付きの VS Code ウィンドウが割り当てられます。", + + "notGitRepo": "このワークスペースは Git リポジトリではありません。worktrees を使うには Git リポジトリが必要です。", + "multiRootNotSupported": "マルチルートのワークスペースでは worktrees はサポートされていません。worktrees を使うには単一のフォルダーを開いてください。", + "subfolderNotSupported": "このワークスペースは Git リポジトリのサブフォルダーです。worktrees を使うにはリポジトリのルートを開いてください。", + "gitRoot": "Git ルート", + + "includeFileExists": ".worktreeinclude ファイルが見つかりました — ファイルは新しい worktrees にコピーされます", + "noIncludeFile": ".worktreeinclude ファイルが見つかりませんでした", + "createFromGitignore": ".gitignore から作成", + + "primary": "プライマリ", + "current": "現在", + "locked": "ロック中", + "detachedHead": "Detached HEAD", + "noBranch": "ブランチなし", + + "openInCurrentWindow": "現在のウィンドウで開く", + "openInNewWindow": "新しいウィンドウで開く", + "merge": "マージ", + "delete": "削除", + "newWorktree": "新しい Worktree", + + "createWorktree": "Worktree を作成", + "createWorktreeDescription": "別のブランチで作業するために、専用ディレクトリに新しい worktree を作成します。", + "branchName": "ブランチ名", + "createNewBranch": "新しいブランチを作成", + "checkoutExisting": "既存のブランチをチェックアウト", + "baseBranch": "ベースブランチ", + "loadingBranches": "ブランチを読み込み中...", + "selectBranch": "ブランチを選択", + "searchBranch": "ブランチを検索...", + "noBranchFound": "ブランチが見つかりません", + "localBranches": "ローカルブランチ", + "remoteBranches": "リモートブランチ", + "worktreePath": "Worktree のパス", + "pathHint": "worktree を作成するパス", + "noIncludeFileWarning": ".worktreeinclude ファイルなし", + "noIncludeFileHint": ".worktreeinclude ファイルがない場合、node_modules などのファイルは新しい worktree にコピーされません。作成後に npm install を実行する必要があるかもしれません。", + "create": "作成", + "creating": "作成中...", + "cancel": "キャンセル", + + "deleteWorktree": "Worktree を削除", + "deleteWorktreeDescription": "これにより worktree とそのファイルがすべて削除されます。", + "deleteWarning": "この操作は元に戻せません。次のものが削除されます:", + "deleteWarningBranch": "ブランチ '{{branch}}' と未コミットの変更すべて", + "deleteWarningFiles": "worktree ディレクトリ内のすべてのファイル", + "forceDelete": "強制削除", + "worktreeIsLocked": "worktree がロックされています", + "deleting": "削除中...", + + "mergeBranch": "ブランチをマージ", + "mergeDescription": "'{{source}}' を '{{target}}' にマージ", + "deleteAfterMerge": "マージが成功したら worktree を削除", + "merging": "マージ中...", + "mergeSuccess": "マージに成功しました", + "mergeConflicts": "マージの競合", + "conflictsDescription": "次のファイルに解決が必要な競合があります:", + "mergeFailed": "マージに失敗しました", + "resolveManually": "手動で解決する", + "askRooResolve": "Roo に解決を依頼", + "close": "閉じる" +} diff --git a/webview-ui/src/i18n/locales/ko/worktrees.json b/webview-ui/src/i18n/locales/ko/worktrees.json new file mode 100644 index 00000000000..3993bd1e45d --- /dev/null +++ b/webview-ui/src/i18n/locales/ko/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "완료", + "description": "Git worktrees를 사용하면 별도의 디렉터리에서 여러 브랜치를 동시에 작업할 수 있습니다. 각 worktree는 Roo Code가 포함된 자체 VS Code 창을 가집니다.", + + "notGitRepo": "이 워크스페이스는 Git 저장소가 아닙니다. worktrees를 사용하려면 Git 저장소가 필요합니다.", + "multiRootNotSupported": "멀티 루트 워크스페이스에서는 worktrees가 지원되지 않습니다. worktrees를 사용하려면 단일 폴더를 여세요.", + "subfolderNotSupported": "이 워크스페이스는 Git 저장소의 하위 폴더입니다. worktrees를 사용하려면 저장소 루트를 여세요.", + "gitRoot": "Git 루트", + + "includeFileExists": ".worktreeinclude 파일을 찾았습니다 — 파일이 새 worktrees로 복사됩니다", + "noIncludeFile": ".worktreeinclude 파일을 찾지 못했습니다", + "createFromGitignore": ".gitignore에서 만들기", + + "primary": "기본", + "current": "현재", + "locked": "잠김", + "detachedHead": "Detached HEAD", + "noBranch": "브랜치 없음", + + "openInCurrentWindow": "현재 창에서 열기", + "openInNewWindow": "새 창에서 열기", + "merge": "병합", + "delete": "삭제", + "newWorktree": "새 Worktree", + + "createWorktree": "Worktree 만들기", + "createWorktreeDescription": "별도의 브랜치에서 작업하기 위해 전용 디렉터리에 새 worktree를 만듭니다.", + "branchName": "브랜치 이름", + "createNewBranch": "새 브랜치 만들기", + "checkoutExisting": "기존 브랜치 체크아웃", + "baseBranch": "기준 브랜치", + "loadingBranches": "브랜치 로딩 중...", + "selectBranch": "브랜치 선택", + "searchBranch": "브랜치 검색...", + "noBranchFound": "브랜치를 찾을 수 없습니다", + "localBranches": "로컬 브랜치", + "remoteBranches": "원격 브랜치", + "worktreePath": "Worktree 경로", + "pathHint": "worktree가 생성될 경로", + "noIncludeFileWarning": ".worktreeinclude 파일 없음", + "noIncludeFileHint": ".worktreeinclude 파일이 없으면 node_modules 같은 파일이 새 worktree로 복사되지 않습니다. 생성 후 npm install을 실행해야 할 수도 있습니다.", + "create": "만들기", + "creating": "만드는 중...", + "cancel": "취소", + + "deleteWorktree": "Worktree 삭제", + "deleteWorktreeDescription": "이 작업은 worktree와 그 안의 모든 파일을 제거합니다.", + "deleteWarning": "이 작업은 되돌릴 수 없습니다. 다음이 삭제됩니다:", + "deleteWarningBranch": "브랜치 '{{branch}}' 및 커밋되지 않은 모든 변경 사항", + "deleteWarningFiles": "worktree 디렉터리의 모든 파일", + "forceDelete": "강제 삭제", + "worktreeIsLocked": "worktree가 잠겨 있습니다", + "deleting": "삭제 중...", + + "mergeBranch": "브랜치 병합", + "mergeDescription": "'{{source}}'을(를) '{{target}}'에 병합", + "deleteAfterMerge": "병합 성공 후 worktree 삭제", + "merging": "병합 중...", + "mergeSuccess": "병합 성공", + "mergeConflicts": "병합 충돌", + "conflictsDescription": "다음 파일에 해결이 필요한 충돌이 있습니다:", + "mergeFailed": "병합 실패", + "resolveManually": "수동으로 해결할게요", + "askRooResolve": "Roo에게 해결 요청", + "close": "닫기" +} diff --git a/webview-ui/src/i18n/locales/nl/worktrees.json b/webview-ui/src/i18n/locales/nl/worktrees.json new file mode 100644 index 00000000000..092225e8d31 --- /dev/null +++ b/webview-ui/src/i18n/locales/nl/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Klaar", + "description": "Met Git worktrees kun je tegelijkertijd aan meerdere branches werken in aparte mappen. Elke worktree krijgt zijn eigen VS Code-venster met Roo Code.", + + "notGitRepo": "Deze workspace is geen Git-repository. Worktrees vereisen een Git-repository om te werken.", + "multiRootNotSupported": "Worktrees worden niet ondersteund in multi-root workspaces. Open één map om worktrees te gebruiken.", + "subfolderNotSupported": "Deze workspace is een submap van een Git-repository. Open de repository-root om worktrees te gebruiken.", + "gitRoot": "Git-root", + + "includeFileExists": ".worktreeinclude-bestand gevonden — bestanden worden naar nieuwe worktrees gekopieerd", + "noIncludeFile": "Geen .worktreeinclude-bestand gevonden", + "createFromGitignore": "Aanmaken vanuit .gitignore", + + "primary": "Primair", + "current": "Huidig", + "locked": "Vergrendeld", + "detachedHead": "Detached HEAD", + "noBranch": "Geen branch", + + "openInCurrentWindow": "Openen in huidig venster", + "openInNewWindow": "Openen in nieuw venster", + "merge": "Samenvoegen", + "delete": "Verwijderen", + "newWorktree": "Nieuwe Worktree", + + "createWorktree": "Worktree aanmaken", + "createWorktreeDescription": "Maak een nieuwe worktree aan om in een aparte branch in zijn eigen map te werken.", + "branchName": "Branchnaam", + "createNewBranch": "Nieuwe branch aanmaken", + "checkoutExisting": "Bestaande branch uitchecken", + "baseBranch": "Basisbranch", + "loadingBranches": "Branches laden...", + "selectBranch": "Branch selecteren", + "searchBranch": "Branches zoeken...", + "noBranchFound": "Geen branch gevonden", + "localBranches": "Lokale branches", + "remoteBranches": "Remote-branches", + "worktreePath": "Worktree-pad", + "pathHint": "Het pad waar de worktree wordt aangemaakt", + "noIncludeFileWarning": "Geen .worktreeinclude-bestand", + "noIncludeFileHint": "Zonder een .worktreeinclude-bestand worden bestanden zoals node_modules niet naar de nieuwe worktree gekopieerd. Mogelijk moet je na het aanmaken npm install uitvoeren.", + "create": "Aanmaken", + "creating": "Bezig met aanmaken...", + "cancel": "Annuleren", + + "deleteWorktree": "Worktree verwijderen", + "deleteWorktreeDescription": "Hiermee wordt de worktree en alle bestanden verwijderd.", + "deleteWarning": "Deze actie kan niet ongedaan worden gemaakt. Het volgende wordt verwijderd:", + "deleteWarningBranch": "De branch '{{branch}}' en alle niet-gecommitte wijzigingen", + "deleteWarningFiles": "Alle bestanden in de worktree-map", + "forceDelete": "Geforceerd verwijderen", + "worktreeIsLocked": "worktree is vergrendeld", + "deleting": "Bezig met verwijderen...", + + "mergeBranch": "Branch samenvoegen", + "mergeDescription": "'{{source}}' samenvoegen in '{{target}}'", + "deleteAfterMerge": "Worktree verwijderen na succesvolle merge", + "merging": "Bezig met samenvoegen...", + "mergeSuccess": "Samenvoegen geslaagd", + "mergeConflicts": "Samenvoegconflicten", + "conflictsDescription": "De volgende bestanden hebben conflicten die moeten worden opgelost:", + "mergeFailed": "Samenvoegen mislukt", + "resolveManually": "Ik los het handmatig op", + "askRooResolve": "Roo vragen om op te lossen", + "close": "Sluiten" +} diff --git a/webview-ui/src/i18n/locales/pl/worktrees.json b/webview-ui/src/i18n/locales/pl/worktrees.json new file mode 100644 index 00000000000..6b335cff829 --- /dev/null +++ b/webview-ui/src/i18n/locales/pl/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Gotowe", + "description": "Git worktrees pozwalają pracować jednocześnie na wielu branchach w oddzielnych katalogach. Każdy worktree ma własne okno VS Code z Roo Code.", + + "notGitRepo": "Ten workspace nie jest repozytorium Git. Worktrees wymagają repozytorium Git, aby działać.", + "multiRootNotSupported": "Worktrees nie są obsługiwane w workspace'ach multi-root. Otwórz pojedynczy folder, aby używać worktrees.", + "subfolderNotSupported": "Ten workspace jest podfolderem repozytorium Git. Otwórz root repozytorium, aby używać worktrees.", + "gitRoot": "Root Git", + + "includeFileExists": "Znaleziono plik .worktreeinclude — pliki zostaną skopiowane do nowych worktrees", + "noIncludeFile": "Nie znaleziono pliku .worktreeinclude", + "createFromGitignore": "Utwórz z .gitignore", + + "primary": "Główny", + "current": "Bieżący", + "locked": "Zablokowany", + "detachedHead": "Detached HEAD", + "noBranch": "Brak brancha", + + "openInCurrentWindow": "Otwórz w bieżącym oknie", + "openInNewWindow": "Otwórz w nowym oknie", + "merge": "Scal", + "delete": "Usuń", + "newWorktree": "Nowy Worktree", + + "createWorktree": "Utwórz Worktree", + "createWorktreeDescription": "Utwórz nowy worktree, aby pracować na oddzielnym branchu w osobnym katalogu.", + "branchName": "Nazwa brancha", + "createNewBranch": "Utwórz nowy branch", + "checkoutExisting": "Checkout istniejącego brancha", + "baseBranch": "Branch bazowy", + "loadingBranches": "Ładowanie branchy...", + "selectBranch": "Wybierz branch", + "searchBranch": "Szukaj branchy...", + "noBranchFound": "Nie znaleziono brancha", + "localBranches": "Branche lokalne", + "remoteBranches": "Branche zdalne", + "worktreePath": "Ścieżka worktree", + "pathHint": "Ścieżka, w której zostanie utworzony worktree", + "noIncludeFileWarning": "Brak pliku .worktreeinclude", + "noIncludeFileHint": "Bez pliku .worktreeinclude pliki takie jak node_modules nie zostaną skopiowane do nowego worktree. Po utworzeniu może być konieczne uruchomienie npm install.", + "create": "Utwórz", + "creating": "Tworzenie...", + "cancel": "Anuluj", + + "deleteWorktree": "Usuń Worktree", + "deleteWorktreeDescription": "To usunie worktree oraz wszystkie jego pliki.", + "deleteWarning": "Tej akcji nie można cofnąć. Zostanie usunięte:", + "deleteWarningBranch": "Branch '{{branch}}' i wszystkie niezatwierdzone zmiany", + "deleteWarningFiles": "Wszystkie pliki w katalogu worktree", + "forceDelete": "Wymuś usunięcie", + "worktreeIsLocked": "worktree jest zablokowany", + "deleting": "Usuwanie...", + + "mergeBranch": "Scal Branch", + "mergeDescription": "Scal '{{source}}' do '{{target}}'", + "deleteAfterMerge": "Usuń worktree po udanym scaleniu", + "merging": "Scalanie...", + "mergeSuccess": "Scalanie zakończone sukcesem", + "mergeConflicts": "Konflikty scalania", + "conflictsDescription": "Następujące pliki mają konflikty, które trzeba rozwiązać:", + "mergeFailed": "Scalanie nie powiodło się", + "resolveManually": "Rozwiążę ręcznie", + "askRooResolve": "Poproś Roo o rozwiązanie", + "close": "Zamknij" +} diff --git a/webview-ui/src/i18n/locales/pt-BR/worktrees.json b/webview-ui/src/i18n/locales/pt-BR/worktrees.json new file mode 100644 index 00000000000..3f8bfe685c7 --- /dev/null +++ b/webview-ui/src/i18n/locales/pt-BR/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Concluído", + "description": "Os worktrees do Git permitem que você trabalhe em vários branches ao mesmo tempo em diretórios separados. Cada worktree tem sua própria janela do VS Code com o Roo Code.", + + "notGitRepo": "Este workspace não é um repositório Git. Worktrees exigem um repositório Git para funcionar.", + "multiRootNotSupported": "Worktrees não são compatíveis com workspaces multi-root. Abra uma única pasta para usar worktrees.", + "subfolderNotSupported": "Este workspace é uma subpasta de um repositório Git. Abra a raiz do repositório para usar worktrees.", + "gitRoot": "Raiz do Git", + + "includeFileExists": "Arquivo .worktreeinclude encontrado — os arquivos serão copiados para os novos worktrees", + "noIncludeFile": "Nenhum arquivo .worktreeinclude encontrado", + "createFromGitignore": "Criar a partir do .gitignore", + + "primary": "Principal", + "current": "Atual", + "locked": "Bloqueado", + "detachedHead": "Detached HEAD", + "noBranch": "Sem branch", + + "openInCurrentWindow": "Abrir na janela atual", + "openInNewWindow": "Abrir em uma nova janela", + "merge": "Mesclar", + "delete": "Excluir", + "newWorktree": "Novo Worktree", + + "createWorktree": "Criar Worktree", + "createWorktreeDescription": "Crie um novo worktree para trabalhar em um branch separado no seu próprio diretório.", + "branchName": "Nome do branch", + "createNewBranch": "Criar novo branch", + "checkoutExisting": "Fazer checkout de um branch existente", + "baseBranch": "Branch base", + "loadingBranches": "Carregando branches...", + "selectBranch": "Selecionar branch", + "searchBranch": "Pesquisar branches...", + "noBranchFound": "Nenhum branch encontrado", + "localBranches": "Branches locais", + "remoteBranches": "Branches remotos", + "worktreePath": "Caminho do worktree", + "pathHint": "O caminho onde o worktree será criado", + "noIncludeFileWarning": "Sem arquivo .worktreeinclude", + "noIncludeFileHint": "Sem um arquivo .worktreeinclude, arquivos como node_modules não serão copiados para o novo worktree. Você pode precisar executar npm install depois de criá-lo.", + "create": "Criar", + "creating": "Criando...", + "cancel": "Cancelar", + + "deleteWorktree": "Excluir Worktree", + "deleteWorktreeDescription": "Isso removerá o worktree e todos os seus arquivos.", + "deleteWarning": "Esta ação não pode ser desfeita. O seguinte será excluído:", + "deleteWarningBranch": "O branch '{{branch}}' e todas as alterações não commitadas", + "deleteWarningFiles": "Todos os arquivos no diretório do worktree", + "forceDelete": "Forçar exclusão", + "worktreeIsLocked": "worktree está bloqueado", + "deleting": "Excluindo...", + + "mergeBranch": "Mesclar Branch", + "mergeDescription": "Mesclar '{{source}}' em '{{target}}'", + "deleteAfterMerge": "Excluir worktree após mesclagem bem-sucedida", + "merging": "Mesclando...", + "mergeSuccess": "Mesclagem bem-sucedida", + "mergeConflicts": "Conflitos de mesclagem", + "conflictsDescription": "Os seguintes arquivos têm conflitos que precisam ser resolvidos:", + "mergeFailed": "Falha na mesclagem", + "resolveManually": "Vou resolver manualmente", + "askRooResolve": "Pedir ao Roo para resolver", + "close": "Fechar" +} diff --git a/webview-ui/src/i18n/locales/ru/worktrees.json b/webview-ui/src/i18n/locales/ru/worktrees.json new file mode 100644 index 00000000000..fd191c2a0d4 --- /dev/null +++ b/webview-ui/src/i18n/locales/ru/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Готово", + "description": "Git worktrees позволяют одновременно работать с несколькими ветками в отдельных каталогах. У каждого worktree есть своё окно VS Code с Roo Code.", + + "notGitRepo": "Этот workspace не является Git-репозиторием. Для работы worktrees нужен Git-репозиторий.", + "multiRootNotSupported": "Worktrees не поддерживаются в multi-root workspace. Открой один каталог, чтобы использовать worktrees.", + "subfolderNotSupported": "Этот workspace является подпапкой Git-репозитория. Открой корень репозитория, чтобы использовать worktrees.", + "gitRoot": "Корень Git", + + "includeFileExists": "Найден файл .worktreeinclude — файлы будут скопированы в новые worktrees", + "noIncludeFile": "Файл .worktreeinclude не найден", + "createFromGitignore": "Создать из .gitignore", + + "primary": "Основной", + "current": "Текущий", + "locked": "Заблокировано", + "detachedHead": "Detached HEAD", + "noBranch": "Нет ветки", + + "openInCurrentWindow": "Открыть в текущем окне", + "openInNewWindow": "Открыть в новом окне", + "merge": "Слить", + "delete": "Удалить", + "newWorktree": "Новый Worktree", + + "createWorktree": "Создать Worktree", + "createWorktreeDescription": "Создай новый worktree, чтобы работать с отдельной веткой в своём каталоге.", + "branchName": "Имя ветки", + "createNewBranch": "Создать новую ветку", + "checkoutExisting": "Переключиться на существующую ветку", + "baseBranch": "Базовая ветка", + "loadingBranches": "Загрузка веток...", + "selectBranch": "Выбрать ветку", + "searchBranch": "Поиск веток...", + "noBranchFound": "Ветка не найдена", + "localBranches": "Локальные ветки", + "remoteBranches": "Удалённые ветки", + "worktreePath": "Путь worktree", + "pathHint": "Путь, где будет создан worktree", + "noIncludeFileWarning": "Нет файла .worktreeinclude", + "noIncludeFileHint": "Без файла .worktreeinclude файлы вроде node_modules не будут скопированы в новый worktree. Возможно, после создания нужно будет выполнить npm install.", + "create": "Создать", + "creating": "Создание...", + "cancel": "Отмена", + + "deleteWorktree": "Удалить Worktree", + "deleteWorktreeDescription": "Это удалит worktree и все его файлы.", + "deleteWarning": "Это действие нельзя отменить. Будет удалено:", + "deleteWarningBranch": "Ветка '{{branch}}' и все незакоммиченные изменения", + "deleteWarningFiles": "Все файлы в каталоге worktree", + "forceDelete": "Удалить принудительно", + "worktreeIsLocked": "worktree заблокирован", + "deleting": "Удаление...", + + "mergeBranch": "Слить ветку", + "mergeDescription": "Слить '{{source}}' в '{{target}}'", + "deleteAfterMerge": "Удалить worktree после успешного слияния", + "merging": "Слияние...", + "mergeSuccess": "Слияние выполнено", + "mergeConflicts": "Конфликты слияния", + "conflictsDescription": "Следующие файлы имеют конфликты, которые нужно разрешить:", + "mergeFailed": "Слияние не удалось", + "resolveManually": "Разрешу вручную", + "askRooResolve": "Попросить Roo разрешить", + "close": "Закрыть" +} diff --git a/webview-ui/src/i18n/locales/tr/worktrees.json b/webview-ui/src/i18n/locales/tr/worktrees.json new file mode 100644 index 00000000000..e7d76fce0f2 --- /dev/null +++ b/webview-ui/src/i18n/locales/tr/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Tamam", + "description": "Git worktrees, ayrı dizinlerde aynı anda birden fazla branch üzerinde çalışmana olanak tanır. Her worktree, Roo Code ile kendi VS Code penceresine sahip olur.", + + "notGitRepo": "Bu çalışma alanı bir Git deposu değil. Worktrees'in çalışması için bir Git deposu gerekir.", + "multiRootNotSupported": "Worktrees, multi-root çalışma alanlarında desteklenmez. Worktrees kullanmak için tek bir klasör aç.", + "subfolderNotSupported": "Bu çalışma alanı bir Git deposunun alt klasörü. Worktrees kullanmak için deponun kökünü aç.", + "gitRoot": "Git kökü", + + "includeFileExists": ".worktreeinclude dosyası bulundu — dosyalar yeni worktrees'e kopyalanacak", + "noIncludeFile": ".worktreeinclude dosyası bulunamadı", + "createFromGitignore": ".gitignore'dan oluştur", + + "primary": "Birincil", + "current": "Mevcut", + "locked": "Kilitli", + "detachedHead": "Detached HEAD", + "noBranch": "Branch yok", + + "openInCurrentWindow": "Mevcut pencerede aç", + "openInNewWindow": "Yeni pencerede aç", + "merge": "Birleştir", + "delete": "Sil", + "newWorktree": "Yeni Worktree", + + "createWorktree": "Worktree oluştur", + "createWorktreeDescription": "Kendi dizininde ayrı bir branch üzerinde çalışmak için yeni bir worktree oluştur.", + "branchName": "Branch adı", + "createNewBranch": "Yeni branch oluştur", + "checkoutExisting": "Mevcut branch'i checkout et", + "baseBranch": "Temel branch", + "loadingBranches": "Branch'ler yükleniyor...", + "selectBranch": "Branch seç", + "searchBranch": "Branch ara...", + "noBranchFound": "Branch bulunamadı", + "localBranches": "Yerel branch'ler", + "remoteBranches": "Uzak branch'ler", + "worktreePath": "Worktree yolu", + "pathHint": "Worktree'nin oluşturulacağı yol", + "noIncludeFileWarning": ".worktreeinclude dosyası yok", + "noIncludeFileHint": ".worktreeinclude dosyası olmadan node_modules gibi dosyalar yeni worktree'ye kopyalanmaz. Oluşturduktan sonra npm install çalıştırman gerekebilir.", + "create": "Oluştur", + "creating": "Oluşturuluyor...", + "cancel": "İptal", + + "deleteWorktree": "Worktree'yi sil", + "deleteWorktreeDescription": "Bu işlem worktree'yi ve tüm dosyalarını kaldırır.", + "deleteWarning": "Bu işlem geri alınamaz. Şunlar silinecek:", + "deleteWarningBranch": "'{{branch}}' branch'i ve commit edilmemiş tüm değişiklikler", + "deleteWarningFiles": "worktree dizinindeki tüm dosyalar", + "forceDelete": "Silmeye zorla", + "worktreeIsLocked": "worktree kilitli", + "deleting": "Siliniyor...", + + "mergeBranch": "Branch birleştir", + "mergeDescription": "'{{source}}' branch'ini '{{target}}' içine birleştir", + "deleteAfterMerge": "Birleştirme başarılı olursa worktree'yi sil", + "merging": "Birleştiriliyor...", + "mergeSuccess": "Birleştirme başarılı", + "mergeConflicts": "Birleştirme çakışmaları", + "conflictsDescription": "Aşağıdaki dosyalarda çözülmesi gereken çakışmalar var:", + "mergeFailed": "Birleştirme başarısız", + "resolveManually": "Kendim çözeceğim", + "askRooResolve": "Roo'dan çözmesini iste", + "close": "Kapat" +} diff --git a/webview-ui/src/i18n/locales/vi/worktrees.json b/webview-ui/src/i18n/locales/vi/worktrees.json new file mode 100644 index 00000000000..d542ecfe0f0 --- /dev/null +++ b/webview-ui/src/i18n/locales/vi/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "Xong", + "description": "Git worktrees cho phép bạn làm việc đồng thời trên nhiều nhánh trong các thư mục riêng. Mỗi worktree có một cửa sổ VS Code riêng với Roo Code.", + + "notGitRepo": "Workspace này không phải là kho Git. Worktrees cần một kho Git để hoạt động.", + "multiRootNotSupported": "Worktrees không được hỗ trợ trong workspace đa gốc. Hãy mở một thư mục đơn để dùng worktrees.", + "subfolderNotSupported": "Workspace này là một thư mục con của kho Git. Hãy mở thư mục gốc của kho để dùng worktrees.", + "gitRoot": "Thư mục gốc Git", + + "includeFileExists": "Đã tìm thấy tệp .worktreeinclude — các tệp sẽ được sao chép sang worktrees mới", + "noIncludeFile": "Không tìm thấy tệp .worktreeinclude", + "createFromGitignore": "Tạo từ .gitignore", + + "primary": "Chính", + "current": "Hiện tại", + "locked": "Đã khóa", + "detachedHead": "Detached HEAD", + "noBranch": "Không có nhánh", + + "openInCurrentWindow": "Mở trong cửa sổ hiện tại", + "openInNewWindow": "Mở trong cửa sổ mới", + "merge": "Gộp", + "delete": "Xóa", + "newWorktree": "Worktree Mới", + + "createWorktree": "Tạo Worktree", + "createWorktreeDescription": "Tạo worktree mới để làm việc trên một nhánh riêng trong thư mục riêng của nó.", + "branchName": "Tên nhánh", + "createNewBranch": "Tạo nhánh mới", + "checkoutExisting": "Checkout nhánh hiện có", + "baseBranch": "Nhánh gốc", + "loadingBranches": "Đang tải nhánh...", + "selectBranch": "Chọn nhánh", + "searchBranch": "Tìm nhánh...", + "noBranchFound": "Không tìm thấy nhánh", + "localBranches": "Nhánh cục bộ", + "remoteBranches": "Nhánh từ xa", + "worktreePath": "Đường dẫn worktree", + "pathHint": "Đường dẫn nơi worktree sẽ được tạo", + "noIncludeFileWarning": "Không có tệp .worktreeinclude", + "noIncludeFileHint": "Không có tệp .worktreeinclude thì các tệp như node_modules sẽ không được sao chép sang worktree mới. Bạn có thể cần chạy npm install sau khi tạo.", + "create": "Tạo", + "creating": "Đang tạo...", + "cancel": "Hủy", + + "deleteWorktree": "Xóa Worktree", + "deleteWorktreeDescription": "Thao tác này sẽ xóa worktree và tất cả các tệp của nó.", + "deleteWarning": "Không thể hoàn tác thao tác này. Những thứ sau sẽ bị xóa:", + "deleteWarningBranch": "Nhánh '{{branch}}' và tất cả thay đổi chưa commit", + "deleteWarningFiles": "Tất cả tệp trong thư mục worktree", + "forceDelete": "Buộc xóa", + "worktreeIsLocked": "worktree đang bị khóa", + "deleting": "Đang xóa...", + + "mergeBranch": "Gộp Nhánh", + "mergeDescription": "Gộp '{{source}}' vào '{{target}}'", + "deleteAfterMerge": "Xóa worktree sau khi gộp thành công", + "merging": "Đang gộp...", + "mergeSuccess": "Gộp thành công", + "mergeConflicts": "Xung đột khi gộp", + "conflictsDescription": "Các tệp sau có xung đột cần được giải quyết:", + "mergeFailed": "Gộp thất bại", + "resolveManually": "Tôi sẽ tự xử lý", + "askRooResolve": "Nhờ Roo xử lý", + "close": "Đóng" +} diff --git a/webview-ui/src/i18n/locales/zh-CN/worktrees.json b/webview-ui/src/i18n/locales/zh-CN/worktrees.json new file mode 100644 index 00000000000..24c87b93bc0 --- /dev/null +++ b/webview-ui/src/i18n/locales/zh-CN/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "完成", + "description": "Git worktrees 可让你在不同目录中同时处理多个分支。每个 worktree 都会拥有一个带 Roo Code 的独立 VS Code 窗口。", + + "notGitRepo": "此工作区不是 Git 仓库。worktrees 需要 Git 仓库才能工作。", + "multiRootNotSupported": "多根工作区不支持 worktrees。请打开单个文件夹以使用 worktrees。", + "subfolderNotSupported": "此工作区是 Git 仓库的子文件夹。请打开仓库根目录以使用 worktrees。", + "gitRoot": "Git 根目录", + + "includeFileExists": "已找到 .worktreeinclude 文件 — 文件将复制到新 worktrees", + "noIncludeFile": "未找到 .worktreeinclude 文件", + "createFromGitignore": "从 .gitignore 创建", + + "primary": "主", + "current": "当前", + "locked": "已锁定", + "detachedHead": "Detached HEAD", + "noBranch": "无分支", + + "openInCurrentWindow": "在当前窗口打开", + "openInNewWindow": "在新窗口打开", + "merge": "合并", + "delete": "删除", + "newWorktree": "新建 Worktree", + + "createWorktree": "创建 Worktree", + "createWorktreeDescription": "创建一个新的 worktree,在独立目录中处理单独的分支。", + "branchName": "分支名称", + "createNewBranch": "创建新分支", + "checkoutExisting": "Checkout 现有分支", + "baseBranch": "基准分支", + "loadingBranches": "正在加载分支...", + "selectBranch": "选择分支", + "searchBranch": "搜索分支...", + "noBranchFound": "未找到分支", + "localBranches": "本地分支", + "remoteBranches": "远程分支", + "worktreePath": "Worktree 路径", + "pathHint": "worktree 将创建到的路径", + "noIncludeFileWarning": "没有 .worktreeinclude 文件", + "noIncludeFileHint": "没有 .worktreeinclude 文件时,node_modules 等文件不会复制到新 worktree。创建后你可能需要运行 npm install。", + "create": "创建", + "creating": "正在创建...", + "cancel": "取消", + + "deleteWorktree": "删除 Worktree", + "deleteWorktreeDescription": "这将删除 worktree 及其所有文件。", + "deleteWarning": "此操作不可逆。将删除以下内容:", + "deleteWarningBranch": "分支 '{{branch}}' 以及所有未提交的更改", + "deleteWarningFiles": "worktree 目录中的所有文件", + "forceDelete": "强制删除", + "worktreeIsLocked": "worktree 已锁定", + "deleting": "正在删除...", + + "mergeBranch": "合并分支", + "mergeDescription": "将 '{{source}}' 合并到 '{{target}}'", + "deleteAfterMerge": "合并成功后删除 worktree", + "merging": "正在合并...", + "mergeSuccess": "合并成功", + "mergeConflicts": "合并冲突", + "conflictsDescription": "以下文件存在冲突,需要解决:", + "mergeFailed": "合并失败", + "resolveManually": "我来手动解决", + "askRooResolve": "让 Roo 来解决", + "close": "关闭" +} diff --git a/webview-ui/src/i18n/locales/zh-TW/worktrees.json b/webview-ui/src/i18n/locales/zh-TW/worktrees.json new file mode 100644 index 00000000000..5f0735a48c9 --- /dev/null +++ b/webview-ui/src/i18n/locales/zh-TW/worktrees.json @@ -0,0 +1,67 @@ +{ + "title": "Worktrees", + "done": "完成", + "description": "Git worktrees 讓你能在不同目錄中同時處理多個分支。每個 worktree 都會有一個搭配 Roo Code 的獨立 VS Code 視窗。", + + "notGitRepo": "此工作區不是 Git 儲存庫。worktrees 需要 Git 儲存庫才能運作。", + "multiRootNotSupported": "多根工作區不支援 worktrees。請開啟單一資料夾以使用 worktrees。", + "subfolderNotSupported": "此工作區是 Git 儲存庫的子資料夾。請開啟儲存庫根目錄以使用 worktrees。", + "gitRoot": "Git 根目錄", + + "includeFileExists": "已找到 .worktreeinclude 檔案 — 檔案將複製到新 worktrees", + "noIncludeFile": "未找到 .worktreeinclude 檔案", + "createFromGitignore": "從 .gitignore 建立", + + "primary": "主要", + "current": "目前", + "locked": "已鎖定", + "detachedHead": "Detached HEAD", + "noBranch": "無分支", + + "openInCurrentWindow": "在目前視窗開啟", + "openInNewWindow": "在新視窗開啟", + "merge": "合併", + "delete": "刪除", + "newWorktree": "新增 Worktree", + + "createWorktree": "建立 Worktree", + "createWorktreeDescription": "建立新的 worktree,讓你在獨立目錄中處理單獨的分支。", + "branchName": "分支名稱", + "createNewBranch": "建立新分支", + "checkoutExisting": "Checkout 現有分支", + "baseBranch": "基準分支", + "loadingBranches": "正在載入分支...", + "selectBranch": "選擇分支", + "searchBranch": "搜尋分支...", + "noBranchFound": "找不到分支", + "localBranches": "本機分支", + "remoteBranches": "遠端分支", + "worktreePath": "Worktree 路徑", + "pathHint": "worktree 將建立到的路徑", + "noIncludeFileWarning": "沒有 .worktreeinclude 檔案", + "noIncludeFileHint": "沒有 .worktreeinclude 檔案時,node_modules 等檔案不會複製到新的 worktree。建立後你可能需要執行 npm install。", + "create": "建立", + "creating": "正在建立...", + "cancel": "取消", + + "deleteWorktree": "刪除 Worktree", + "deleteWorktreeDescription": "這會刪除 worktree 及其所有檔案。", + "deleteWarning": "此操作無法復原。將刪除以下內容:", + "deleteWarningBranch": "分支 '{{branch}}' 以及所有未提交的變更", + "deleteWarningFiles": "worktree 目錄中的所有檔案", + "forceDelete": "強制刪除", + "worktreeIsLocked": "worktree 已鎖定", + "deleting": "正在刪除...", + + "mergeBranch": "合併分支", + "mergeDescription": "將 '{{source}}' 合併到 '{{target}}'", + "deleteAfterMerge": "合併成功後刪除 worktree", + "merging": "正在合併...", + "mergeSuccess": "合併成功", + "mergeConflicts": "合併衝突", + "conflictsDescription": "以下檔案有衝突需要解決:", + "mergeFailed": "合併失敗", + "resolveManually": "我會手動解決", + "askRooResolve": "請 Roo 協助解決", + "close": "關閉" +}