mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
ead1658441
commit
04256be956
68 changed files with 4287 additions and 16 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"@roo-code/types": "workspace:^",
|
||||
"esbuild": "^0.25.0",
|
||||
"execa": "^9.5.2",
|
||||
"ignore": "^7.0.3",
|
||||
"openai": "^5.12.2",
|
||||
"zod": "^3.25.61"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./custom-tools/index.js"
|
||||
export * from "./debug-log/index.js"
|
||||
export * from "./message-utils/index.js"
|
||||
export * from "./worktree/index.js"
|
||||
|
|
|
|||
268
packages/core/src/worktree/__tests__/worktree-include.spec.ts
Normal file
268
packages/core/src/worktree/__tests__/worktree-include.spec.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
146
packages/core/src/worktree/__tests__/worktree-service.spec.ts
Normal file
146
packages/core/src/worktree/__tests__/worktree-service.spec.ts
Normal 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
13
packages/core/src/worktree/index.ts
Normal file
13
packages/core/src/worktree/index.ts
Normal 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"
|
||||
17
packages/core/src/worktree/types.ts
Normal file
17
packages/core/src/worktree/types.ts
Normal 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"
|
||||
256
packages/core/src/worktree/worktree-include.ts
Normal file
256
packages/core/src/worktree/worktree-include.ts
Normal 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()
|
||||
444
packages/core/src/worktree/worktree-service.ts
Normal file
444
packages/core/src/worktree/worktree-service.ts
Normal 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()
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export const commandIds = [
|
|||
"popoutButtonClicked",
|
||||
"cloudButtonClicked",
|
||||
"settingsButtonClicked",
|
||||
"worktreesButtonClicked",
|
||||
|
||||
"openInNewTab",
|
||||
|
||||
|
|
|
|||
129
packages/types/src/worktree.ts
Normal file
129
packages/types/src/worktree.ts
Normal 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
73
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -66,6 +66,19 @@ const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
|
|||
|
||||
import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace"
|
||||
import { setPendingTodoList } from "../tools/UpdateTodoListTool"
|
||||
import {
|
||||
handleListWorktrees,
|
||||
handleCreateWorktree,
|
||||
handleDeleteWorktree,
|
||||
handleSwitchWorktree,
|
||||
handleGetAvailableBranches,
|
||||
handleGetWorktreeDefaults,
|
||||
handleGetWorktreeIncludeStatus,
|
||||
handleCheckBranchWorktreeInclude,
|
||||
handleCreateWorktreeInclude,
|
||||
handleCheckoutBranch,
|
||||
handleMergeWorktree,
|
||||
} from "./worktree"
|
||||
|
||||
export const webviewMessageHandler = async (
|
||||
provider: ClineProvider,
|
||||
|
|
@ -3389,6 +3402,247 @@ export const webviewMessageHandler = async (
|
|||
break
|
||||
}
|
||||
|
||||
/**
|
||||
* Git Worktree Management
|
||||
*/
|
||||
|
||||
case "listWorktrees": {
|
||||
try {
|
||||
const { worktrees, isGitRepo, isMultiRoot, isSubfolder, gitRootPath, error } =
|
||||
await handleListWorktrees(provider)
|
||||
|
||||
await provider.postMessageToWebview({
|
||||
type: "worktreeList",
|
||||
worktrees,
|
||||
isGitRepo,
|
||||
isMultiRoot,
|
||||
isSubfolder,
|
||||
gitRootPath,
|
||||
error,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
await provider.postMessageToWebview({
|
||||
type: "worktreeList",
|
||||
worktrees: [],
|
||||
isGitRepo: false,
|
||||
isMultiRoot: false,
|
||||
isSubfolder: false,
|
||||
gitRootPath: "",
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "createWorktree": {
|
||||
try {
|
||||
const { success, message: text } = await handleCreateWorktree(provider, {
|
||||
path: message.worktreePath!,
|
||||
branch: message.worktreeBranch,
|
||||
baseBranch: message.worktreeBaseBranch,
|
||||
createNewBranch: message.worktreeCreateNewBranch,
|
||||
})
|
||||
|
||||
await provider.postMessageToWebview({ type: "worktreeResult", success, text })
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
await provider.postMessageToWebview({ type: "worktreeResult", success: false, text: errorMessage })
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "deleteWorktree": {
|
||||
try {
|
||||
const { success, message: text } = await handleDeleteWorktree(
|
||||
provider,
|
||||
message.worktreePath!,
|
||||
message.worktreeForce ?? false,
|
||||
)
|
||||
|
||||
await provider.postMessageToWebview({ type: "worktreeResult", success, text })
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
await provider.postMessageToWebview({ type: "worktreeResult", success: false, text: errorMessage })
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "switchWorktree": {
|
||||
try {
|
||||
const { success, message: text } = await handleSwitchWorktree(
|
||||
provider,
|
||||
message.worktreePath!,
|
||||
message.worktreeNewWindow ?? true,
|
||||
)
|
||||
|
||||
await provider.postMessageToWebview({ type: "worktreeResult", success, text })
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
await provider.postMessageToWebview({ type: "worktreeResult", success: false, text: errorMessage })
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "getAvailableBranches": {
|
||||
try {
|
||||
const { localBranches, remoteBranches, currentBranch } = await handleGetAvailableBranches(provider)
|
||||
|
||||
await provider.postMessageToWebview({
|
||||
type: "branchList",
|
||||
localBranches,
|
||||
remoteBranches,
|
||||
currentBranch,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
await provider.postMessageToWebview({
|
||||
type: "branchList",
|
||||
localBranches: [],
|
||||
remoteBranches: [],
|
||||
currentBranch: "",
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "getWorktreeDefaults": {
|
||||
try {
|
||||
const { suggestedBranch, suggestedPath } = await handleGetWorktreeDefaults(provider)
|
||||
await provider.postMessageToWebview({ type: "worktreeDefaults", suggestedBranch, suggestedPath })
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
await provider.postMessageToWebview({
|
||||
type: "worktreeDefaults",
|
||||
suggestedBranch: "",
|
||||
suggestedPath: "",
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "getWorktreeIncludeStatus": {
|
||||
try {
|
||||
const worktreeIncludeStatus = await handleGetWorktreeIncludeStatus(provider)
|
||||
await provider.postMessageToWebview({ type: "worktreeIncludeStatus", worktreeIncludeStatus })
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
await provider.postMessageToWebview({
|
||||
type: "worktreeIncludeStatus",
|
||||
worktreeIncludeStatus: {
|
||||
exists: false,
|
||||
hasGitignore: false,
|
||||
gitignoreContent: undefined,
|
||||
},
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "checkBranchWorktreeInclude": {
|
||||
try {
|
||||
const branch = message.worktreeBranch
|
||||
if (!branch) {
|
||||
await provider.postMessageToWebview({
|
||||
type: "branchWorktreeIncludeResult",
|
||||
hasWorktreeInclude: false,
|
||||
error: "No branch specified",
|
||||
})
|
||||
break
|
||||
}
|
||||
const hasWorktreeInclude = await handleCheckBranchWorktreeInclude(provider, branch)
|
||||
await provider.postMessageToWebview({
|
||||
type: "branchWorktreeIncludeResult",
|
||||
branch,
|
||||
hasWorktreeInclude,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
await provider.postMessageToWebview({
|
||||
type: "branchWorktreeIncludeResult",
|
||||
hasWorktreeInclude: false,
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "createWorktreeInclude": {
|
||||
try {
|
||||
const { success, message: text } = await handleCreateWorktreeInclude(
|
||||
provider,
|
||||
message.worktreeIncludeContent ?? "",
|
||||
)
|
||||
|
||||
await provider.postMessageToWebview({ type: "worktreeResult", success, text })
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
provider.log(`Error creating worktree include: ${errorMessage}`)
|
||||
await provider.postMessageToWebview({ type: "worktreeResult", success: false, text: errorMessage })
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "checkoutBranch": {
|
||||
try {
|
||||
const { success, message: text } = await handleCheckoutBranch(provider, message.worktreeBranch!)
|
||||
await provider.postMessageToWebview({ type: "worktreeResult", success, text })
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
await provider.postMessageToWebview({ type: "worktreeResult", success: false, text: errorMessage })
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "mergeWorktree": {
|
||||
try {
|
||||
const result = await handleMergeWorktree(provider, {
|
||||
worktreePath: message.worktreePath!,
|
||||
targetBranch: message.worktreeTargetBranch!,
|
||||
deleteAfterMerge: message.worktreeDeleteAfterMerge,
|
||||
})
|
||||
|
||||
await provider.postMessageToWebview({
|
||||
type: "mergeWorktreeResult",
|
||||
success: result.success,
|
||||
text: result.message,
|
||||
hasConflicts: result.hasConflicts,
|
||||
conflictingFiles: result.conflictingFiles,
|
||||
sourceBranch: result.sourceBranch,
|
||||
targetBranch: result.targetBranch,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
await provider.postMessageToWebview({
|
||||
type: "mergeWorktreeResult",
|
||||
success: false,
|
||||
text: errorMessage,
|
||||
hasConflicts: false,
|
||||
conflictingFiles: [],
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
// console.log(`Unhandled message type: ${message.type}`)
|
||||
//
|
||||
|
|
|
|||
277
src/core/webview/worktree/handlers.ts
Normal file
277
src/core/webview/worktree/handlers.ts
Normal 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)
|
||||
}
|
||||
23
src/core/webview/worktree/index.ts
Normal file
23
src/core/webview/worktree/index.ts
Normal 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"
|
||||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,11 @@
|
|||
"title": "%command.settings.title%",
|
||||
"icon": "$(settings-gear)"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.worktreesButtonClicked",
|
||||
"title": "%command.worktrees.title%",
|
||||
"icon": "$(git-branch)"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.openInNewTab",
|
||||
"title": "%command.openInNewTab.title%",
|
||||
|
|
@ -239,9 +244,14 @@
|
|||
"when": "view == roo-cline.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.popoutButtonClicked",
|
||||
"command": "roo-cline.worktreesButtonClicked",
|
||||
"group": "overflow@2",
|
||||
"when": "view == roo-cline.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.popoutButtonClicked",
|
||||
"group": "overflow@3",
|
||||
"when": "view == roo-cline.SidebarProvider"
|
||||
}
|
||||
],
|
||||
"editor/title": [
|
||||
|
|
@ -271,9 +281,14 @@
|
|||
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.popoutButtonClicked",
|
||||
"command": "roo-cline.worktreesButtonClicked",
|
||||
"group": "overflow@2",
|
||||
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.popoutButtonClicked",
|
||||
"group": "overflow@3",
|
||||
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
1
src/package.nls.ca.json
generated
1
src/package.nls.ca.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "Obrir a l'Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Configuració",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Documentació",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "Ordres que es poden executar automàticament quan 'Aprova sempre les operacions d'execució' està activat",
|
||||
|
|
|
|||
1
src/package.nls.de.json
generated
1
src/package.nls.de.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "Im Editor Öffnen",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Einstellungen",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Dokumentation",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "Befehle, die automatisch ausgeführt werden können, wenn 'Ausführungsoperationen immer genehmigen' aktiviert ist",
|
||||
|
|
|
|||
1
src/package.nls.es.json
generated
1
src/package.nls.es.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "Abrir en Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Configuración",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Documentación",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "Comandos que pueden ejecutarse automáticamente cuando 'Aprobar siempre operaciones de ejecución' está activado",
|
||||
|
|
|
|||
1
src/package.nls.fr.json
generated
1
src/package.nls.fr.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "Ouvrir dans l'Éditeur",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Paramètres",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Documentation",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "Commandes pouvant être exécutées automatiquement lorsque 'Toujours approuver les opérations d'exécution' est activé",
|
||||
|
|
|
|||
1
src/package.nls.hi.json
generated
1
src/package.nls.hi.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "एडिटर में खोलें",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "सेटिंग्स",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "दस्तावेज़ीकरण",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "वे कमांड जो स्वचालित रूप से निष्पादित की जा सकती हैं जब 'हमेशा निष्पादन संचालन को स्वीकृत करें' सक्रिय हो",
|
||||
|
|
|
|||
1
src/package.nls.id.json
generated
1
src/package.nls.id.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"command.openInEditor.title": "Buka di Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Pengaturan",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Dokumentasi",
|
||||
"command.openInNewTab.title": "Buka di Tab Baru",
|
||||
"command.explainCode.title": "Jelaskan Kode",
|
||||
|
|
|
|||
1
src/package.nls.it.json
generated
1
src/package.nls.it.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "Apri nell'Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Impostazioni",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Documentazione",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "Comandi che possono essere eseguiti automaticamente quando 'Approva sempre le operazioni di esecuzione' è attivato",
|
||||
|
|
|
|||
1
src/package.nls.ja.json
generated
1
src/package.nls.ja.json
generated
|
|
@ -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": "コードの説明",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"command.openInEditor.title": "Open in Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Settings",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Documentation",
|
||||
"command.openInNewTab.title": "Open In New Tab",
|
||||
"command.explainCode.title": "Explain Code",
|
||||
|
|
|
|||
1
src/package.nls.ko.json
generated
1
src/package.nls.ko.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "에디터에서 열기",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "설정",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "문서",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "'항상 실행 작업 승인' 이 활성화되어 있을 때 자동으로 실행할 수 있는 명령어",
|
||||
|
|
|
|||
1
src/package.nls.nl.json
generated
1
src/package.nls.nl.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"command.openInEditor.title": "Openen in Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Instellingen",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Documentatie",
|
||||
"command.openInNewTab.title": "Openen in Nieuw Tabblad",
|
||||
"command.explainCode.title": "Leg Code Uit",
|
||||
|
|
|
|||
1
src/package.nls.pl.json
generated
1
src/package.nls.pl.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "Otwórz w Edytorze",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Ustawienia",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Dokumentacja",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "Polecenia, które mogą być wykonywane automatycznie, gdy włączona jest opcja 'Zawsze zatwierdzaj operacje wykonania'",
|
||||
|
|
|
|||
1
src/package.nls.pt-BR.json
generated
1
src/package.nls.pt-BR.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "Abrir no Editor",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Configurações",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Documentação",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "Comandos que podem ser executados automaticamente quando 'Sempre aprovar operações de execução' está ativado",
|
||||
|
|
|
|||
1
src/package.nls.ru.json
generated
1
src/package.nls.ru.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"command.openInEditor.title": "Открыть в редакторе",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Настройки",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Документация",
|
||||
"command.openInNewTab.title": "Открыть в новой вкладке",
|
||||
"command.explainCode.title": "Объяснить код",
|
||||
|
|
|
|||
1
src/package.nls.tr.json
generated
1
src/package.nls.tr.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "Düzenleyicide Aç",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Ayarlar",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Dokümantasyon",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "'Her zaman yürütme işlemlerini onayla' etkinleştirildiğinde otomatik olarak yürütülebilen komutlar",
|
||||
|
|
|
|||
1
src/package.nls.vi.json
generated
1
src/package.nls.vi.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "Mở trong Trình Soạn Thảo",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "Cài Đặt",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "Tài Liệu",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "Các lệnh có thể được thực thi tự động khi 'Luôn phê duyệt các thao tác thực thi' được bật",
|
||||
|
|
|
|||
1
src/package.nls.zh-CN.json
generated
1
src/package.nls.zh-CN.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"command.openInEditor.title": "在编辑器中打开",
|
||||
"command.cloud.title": "Cloud",
|
||||
"command.settings.title": "设置",
|
||||
"command.worktrees.title": "Worktrees",
|
||||
"command.documentation.title": "文档",
|
||||
"configuration.title": "Roo Code",
|
||||
"commands.allowedCommands.description": "当启用'始终批准执行操作'时可以自动执行的命令",
|
||||
|
|
|
|||
1
src/package.nls.zh-TW.json
generated
1
src/package.nls.zh-TW.json
generated
|
|
@ -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": "當啟用'始終批准執行操作'時可以自動執行的命令",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export * from "./dropdown-menu"
|
|||
export * from "./input"
|
||||
export * from "./popover"
|
||||
export * from "./progress"
|
||||
export * from "./radio-group"
|
||||
export * from "./searchable-select"
|
||||
export * from "./separator"
|
||||
export * from "./slider"
|
||||
|
|
|
|||
35
webview-ui/src/components/ui/radio-group.tsx
Normal file
35
webview-ui/src/components/ui/radio-group.tsx
Normal 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 }
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,10 @@ function SelectLabel({ className, ...props }: React.ComponentProps<typeof Select
|
|||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("px-2 py-1.5 text-sm font-medium", className)}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-xs font-semibold text-vscode-descriptionForeground uppercase tracking-wide",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
230
webview-ui/src/components/worktrees/CreateWorktreeModal.tsx
Normal file
230
webview-ui/src/components/worktrees/CreateWorktreeModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
142
webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx
Normal file
142
webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
526
webview-ui/src/components/worktrees/WorktreesView.tsx
Normal file
526
webview-ui/src/components/worktrees/WorktreesView.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
3
webview-ui/src/components/worktrees/index.ts
Normal file
3
webview-ui/src/components/worktrees/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { WorktreesView } from "./WorktreesView"
|
||||
export { CreateWorktreeModal } from "./CreateWorktreeModal"
|
||||
export { DeleteWorktreeModal } from "./DeleteWorktreeModal"
|
||||
67
webview-ui/src/i18n/locales/ca/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/ca/worktrees.json
generated
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/de/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/de/worktrees.json
generated
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/en/worktrees.json
Normal file
67
webview-ui/src/i18n/locales/en/worktrees.json
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/es/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/es/worktrees.json
generated
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/fr/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/fr/worktrees.json
generated
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/hi/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/hi/worktrees.json
generated
Normal 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": "बंद करें"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/id/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/id/worktrees.json
generated
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/it/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/it/worktrees.json
generated
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/ja/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/ja/worktrees.json
generated
Normal 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": "閉じる"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/ko/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/ko/worktrees.json
generated
Normal 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": "닫기"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/nl/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/nl/worktrees.json
generated
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/pl/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/pl/worktrees.json
generated
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/pt-BR/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/pt-BR/worktrees.json
generated
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/ru/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/ru/worktrees.json
generated
Normal 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": "Закрыть"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/tr/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/tr/worktrees.json
generated
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/vi/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/vi/worktrees.json
generated
Normal 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"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/zh-CN/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/zh-CN/worktrees.json
generated
Normal 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": "关闭"
|
||||
}
|
||||
67
webview-ui/src/i18n/locales/zh-TW/worktrees.json
generated
Normal file
67
webview-ui/src/i18n/locales/zh-TW/worktrees.json
generated
Normal 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": "關閉"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue