Git worktree management (#10458)

Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: Hannes Rudolph <hrudolph@gmail.com>
Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
This commit is contained in:
Chris Estreich 2026-01-20 13:39:24 -08:00 committed by GitHub
parent ead1658441
commit 04256be956
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 4287 additions and 16 deletions

View file

@ -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",

View file

@ -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"
},

View file

@ -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"

View file

@ -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", "test@example.com"])
await fs.writeFile(path.join(repoDir, "README.md"), "test")
await execGit(repoDir, ["add", "README.md"])
await execGit(repoDir, ["commit", "-m", "init"])
const baseBranch = (await execGit(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"])).trim()
expect(await service.branchHasWorktreeInclude(repoDir, baseBranch)).toBe(false)
await execGit(repoDir, ["checkout", "-b", "with-include"])
await fs.writeFile(path.join(repoDir, ".worktreeinclude"), "node_modules")
await execGit(repoDir, ["add", ".worktreeinclude"])
await execGit(repoDir, ["commit", "-m", "add include"])
expect(await service.branchHasWorktreeInclude(repoDir, "with-include")).toBe(true)
}, 30_000)
})
describe("getStatus", () => {
it("should return correct status when both files exist", async () => {
const gitignoreContent = "node_modules\n.env\ndist"
await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "node_modules")
await fs.writeFile(path.join(tempDir, ".gitignore"), gitignoreContent)
const result = await service.getStatus(tempDir)
expect(result.exists).toBe(true)
expect(result.hasGitignore).toBe(true)
expect(result.gitignoreContent).toBe(gitignoreContent)
})
it("should return correct status when only .gitignore exists", async () => {
const gitignoreContent = "node_modules\n.env"
await fs.writeFile(path.join(tempDir, ".gitignore"), gitignoreContent)
const result = await service.getStatus(tempDir)
expect(result.exists).toBe(false)
expect(result.hasGitignore).toBe(true)
expect(result.gitignoreContent).toBe(gitignoreContent)
})
it("should return correct status when only .worktreeinclude exists", async () => {
await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "node_modules")
const result = await service.getStatus(tempDir)
expect(result.exists).toBe(true)
expect(result.hasGitignore).toBe(false)
expect(result.gitignoreContent).toBeUndefined()
})
it("should return correct status when neither file exists", async () => {
const result = await service.getStatus(tempDir)
expect(result.exists).toBe(false)
expect(result.hasGitignore).toBe(false)
expect(result.gitignoreContent).toBeUndefined()
})
})
describe("createWorktreeInclude", () => {
it("should create .worktreeinclude file with specified content", async () => {
const content = "node_modules\n.env\ndist"
await service.createWorktreeInclude(tempDir, content)
const fileContent = await fs.readFile(path.join(tempDir, ".worktreeinclude"), "utf-8")
expect(fileContent).toBe(content)
})
it("should overwrite existing .worktreeinclude file", async () => {
await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "old content")
const newContent = "new content"
await service.createWorktreeInclude(tempDir, newContent)
const fileContent = await fs.readFile(path.join(tempDir, ".worktreeinclude"), "utf-8")
expect(fileContent).toBe(newContent)
})
})
describe("copyWorktreeIncludeFiles", () => {
let sourceDir: string
let targetDir: string
beforeEach(async () => {
sourceDir = path.join(tempDir, "source")
targetDir = path.join(tempDir, "target")
await fs.mkdir(sourceDir, { recursive: true })
await fs.mkdir(targetDir, { recursive: true })
})
it("should return empty array when no .worktreeinclude exists", async () => {
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toEqual([])
})
it("should return empty array when no .gitignore exists", async () => {
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toEqual([])
})
it("should return empty array when patterns do not match", async () => {
// .worktreeinclude wants node_modules, .gitignore only ignores .env
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
await fs.writeFile(path.join(sourceDir, ".gitignore"), ".env")
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toEqual([])
})
it("should copy files that match both patterns", async () => {
// Both files include node_modules
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
// Create a file in node_modules
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
await fs.writeFile(path.join(sourceDir, "node_modules", "package.json"), '{"name": "test"}')
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toContain("node_modules")
// Verify the file was copied
const copiedContent = await fs.readFile(path.join(targetDir, "node_modules", "package.json"), "utf-8")
expect(copiedContent).toBe('{"name": "test"}')
})
it("should only copy intersection of patterns", async () => {
// .worktreeinclude: node_modules, dist
// .gitignore: node_modules, .env
// Only node_modules should be copied (intersection)
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules\ndist")
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules\n.env")
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
await fs.mkdir(path.join(sourceDir, "dist"), { recursive: true })
await fs.writeFile(path.join(sourceDir, ".env"), "SECRET=123")
await fs.writeFile(path.join(sourceDir, "node_modules", "test.txt"), "test")
await fs.writeFile(path.join(sourceDir, "dist", "main.js"), "console.log('dist')")
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
// Only node_modules should be in the result (matches both)
expect(result).toContain("node_modules")
expect(result).not.toContain("dist") // only in .worktreeinclude
expect(result).not.toContain(".env") // only in .gitignore
// Verify node_modules was copied
const nodeModulesExists = await fs
.access(path.join(targetDir, "node_modules"))
.then(() => true)
.catch(() => false)
expect(nodeModulesExists).toBe(true)
// Verify dist was NOT copied
const distExists = await fs
.access(path.join(targetDir, "dist"))
.then(() => true)
.catch(() => false)
expect(distExists).toBe(false)
})
it("should skip .git directory", async () => {
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), ".git")
await fs.writeFile(path.join(sourceDir, ".gitignore"), ".git")
await fs.mkdir(path.join(sourceDir, ".git"), { recursive: true })
await fs.writeFile(path.join(sourceDir, ".git", "config"), "[core]")
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).not.toContain(".git")
})
it("should copy single files", async () => {
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), ".env.local")
await fs.writeFile(path.join(sourceDir, ".gitignore"), ".env.local")
await fs.writeFile(path.join(sourceDir, ".env.local"), "LOCAL_VAR=value")
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toContain(".env.local")
const copiedContent = await fs.readFile(path.join(targetDir, ".env.local"), "utf-8")
expect(copiedContent).toBe("LOCAL_VAR=value")
})
it("should ignore comment lines in pattern files", async () => {
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "# comment\nnode_modules\n# another comment")
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toContain("node_modules")
})
})
})

View file

@ -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,
})
})
})
})

View file

@ -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"

View file

@ -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"

View file

@ -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()

View file

@ -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()

View file

@ -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>

View file

@ -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"

View file

@ -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 {

View file

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

View file

@ -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
pnpm-lock.yaml generated
View file

@ -103,6 +103,12 @@ importers:
commander:
specifier: ^12.1.0
version: 12.1.0
cross-spawn:
specifier: ^7.0.6
version: 7.0.6
execa:
specifier: ^9.5.2
version: 9.6.0
fuzzysort:
specifier: ^3.1.0
version: 3.1.0
@ -551,6 +557,9 @@ importers:
execa:
specifier: ^9.5.2
version: 9.6.0
ignore:
specifier: ^7.0.3
version: 7.0.5
openai:
specifier: ^5.12.2
version: 5.12.2(ws@8.18.3)(zod@3.25.76)
@ -1115,6 +1124,9 @@ importers:
'@radix-ui/react-progress':
specifier: ^1.1.2
version: 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-radio-group':
specifier: ^1.3.8
version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-select':
specifier: ^2.1.6
version: 2.2.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -3074,6 +3086,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-radio-group@1.3.8':
resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==}
peerDependencies:
'@types/react': ^18.3.23
'@types/react-dom': ^18.3.5
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.10':
resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==}
peerDependencies:
@ -3087,6 +3112,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.11':
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
peerDependencies:
'@types/react': ^18.3.23
'@types/react-dom': ^18.3.5
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.9':
resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==}
peerDependencies:
@ -12862,6 +12900,24 @@ snapshots:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.2
@ -12879,6 +12935,23 @@ snapshots:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-roving-focus@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.2

View file

@ -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")

View file

@ -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}`)
//

View file

@ -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)
}

View file

@ -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"

View file

@ -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, {

View file

@ -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"
}
]
},

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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é",

View file

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

View file

@ -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",

View file

@ -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",

View file

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

View file

@ -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",

View file

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

View file

@ -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",

View file

@ -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'",

View file

@ -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",

View file

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

View file

@ -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",

View file

@ -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",

View file

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

View file

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

View file

@ -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",

View file

@ -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"}

View file

@ -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

View file

@ -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"

View file

@ -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 }

View file

@ -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}

View file

@ -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}
/>
)

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

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

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

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

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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": "閉じる"
}

View file

@ -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": "닫기"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

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

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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": "关闭"
}

View file

@ -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": "關閉"
}