Browse Source

Git worktree management (#10458)

Co-authored-by: Roo Code <[email protected]>
Co-authored-by: Hannes Rudolph <[email protected]>
Co-authored-by: daniel-lxs <[email protected]>
Chris Estreich 1 day ago
parent
commit
04256be956
68 changed files with 4287 additions and 16 deletions
  1. 2 0
      apps/cli/package.json
  2. 1 0
      packages/core/package.json
  3. 1 0
      packages/core/src/index.ts
  4. 268 0
      packages/core/src/worktree/__tests__/worktree-include.spec.ts
  5. 146 0
      packages/core/src/worktree/__tests__/worktree-service.spec.ts
  6. 13 0
      packages/core/src/worktree/index.ts
  7. 17 0
      packages/core/src/worktree/types.ts
  8. 256 0
      packages/core/src/worktree/worktree-include.ts
  9. 444 0
      packages/core/src/worktree/worktree-service.ts
  10. 6 0
      packages/types/src/global-settings.ts
  11. 1 0
      packages/types/src/index.ts
  12. 77 0
      packages/types/src/vscode-extension-host.ts
  13. 1 0
      packages/types/src/vscode.ts
  14. 129 0
      packages/types/src/worktree.ts
  15. 73 0
      pnpm-lock.yaml
  16. 6 0
      src/activate/registerCommands.ts
  17. 254 0
      src/core/webview/webviewMessageHandler.ts
  18. 277 0
      src/core/webview/worktree/handlers.ts
  19. 23 0
      src/core/webview/worktree/index.ts
  20. 52 0
      src/extension.ts
  21. 17 2
      src/package.json
  22. 1 0
      src/package.nls.ca.json
  23. 1 0
      src/package.nls.de.json
  24. 1 0
      src/package.nls.es.json
  25. 1 0
      src/package.nls.fr.json
  26. 1 0
      src/package.nls.hi.json
  27. 1 0
      src/package.nls.id.json
  28. 1 0
      src/package.nls.it.json
  29. 1 0
      src/package.nls.ja.json
  30. 1 0
      src/package.nls.json
  31. 1 0
      src/package.nls.ko.json
  32. 1 0
      src/package.nls.nl.json
  33. 1 0
      src/package.nls.pl.json
  34. 1 0
      src/package.nls.pt-BR.json
  35. 1 0
      src/package.nls.ru.json
  36. 1 0
      src/package.nls.tr.json
  37. 1 0
      src/package.nls.vi.json
  38. 1 0
      src/package.nls.zh-CN.json
  39. 1 0
      src/package.nls.zh-TW.json
  40. 1 0
      webview-ui/package.json
  41. 4 1
      webview-ui/src/App.tsx
  42. 22 7
      webview-ui/src/components/ui/command.tsx
  43. 1 0
      webview-ui/src/components/ui/index.ts
  44. 35 0
      webview-ui/src/components/ui/radio-group.tsx
  45. 31 5
      webview-ui/src/components/ui/searchable-select.tsx
  46. 4 1
      webview-ui/src/components/ui/select.tsx
  47. 230 0
      webview-ui/src/components/worktrees/CreateWorktreeModal.tsx
  48. 142 0
      webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx
  49. 526 0
      webview-ui/src/components/worktrees/WorktreesView.tsx
  50. 3 0
      webview-ui/src/components/worktrees/index.ts
  51. 67 0
      webview-ui/src/i18n/locales/ca/worktrees.json
  52. 67 0
      webview-ui/src/i18n/locales/de/worktrees.json
  53. 67 0
      webview-ui/src/i18n/locales/en/worktrees.json
  54. 67 0
      webview-ui/src/i18n/locales/es/worktrees.json
  55. 67 0
      webview-ui/src/i18n/locales/fr/worktrees.json
  56. 67 0
      webview-ui/src/i18n/locales/hi/worktrees.json
  57. 67 0
      webview-ui/src/i18n/locales/id/worktrees.json
  58. 67 0
      webview-ui/src/i18n/locales/it/worktrees.json
  59. 67 0
      webview-ui/src/i18n/locales/ja/worktrees.json
  60. 67 0
      webview-ui/src/i18n/locales/ko/worktrees.json
  61. 67 0
      webview-ui/src/i18n/locales/nl/worktrees.json
  62. 67 0
      webview-ui/src/i18n/locales/pl/worktrees.json
  63. 67 0
      webview-ui/src/i18n/locales/pt-BR/worktrees.json
  64. 67 0
      webview-ui/src/i18n/locales/ru/worktrees.json
  65. 67 0
      webview-ui/src/i18n/locales/tr/worktrees.json
  66. 67 0
      webview-ui/src/i18n/locales/vi/worktrees.json
  67. 67 0
      webview-ui/src/i18n/locales/zh-CN/worktrees.json
  68. 67 0
      webview-ui/src/i18n/locales/zh-TW/worktrees.json

+ 2 - 0
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",

+ 1 - 0
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"
 	},

