mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-07 02:58:15 +00:00
* Fix EXT-553: Remove percentage-based progress tracking for worktree file copying - Removed totalBytes from CopyProgress interface - Removed Math.min() clamping that caused stuck-at-100% issue - Changed UI from progress bar to spinner with activity indicator - Shows 'item — X MB copied' instead of percentage - Updated all 18 locale files - Uses native cp with polling (no new dependencies) * fix: translate copyingProgress text in all 17 non-English locale files --------- Co-authored-by: Roo Code <roomote@roocode.com>
261 lines
8.4 KiB
TypeScript
261 lines
8.4 KiB
TypeScript
import { useState, useEffect, useCallback, useMemo } from "react"
|
|
import prettyBytes from "pretty-bytes"
|
|
|
|
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)
|
|
const [copyProgress, setCopyProgress] = useState<{
|
|
bytesCopied: number
|
|
itemName: 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 "worktreeCopyProgress": {
|
|
setCopyProgress({
|
|
bytesCopied: message.copyProgressBytesCopied ?? 0,
|
|
itemName: message.copyProgressItemName ?? "",
|
|
})
|
|
break
|
|
}
|
|
case "worktreeResult": {
|
|
setIsCreating(false)
|
|
setCopyProgress(null)
|
|
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>
|
|
)}
|
|
|
|
{/* Progress section - appears during file copying */}
|
|
{copyProgress && (
|
|
<div className="flex flex-col gap-2 px-3 py-3 rounded-lg bg-vscode-editor-background border border-vscode-panel-border">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<span className="codicon codicon-loading codicon-modifier-spin text-vscode-button-background" />
|
|
<span className="text-vscode-foreground font-medium">
|
|
{t("worktrees:copyingFiles")}
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-vscode-descriptionForeground truncate">
|
|
{t("worktrees:copyingProgress", {
|
|
item: copyProgress.itemName,
|
|
copied: prettyBytes(copyProgress.bytesCopied),
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="secondary" onClick={onClose} disabled={isCreating}>
|
|
{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>
|
|
)
|
|
}
|