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