+ 1 - 0
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"

+ 268 - 0
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<string> {
+	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", "[email protected]"])
+
+			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")
+		})
+	})
+})

+ 146 - 0
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<WorktreeService["parseWorktreeOutput"]> => {
+			// @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,
+			})
+		})
+	})
+})

+ 13 - 0
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"

+ 17 - 0
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"

+ 256 - 0
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<boolean> {
+		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<boolean> {
+		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<WorktreeIncludeStatus> {
+		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<void> {
+		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<string[]> {
+		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<string[]> {
+		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<string[]> {
+		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<void> {
+		// 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()

+ 444 - 0
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<boolean> {
+		try {
+			await execAsync("git --version")
+			return true
+		} catch {
+			return false
+		}
+	}
+
+	/**
+	 * Check if a directory is a git repository.
+	 */
+	async checkGitRepo(cwd: string): Promise<boolean> {
+		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<string | null> {
+		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<string | null> {
+		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<string | null> {
+		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<Worktree[]> {
+		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<WorktreeResult> {
+		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 <branch> <path> [<base>]
+				args.push("-b", branch, worktreePath)
+				if (baseBranch) {
+					args.push(baseBranch)
+				}
+			} else if (branch) {
+				// Checkout existing branch: git worktree add <path> <branch>
+				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<WorktreeResult> {
+		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<BranchInfo> {
+		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<MergeWorktreeResult> {
+		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<WorktreeResult> {
+		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<Worktree> = {
+				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()

+ 6 - 0
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<typeof globalSettingsSchema>

+ 1 - 0
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"

+ 77 - 0
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 {

+ 1 - 0
packages/types/src/vscode.ts

@@ -35,6 +35,7 @@ export const commandIds = [
 	"popoutButtonClicked",
 	"cloudButtonClicked",
 	"settingsButtonClicked",
+	"worktreesButtonClicked",
 
 	"openInNewTab",
 

+ 129 - 0
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
+}

+ 73 - 0
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([email protected])([email protected])
@@ -1115,6 +1124,9 @@ importers:
       '@radix-ui/react-progress':
         specifier: ^1.1.2
         version: 1.1.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-radio-group':
+        specifier: ^1.3.8
+        version: 1.3.8(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
       '@radix-ui/react-select':
         specifier: ^2.1.6
         version: 2.2.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
@@ -3074,6 +3086,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/[email protected]':
+    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/[email protected]':
     resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==}
     peerDependencies:
@@ -3087,6 +3112,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/[email protected]':
+    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/[email protected]':
     resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==}
     peerDependencies:
@@ -12862,6 +12900,24 @@ snapshots:
       '@types/react': 18.3.23
       '@types/react-dom': 18.3.7(@types/[email protected])
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-direction': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-presence': 1.1.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-roving-focus': 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-previous': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-size': 1.1.1(@types/[email protected])([email protected])
+      react: 18.3.1
+      react-dom: 18.3.1([email protected])
+    optionalDependencies:
+      '@types/react': 18.3.23
+      '@types/react-dom': 18.3.7(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
       '@radix-ui/primitive': 1.1.2
@@ -12879,6 +12935,23 @@ snapshots:
       '@types/react': 18.3.23
       '@types/react-dom': 18.3.7(@types/[email protected])
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-collection': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-direction': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      react: 18.3.1
+      react-dom: 18.3.1([email protected])
+    optionalDependencies:
+      '@types/react': 18.3.23
+      '@types/react-dom': 18.3.7(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
       '@radix-ui/primitive': 1.1.2

+ 6 - 0
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")

+ 254 - 0
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}`)
 			//

+ 277 - 0
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<boolean> {
+	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<WorktreeListResponse> {
+	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<WorktreeResult> {
+	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<WorktreeResult> {
+	const cwd = provider.cwd
+	return worktreeService.deleteWorktree(cwd, worktreePath, force)
+}
+
+export async function handleSwitchWorktree(
+	provider: ClineProvider,
+	worktreePath: string,
+	newWindow: boolean,
+): Promise<WorktreeResult> {
+	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<BranchInfo> {
+	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<WorktreeDefaultsResponse> {
+	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<WorktreeIncludeStatus> {
+	const cwd = provider.cwd
+	return worktreeIncludeService.getStatus(cwd)
+}
+
+export async function handleCheckBranchWorktreeInclude(provider: ClineProvider, branch: string): Promise<boolean> {
+	const cwd = provider.cwd
+	return worktreeIncludeService.branchHasWorktreeInclude(cwd, branch)
+}
+
+export async function handleCreateWorktreeInclude(provider: ClineProvider, content: string): Promise<WorktreeResult> {
+	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<WorktreeResult> {
+	const cwd = provider.cwd
+	return worktreeService.checkoutBranch(cwd, branch)
+}
+
+export async function handleMergeWorktree(
+	provider: ClineProvider,
+	options: {
+		worktreePath: string
+		targetBranch: string
+		deleteAfterMerge?: boolean
+	},
+): Promise<MergeWorktreeResult> {
+	const cwd = provider.cwd
+	return worktreeService.mergeWorktree(cwd, options)
+}

+ 23 - 0
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"

+ 52 - 0
src/extension.ts

@@ -62,6 +62,55 @@ let authStateChangedHandler: ((data: { state: AuthState; previousState: AuthStat
 let settingsUpdatedHandler: (() => void) | undefined
 let userInfoHandler: ((data: { userInfo: CloudUserInfo }) => Promise<void>) | 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<void> {
+	try {
+		const worktreeAutoOpenPath = context.globalState.get<string>("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, {

+ 17 - 2
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"
 				}
 			]
 		},

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 0
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é",

+ 1 - 0
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": "वे कमांड जो स्वचालित रूप से निष्पादित की जा सकती हैं जब 'हमेशा निष्पादन संचालन को स्वीकृत करें' सक्रिय हो",

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 0
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": "コードの説明",

+ 1 - 0
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",

+ 1 - 0
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": "'항상 실행 작업 승인' 이 활성화되어 있을 때 자동으로 실행할 수 있는 명령어",

+ 1 - 0
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",

+ 1 - 0
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'",

+ 1 - 0
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",

+ 1 - 0
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": "Объяснить код",

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 0
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": "当启用'始终批准执行操作'时可以自动执行的命令",

+ 1 - 0
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": "當啟用'始終批准執行操作'時可以自動執行的命令",

+ 1 - 0
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",

+ 4 - 1
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<Record<NonNullable<ExtensionMessage["action"]
 	historyButtonClicked: "history",
 	marketplaceButtonClicked: "marketplace",
 	cloudButtonClicked: "cloud",
+	worktreesButtonClicked: "worktrees",
 }
 
 const App = () => {
@@ -245,6 +247,7 @@ const App = () => {
 					organizations={cloudOrganizations}
 				/>
 			)}
+			{tab === "worktrees" && <WorktreesView onDone={() => switchTab("chat")} />}
 			<ChatView
 				ref={chatViewRef}
 				isHidden={tab !== "chat"}

+ 22 - 7
webview-ui/src/components/ui/command.tsx

@@ -57,13 +57,28 @@ CommandInput.displayName = CommandPrimitive.Input.displayName
 const CommandList = React.forwardRef<
 	React.ElementRef<typeof CommandPrimitive.List>,
 	React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
->(({ className, ...props }, ref) => (
-	<CommandPrimitive.List
-		ref={ref}
-		className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
-		{...props}
-	/>
-))
+>(({ className, onWheel, ...props }, ref) => {
+	const handleWheel = React.useCallback(
+		(e: React.WheelEvent<HTMLDivElement>) => {
+			// 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 (
+		<CommandPrimitive.List
+			ref={ref}
+			className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden overscroll-contain", className)}
+			onWheel={handleWheel}
+			{...props}
+		/>
+	)
+})
 
 CommandList.displayName = CommandPrimitive.List.displayName
 

+ 1 - 0
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"

+ 35 - 0
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<typeof RadioGroupPrimitive.Root>,
+	React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
+>(({ className, ...props }, ref) => {
+	return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />
+})
+RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
+
+const RadioGroupItem = React.forwardRef<
+	React.ElementRef<typeof RadioGroupPrimitive.Item>,
+	React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
+>(({ className, ...props }, ref) => {
+	return (
+		<RadioGroupPrimitive.Item
+			ref={ref}
+			className={cn(
+				"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
+				className,
+			)}
+			{...props}>
+			<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
+				<Circle className="h-3.5 w-3.5 fill-primary" />
+			</RadioGroupPrimitive.Indicator>
+		</RadioGroupPrimitive.Item>
+	)
+})
+RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
+
+export { RadioGroup, RadioGroupItem }

+ 31 - 5
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({
 				</Button>
 			</PopoverTrigger>
 			<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
-				<Command>
+				<Command shouldFilter={false}>
 					<div className="relative">
 						<CommandInput
 							ref={searchInputRef}

+ 4 - 1
webview-ui/src/components/ui/select.tsx

@@ -78,7 +78,10 @@ function SelectLabel({ className, ...props }: React.ComponentProps<typeof Select
 	return (
 		<SelectPrimitive.Label
 			data-slot="select-label"
-			className={cn("px-2 py-1.5 text-sm font-medium", className)}
+			className={cn(
+				"px-2 py-1.5 text-xs font-semibold text-vscode-descriptionForeground uppercase tracking-wide",
+				className,
+			)}
 			{...props}
 		/>
 	)

+ 230 - 0
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<WorktreeDefaultsResponse | null>(null)
+	const [branches, setBranches] = useState<BranchInfo | null>(null)
+	const [includeStatus, setIncludeStatus] = useState<WorktreeIncludeStatus | null>(null)
+
+	// UI state
+	const [isCreating, setIsCreating] = useState(false)
+	const [error, setError] = useState<string | null>(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: <span className="codicon codicon-git-branch mr-2 text-vscode-descriptionForeground" />,
+		}))
+
+		const remoteOptions: SearchableSelectOption[] = branches.remoteBranches.map((branch) => ({
+			value: branch,
+			label: branch,
+			icon: <span className="codicon codicon-cloud mr-2 text-vscode-descriptionForeground" />,
+		}))
+
+		return [...localOptions, ...remoteOptions]
+	}, [branches])
+
+	return (
+		<Dialog open={open} onOpenChange={(isOpen: boolean) => !isOpen && onClose()}>
+			<DialogContent className="max-w-lg">
+				<DialogHeader>
+					<DialogTitle>{t("worktrees:createWorktree")}</DialogTitle>
+					<DialogDescription>{t("worktrees:createWorktreeDescription")}</DialogDescription>
+				</DialogHeader>
+
+				<div className="flex flex-col gap-3">
+					{/* No .worktreeinclude warning - shows when the current worktree doesn't have .worktreeinclude */}
+					{includeStatus?.exists === false && (
+						<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-vscode-inputValidation-warningBackground border border-vscode-inputValidation-warningBorder text-sm">
+							<span className="codicon codicon-warning text-vscode-charts-yellow flex-shrink-0" />
+							<span className="text-vscode-foreground">
+								<span className="font-medium">{t("worktrees:noIncludeFileWarning")}</span>
+								{" — "}
+								<span className="text-vscode-descriptionForeground">
+									{t("worktrees:noIncludeFileHint")}
+								</span>
+							</span>
+						</div>
+					)}
+
+					{/* Branch name */}
+					<div className="flex flex-col gap-1">
+						<label className="text-sm text-vscode-foreground">{t("worktrees:branchName")}</label>
+						<Input
+							value={branchName}
+							onChange={(e) => setBranchName(e.target.value)}
+							placeholder={defaults?.suggestedBranch || "worktree/feature-name"}
+							className="rounded-full"
+						/>
+					</div>
+
+					{/* Base branch selector */}
+					<div className="flex flex-col gap-1">
+						<label className="text-sm text-vscode-foreground">{t("worktrees:baseBranch")}</label>
+						{!branches ? (
+							<div className="flex items-center gap-2 h-8 px-2 text-sm text-vscode-descriptionForeground">
+								<span className="codicon codicon-loading codicon-modifier-spin" />
+								<span>{t("worktrees:loadingBranches")}</span>
+							</div>
+						) : (
+							<SearchableSelect
+								value={baseBranch}
+								onValueChange={setBaseBranch}
+								options={branchOptions}
+								placeholder={t("worktrees:selectBranch")}
+								searchPlaceholder={t("worktrees:searchBranch")}
+								emptyMessage={t("worktrees:noBranchFound")}
+							/>
+						)}
+					</div>
+
+					{/* Worktree path */}
+					<div className="flex flex-col gap-1">
+						<label className="text-sm text-vscode-foreground">{t("worktrees:worktreePath")}</label>
+						<Input
+							value={worktreePath}
+							onChange={(e) => setWorktreePath(e.target.value)}
+							placeholder={defaults?.suggestedPath || "/path/to/worktree"}
+							className="rounded-full"
+						/>
+						<p className="text-xs text-vscode-descriptionForeground">{t("worktrees:pathHint")}</p>
+					</div>
+
+					{/* Error message */}
+					{error && (
+						<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-vscode-inputValidation-errorBackground border border-vscode-inputValidation-errorBorder text-sm">
+							<span className="codicon codicon-error text-vscode-errorForeground flex-shrink-0" />
+							<p className="text-vscode-errorForeground">{error}</p>
+						</div>
+					)}
+				</div>
+
+				<DialogFooter>
+					<Button variant="secondary" onClick={onClose}>
+						{t("worktrees:cancel")}
+					</Button>
+					<Button onClick={handleCreate} disabled={!isValid || isCreating}>
+						{isCreating ? (
+							<>
+								<span className="codicon codicon-loading codicon-modifier-spin mr-2" />
+								{t("worktrees:creating")}
+							</>
+						) : (
+							t("worktrees:create")
+						)}
+					</Button>
+				</DialogFooter>
+			</DialogContent>
+		</Dialog>
+	)
+}

+ 142 - 0
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<string | null>(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 (
+		<Dialog open={open} onOpenChange={(isOpen: boolean) => !isOpen && onClose()}>
+			<DialogContent>
+				<DialogHeader>
+					<div className="flex items-center gap-2">
+						<span className="codicon codicon-warning text-vscode-charts-yellow text-xl" />
+						<DialogTitle>{t("worktrees:deleteWorktree")}</DialogTitle>
+					</div>
+					<DialogDescription>{t("worktrees:deleteWorktreeDescription")}</DialogDescription>
+				</DialogHeader>
+
+				<div className="flex flex-col gap-3 overflow-hidden">
+					{/* Worktree info */}
+					<div className="px-2 py-1.5 rounded bg-vscode-input-background border border-vscode-input-border">
+						<div className="flex items-center gap-2">
+							<span className="codicon codicon-git-branch flex-shrink-0" />
+							<span className="font-medium text-vscode-foreground truncate">
+								{worktree.branch ||
+									(worktree.isDetached ? t("worktrees:detachedHead") : t("worktrees:noBranch"))}
+							</span>
+						</div>
+						<div className="text-xs text-vscode-descriptionForeground mt-0.5 truncate">{worktree.path}</div>
+					</div>
+
+					{/* Warning message */}
+					<div className="flex items-start gap-2 p-2 rounded bg-vscode-inputValidation-warningBackground border border-vscode-inputValidation-warningBorder text-sm">
+						<span className="codicon codicon-warning text-vscode-charts-yellow flex-shrink-0 mt-0.5" />
+						<div className="min-w-0">
+							<p className="text-vscode-foreground m-0">{t("worktrees:deleteWarning")}</p>
+							<ul className="mt-1 mb-0 pl-0 list-none space-y-0.5 text-vscode-descriptionForeground">
+								<li>• {t("worktrees:deleteWarningBranch", { branch: worktree.branch || "HEAD" })}</li>
+								<li>• {t("worktrees:deleteWarningFiles")}</li>
+							</ul>
+						</div>
+					</div>
+
+					{/* Force delete option (if worktree is locked) */}
+					{worktree.isLocked && (
+						<div className="flex items-center gap-2">
+							<Checkbox
+								id="force-delete"
+								checked={forceDelete}
+								onCheckedChange={(checked) => setForceDelete(checked === true)}
+							/>
+							<label htmlFor="force-delete" className="text-sm text-vscode-foreground cursor-pointer">
+								{t("worktrees:forceDelete")}
+								<span className="text-vscode-descriptionForeground ml-1">
+									({t("worktrees:worktreeIsLocked")})
+								</span>
+							</label>
+						</div>
+					)}
+
+					{/* Error message */}
+					{error && (
+						<div className="flex items-center gap-2 px-2 py-1.5 rounded bg-vscode-inputValidation-errorBackground border border-vscode-inputValidation-errorBorder text-sm">
+							<span className="codicon codicon-error text-vscode-errorForeground flex-shrink-0" />
+							<p className="text-vscode-errorForeground">{error}</p>
+						</div>
+					)}
+				</div>
+
+				<DialogFooter>
+					<Button variant="secondary" onClick={onClose}>
+						{t("worktrees:cancel")}
+					</Button>
+					<Button variant="destructive" onClick={handleDelete} disabled={isDeleting}>
+						{isDeleting ? (
+							<>
+								<span className="codicon codicon-loading codicon-modifier-spin mr-2" />
+								{t("worktrees:deleting")}
+							</>
+						) : (
+							t("worktrees:delete")
+						)}
+					</Button>
+				</DialogFooter>
+			</DialogContent>
+		</Dialog>
+	)
+}

+ 526 - 0
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<Worktree[]>([])
+	const [isLoading, setIsLoading] = useState(true)
+	const [error, setError] = useState<string | null>(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<WorktreeIncludeStatus | null>(null)
+	const [isCreatingInclude, setIsCreatingInclude] = useState(false)
+
+	// Modals
+	const [showCreateModal, setShowCreateModal] = useState(false)
+	const [deleteWorktree, setDeleteWorktree] = useState<Worktree | null>(null)
+
+	// Merge state
+	const [mergeWorktree, setMergeWorktree] = useState<Worktree | null>(null)
+	const [mergeTargetBranch, setMergeTargetBranch] = useState("")
+	const [mergeDeleteAfter, setMergeDeleteAfter] = useState(false)
+	const [isMerging, setIsMerging] = useState(false)
+	const [mergeResult, setMergeResult] = useState<MergeWorktreeResult | null>(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 (
+			<Tab>
+				<TabHeader className="flex justify-between items-center">
+					<h3 className="text-vscode-foreground m-0">{t("worktrees:title")}</h3>
+					<Button onClick={onDone}>{t("worktrees:done")}</Button>
+				</TabHeader>
+				<TabContent>
+					<div className="flex flex-col items-center justify-center h-48 text-vscode-descriptionForeground">
+						<span className="codicon codicon-warning text-4xl mb-4" />
+						<p className="text-center">{t("worktrees:notGitRepo")}</p>
+					</div>
+				</TabContent>
+			</Tab>
+		)
+	}
+
+	if (isMultiRoot) {
+		return (
+			<Tab>
+				<TabHeader className="flex justify-between items-center">
+					<h3 className="text-vscode-foreground m-0">{t("worktrees:title")}</h3>
+					<Button onClick={onDone}>{t("worktrees:done")}</Button>
+				</TabHeader>
+				<TabContent>
+					<div className="flex flex-col items-center justify-center h-48 text-vscode-descriptionForeground">
+						<span className="codicon codicon-warning text-4xl mb-4" />
+						<p className="text-center">{t("worktrees:multiRootNotSupported")}</p>
+					</div>
+				</TabContent>
+			</Tab>
+		)
+	}
+
+	if (isSubfolder) {
+		return (
+			<Tab>
+				<TabHeader className="flex justify-between items-center">
+					<h3 className="text-vscode-foreground m-0">{t("worktrees:title")}</h3>
+					<Button onClick={onDone}>{t("worktrees:done")}</Button>
+				</TabHeader>
+				<TabContent>
+					<div className="flex flex-col items-center justify-center h-48 text-vscode-descriptionForeground">
+						<span className="codicon codicon-warning text-4xl mb-4" />
+						<p className="text-center">{t("worktrees:subfolderNotSupported")}</p>
+						<p className="text-sm mt-2 text-center">
+							{t("worktrees:gitRoot")}:{" "}
+							<code className="bg-vscode-input-background px-2 py-1 rounded">{gitRootPath}</code>
+						</p>
+					</div>
+				</TabContent>
+			</Tab>
+		)
+	}
+
+	// Find the primary (bare/main) worktree for merge target.
+	const primaryWorktree = worktrees.find((w) => w.isBare || worktrees.indexOf(w) === 0)
+
+	return (
+		<Tab>
+			<TabHeader className="flex flex-col gap-2">
+				<div className="flex justify-between items-center">
+					<h3 className="text-vscode-foreground m-0">{t("worktrees:title")}</h3>
+					<Button onClick={onDone}>{t("worktrees:done")}</Button>
+				</div>
+				<p className="text-vscode-descriptionForeground text-sm m-0">{t("worktrees:description")}</p>
+
+				{/* Worktree include status */}
+				{includeStatus && (
+					<div className="flex items-center gap-2 text-sm">
+						{includeStatus.exists ? (
+							<>
+								<span className="codicon codicon-check text-vscode-charts-green" />
+								<span className="text-vscode-descriptionForeground">
+									{t("worktrees:includeFileExists")}
+								</span>
+							</>
+						) : (
+							<>
+								<span className="codicon codicon-warning text-vscode-charts-yellow" />
+								<span className="text-vscode-descriptionForeground">
+									{t("worktrees:noIncludeFile")}
+								</span>
+								{includeStatus.hasGitignore && (
+									<Button
+										variant="secondary"
+										size="sm"
+										onClick={handleCreateWorktreeInclude}
+										disabled={isCreatingInclude}>
+										{t("worktrees:createFromGitignore")}
+									</Button>
+								)}
+							</>
+						)}
+					</div>
+				)}
+			</TabHeader>
+
+			<TabContent className="px-2 py-0">
+				{isLoading ? (
+					<div className="flex items-center justify-center h-48">
+						<span className="codicon codicon-loading codicon-modifier-spin text-2xl" />
+					</div>
+				) : error ? (
+					<div className="flex flex-col items-center justify-center h-48 text-vscode-errorForeground">
+						<span className="codicon codicon-error text-4xl mb-4" />
+						<p className="text-center">{error}</p>
+					</div>
+				) : (
+					<div className="flex flex-col gap-1 py-2">
+						{worktrees.map((worktree) => (
+							<div
+								key={worktree.path}
+								className={`p-3 rounded-xl border transition-colors ${
+									worktree.isCurrent
+										? "border-vscode-focusBorder bg-vscode-list-activeSelectionBackground"
+										: "border-transparent bg-vscode-editor-background hover:bg-vscode-editor-foreground/10"
+								}`}>
+								<div className="flex items-start justify-between">
+									<div className="flex-1 min-w-0">
+										<div className="flex items-center gap-2 flex-wrap">
+											<span className="codicon codicon-git-branch" />
+											<span className="font-medium truncate">
+												{worktree.branch ||
+													(worktree.isDetached
+														? t("worktrees:detachedHead")
+														: t("worktrees:noBranch"))}
+											</span>
+											{worktree.isBare && <Badge>{t("worktrees:primary")}</Badge>}
+											{worktree.isCurrent && (
+												<Badge variant="secondary">{t("worktrees:current")}</Badge>
+											)}
+											{worktree.isLocked && (
+												<StandardTooltip content={worktree.lockReason || t("worktrees:locked")}>
+													<span className="codicon codicon-lock text-vscode-charts-yellow" />
+												</StandardTooltip>
+											)}
+										</div>
+										<div className="text-xs text-vscode-descriptionForeground mt-1 truncate">
+											{worktree.path}
+										</div>
+									</div>
+
+									<div className="flex items-center gap-1 ml-2 flex-shrink-0">
+										{!worktree.isCurrent && (
+											<>
+												<StandardTooltip content={t("worktrees:openInCurrentWindow")}>
+													<Button
+														variant="ghost"
+														size="sm"
+														onClick={() => handleSwitchWorktree(worktree.path, false)}>
+														<span className="codicon codicon-window" />
+													</Button>
+												</StandardTooltip>
+												<StandardTooltip content={t("worktrees:openInNewWindow")}>
+													<Button
+														variant="ghost"
+														size="sm"
+														onClick={() => handleSwitchWorktree(worktree.path, true)}>
+														<span className="codicon codicon-empty-window" />
+													</Button>
+												</StandardTooltip>
+											</>
+										)}
+										{!worktree.isBare &&
+											worktree.branch &&
+											primaryWorktree &&
+											worktree.branch !== primaryWorktree.branch && (
+												<StandardTooltip content={t("worktrees:merge")}>
+													<Button
+														variant="ghost"
+														size="sm"
+														onClick={() => {
+															setMergeWorktree(worktree)
+															setMergeTargetBranch(primaryWorktree.branch || "main")
+														}}>
+														<span className="codicon codicon-git-merge" />
+													</Button>
+												</StandardTooltip>
+											)}
+										{!worktree.isBare && !worktree.isCurrent && (
+											<StandardTooltip content={t("worktrees:delete")}>
+												<Button
+													variant="ghost"
+													size="sm"
+													onClick={() => setDeleteWorktree(worktree)}>
+													<span className="codicon codicon-trash text-vscode-errorForeground" />
+												</Button>
+											</StandardTooltip>
+										)}
+									</div>
+								</div>
+							</div>
+						))}
+
+						{/* New Worktree button */}
+						<Button variant="secondary" className="mt-2" onClick={() => setShowCreateModal(true)}>
+							<span className="codicon codicon-add mr-2" />
+							{t("worktrees:newWorktree")}
+						</Button>
+					</div>
+				)}
+			</TabContent>
+
+			{/* Create Modal */}
+			{showCreateModal && (
+				<CreateWorktreeModal
+					open={showCreateModal}
+					onClose={() => setShowCreateModal(false)}
+					onSuccess={() => {
+						setShowCreateModal(false)
+						fetchWorktrees()
+					}}
+				/>
+			)}
+
+			{/* Delete Modal */}
+			{deleteWorktree && (
+				<DeleteWorktreeModal
+					open={!!deleteWorktree}
+					onClose={() => setDeleteWorktree(null)}
+					worktree={deleteWorktree}
+					onSuccess={() => {
+						setDeleteWorktree(null)
+						fetchWorktrees()
+					}}
+				/>
+			)}
+
+			{/* Merge Modal */}
+			{mergeWorktree && !mergeResult && (
+				<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
+					<div className="bg-vscode-editor-background border border-vscode-panel-border rounded-lg p-4 max-w-md w-full mx-4">
+						<h3 className="text-lg font-medium text-vscode-foreground mb-4">
+							{t("worktrees:mergeBranch")}
+						</h3>
+						<p className="text-sm text-vscode-descriptionForeground mb-4">
+							{t("worktrees:mergeDescription", {
+								source: mergeWorktree.branch,
+								target: mergeTargetBranch,
+							})}
+						</p>
+
+						<div className="mb-4">
+							<label className="flex items-center gap-2 text-sm text-vscode-foreground">
+								<input
+									type="checkbox"
+									checked={mergeDeleteAfter}
+									onChange={(e) => setMergeDeleteAfter(e.target.checked)}
+								/>
+								{t("worktrees:deleteAfterMerge")}
+							</label>
+						</div>
+
+						<div className="flex justify-end gap-2">
+							<Button variant="secondary" onClick={() => setMergeWorktree(null)}>
+								{t("worktrees:cancel")}
+							</Button>
+							<Button onClick={handleMerge} disabled={isMerging}>
+								{isMerging ? (
+									<>
+										<span className="codicon codicon-loading codicon-modifier-spin mr-2" />
+										{t("worktrees:merging")}
+									</>
+								) : (
+									t("worktrees:merge")
+								)}
+							</Button>
+						</div>
+					</div>
+				</div>
+			)}
+
+			{/* Merge Result Modal */}
+			{mergeResult && (
+				<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
+					<div className="bg-vscode-editor-background border border-vscode-panel-border rounded-lg p-4 max-w-md w-full mx-4">
+						{mergeResult.success ? (
+							<>
+								<div className="flex items-center gap-2 mb-4">
+									<span className="codicon codicon-check text-vscode-charts-green text-2xl" />
+									<h3 className="text-lg font-medium text-vscode-foreground">
+										{t("worktrees:mergeSuccess")}
+									</h3>
+								</div>
+								<p className="text-sm text-vscode-descriptionForeground mb-4">{mergeResult.message}</p>
+								<div className="flex justify-end">
+									<Button
+										onClick={() => {
+											setMergeWorktree(null)
+											setMergeResult(null)
+										}}>
+										{t("worktrees:done")}
+									</Button>
+								</div>
+							</>
+						) : mergeResult.hasConflicts ? (
+							<>
+								<div className="flex items-center gap-2 mb-4">
+									<span className="codicon codicon-warning text-vscode-charts-yellow text-2xl" />
+									<h3 className="text-lg font-medium text-vscode-foreground">
+										{t("worktrees:mergeConflicts")}
+									</h3>
+								</div>
+								<p className="text-sm text-vscode-descriptionForeground mb-2">
+									{t("worktrees:conflictsDescription")}
+								</p>
+								<div className="bg-vscode-input-background rounded p-2 mb-4 max-h-32 overflow-y-auto">
+									{mergeResult.conflictingFiles.map((file) => (
+										<div key={file} className="text-xs text-vscode-foreground font-mono">
+											{file}
+										</div>
+									))}
+								</div>
+								<div className="flex justify-end gap-2">
+									<Button
+										variant="secondary"
+										onClick={() => {
+											setMergeWorktree(null)
+											setMergeResult(null)
+										}}>
+										{t("worktrees:resolveManually")}
+									</Button>
+									<Button onClick={handleAskRooResolve}>{t("worktrees:askRooResolve")}</Button>
+								</div>
+							</>
+						) : (
+							<>
+								<div className="flex items-center gap-2 mb-4">
+									<span className="codicon codicon-error text-vscode-errorForeground text-2xl" />
+									<h3 className="text-lg font-medium text-vscode-foreground">
+										{t("worktrees:mergeFailed")}
+									</h3>
+								</div>
+								<p className="text-sm text-vscode-descriptionForeground mb-4">{mergeResult.message}</p>
+								<div className="flex justify-end">
+									<Button
+										onClick={() => {
+											setMergeWorktree(null)
+											setMergeResult(null)
+										}}>
+										{t("worktrees:close")}
+									</Button>
+								</div>
+							</>
+						)}
+					</div>
+				</div>
+			)}
+		</Tab>
+	)
+}

+ 3 - 0
webview-ui/src/components/worktrees/index.ts

@@ -0,0 +1,3 @@
+export { WorktreesView } from "./WorktreesView"
+export { CreateWorktreeModal } from "./CreateWorktreeModal"
+export { DeleteWorktreeModal } from "./DeleteWorktreeModal"

+ 67 - 0
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"
+}

+ 67 - 0
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"
+}

+ 67 - 0
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"
+}

+ 67 - 0
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"
+}

+ 67 - 0
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"
+}

+ 67 - 0
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": "बंद करें"
+}

+ 67 - 0
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"
+}

+ 67 - 0
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"
+}

+ 67 - 0
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": "閉じる"
+}

+ 67 - 0
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": "닫기"
+}

+ 67 - 0
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"
+}

+ 67 - 0
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"
+}

+ 67 - 0
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"
+}

+ 67 - 0
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": "Закрыть"
+}

+ 67 - 0
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"
+}

+ 67 - 0
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"
+}

+ 67 - 0
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": "关闭"
+}

+ 67 - 0
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": "關閉"
+}