CreateWorktreeModal.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import { useState, useEffect, useCallback, useMemo } from "react"
  2. import type { WorktreeDefaultsResponse, BranchInfo, WorktreeIncludeStatus } from "@roo-code/types"
  3. import { vscode } from "@/utils/vscode"
  4. import { useAppTranslation } from "@/i18n/TranslationContext"
  5. import {
  6. Dialog,
  7. DialogContent,
  8. DialogDescription,
  9. DialogFooter,
  10. DialogHeader,
  11. DialogTitle,
  12. Button,
  13. Input,
  14. } from "@/components/ui"
  15. import { SearchableSelect, type SearchableSelectOption } from "@/components/ui/searchable-select"
  16. interface CreateWorktreeModalProps {
  17. open: boolean
  18. onClose: () => void
  19. openAfterCreate?: boolean
  20. onSuccess?: () => void
  21. }
  22. export const CreateWorktreeModal = ({
  23. open,
  24. onClose,
  25. openAfterCreate = false,
  26. onSuccess,
  27. }: CreateWorktreeModalProps) => {
  28. const { t } = useAppTranslation()
  29. // Form state
  30. const [branchName, setBranchName] = useState("")
  31. const [worktreePath, setWorktreePath] = useState("")
  32. const [baseBranch, setBaseBranch] = useState("")
  33. // Data state
  34. const [defaults, setDefaults] = useState<WorktreeDefaultsResponse | null>(null)
  35. const [branches, setBranches] = useState<BranchInfo | null>(null)
  36. const [includeStatus, setIncludeStatus] = useState<WorktreeIncludeStatus | null>(null)
  37. // UI state
  38. const [isCreating, setIsCreating] = useState(false)
  39. const [error, setError] = useState<string | null>(null)
  40. // Fetch defaults and branches on open
  41. useEffect(() => {
  42. if (open) {
  43. vscode.postMessage({ type: "getWorktreeDefaults" })
  44. vscode.postMessage({ type: "getAvailableBranches" })
  45. vscode.postMessage({ type: "getWorktreeIncludeStatus" })
  46. }
  47. }, [open])
  48. // Handle messages from extension
  49. useEffect(() => {
  50. const handleMessage = (event: MessageEvent) => {
  51. const message = event.data
  52. switch (message.type) {
  53. case "worktreeDefaults": {
  54. const data = message as WorktreeDefaultsResponse
  55. setDefaults(data)
  56. setBranchName(data.suggestedBranch)
  57. setWorktreePath(data.suggestedPath)
  58. break
  59. }
  60. case "branchList": {
  61. const data = message as BranchInfo
  62. setBranches(data)
  63. setBaseBranch(data.currentBranch || "main")
  64. break
  65. }
  66. case "worktreeIncludeStatus": {
  67. setIncludeStatus(message.worktreeIncludeStatus)
  68. break
  69. }
  70. case "worktreeResult": {
  71. setIsCreating(false)
  72. if (message.success) {
  73. if (openAfterCreate) {
  74. vscode.postMessage({
  75. type: "switchWorktree",
  76. worktreePath: worktreePath,
  77. worktreeNewWindow: true,
  78. })
  79. }
  80. onSuccess?.()
  81. onClose()
  82. } else {
  83. setError(message.text || "Unknown error")
  84. }
  85. break
  86. }
  87. }
  88. }
  89. window.addEventListener("message", handleMessage)
  90. return () => window.removeEventListener("message", handleMessage)
  91. }, [openAfterCreate, worktreePath, onSuccess, onClose])
  92. const handleCreate = useCallback(() => {
  93. setError(null)
  94. setIsCreating(true)
  95. vscode.postMessage({
  96. type: "createWorktree",
  97. worktreePath: worktreePath,
  98. worktreeBranch: branchName,
  99. worktreeBaseBranch: baseBranch,
  100. worktreeCreateNewBranch: true,
  101. })
  102. }, [worktreePath, branchName, baseBranch])
  103. const isValid = branchName.trim() && worktreePath.trim() && baseBranch.trim()
  104. // Convert branches to SearchableSelect options format
  105. const branchOptions = useMemo((): SearchableSelectOption[] => {
  106. if (!branches) return []
  107. const localOptions: SearchableSelectOption[] = branches.localBranches.map((branch) => ({
  108. value: branch,
  109. label: branch,
  110. icon: <span className="codicon codicon-git-branch mr-2 text-vscode-descriptionForeground" />,
  111. }))
  112. const remoteOptions: SearchableSelectOption[] = branches.remoteBranches.map((branch) => ({
  113. value: branch,
  114. label: branch,
  115. icon: <span className="codicon codicon-cloud mr-2 text-vscode-descriptionForeground" />,
  116. }))
  117. return [...localOptions, ...remoteOptions]
  118. }, [branches])
  119. return (
  120. <Dialog open={open} onOpenChange={(isOpen: boolean) => !isOpen && onClose()}>
  121. <DialogContent className="max-w-lg">
  122. <DialogHeader>
  123. <DialogTitle>{t("worktrees:createWorktree")}</DialogTitle>
  124. <DialogDescription>{t("worktrees:createWorktreeDescription")}</DialogDescription>
  125. </DialogHeader>
  126. <div className="flex flex-col gap-3">
  127. {/* No .worktreeinclude warning - shows when the current worktree doesn't have .worktreeinclude */}
  128. {includeStatus?.exists === false && (
  129. <div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-vscode-inputValidation-warningBackground border border-vscode-inputValidation-warningBorder text-sm">
  130. <span className="codicon codicon-warning text-vscode-charts-yellow flex-shrink-0" />
  131. <span className="text-vscode-foreground">
  132. <span className="font-medium">{t("worktrees:noIncludeFileWarning")}</span>
  133. {" — "}
  134. <span className="text-vscode-descriptionForeground">
  135. {t("worktrees:noIncludeFileHint")}
  136. </span>
  137. </span>
  138. </div>
  139. )}
  140. {/* Branch name */}
  141. <div className="flex flex-col gap-1">
  142. <label className="text-sm text-vscode-foreground">{t("worktrees:branchName")}</label>
  143. <Input
  144. value={branchName}
  145. onChange={(e) => setBranchName(e.target.value)}
  146. placeholder={defaults?.suggestedBranch || "worktree/feature-name"}
  147. className="rounded-full"
  148. />
  149. </div>
  150. {/* Base branch selector */}
  151. <div className="flex flex-col gap-1">
  152. <label className="text-sm text-vscode-foreground">{t("worktrees:baseBranch")}</label>
  153. {!branches ? (
  154. <div className="flex items-center gap-2 h-8 px-2 text-sm text-vscode-descriptionForeground">
  155. <span className="codicon codicon-loading codicon-modifier-spin" />
  156. <span>{t("worktrees:loadingBranches")}</span>
  157. </div>
  158. ) : (
  159. <SearchableSelect
  160. value={baseBranch}
  161. onValueChange={setBaseBranch}
  162. options={branchOptions}
  163. placeholder={t("worktrees:selectBranch")}
  164. searchPlaceholder={t("worktrees:searchBranch")}
  165. emptyMessage={t("worktrees:noBranchFound")}
  166. />
  167. )}
  168. </div>
  169. {/* Worktree path */}
  170. <div className="flex flex-col gap-1">
  171. <label className="text-sm text-vscode-foreground">{t("worktrees:worktreePath")}</label>
  172. <Input
  173. value={worktreePath}
  174. onChange={(e) => setWorktreePath(e.target.value)}
  175. placeholder={defaults?.suggestedPath || "/path/to/worktree"}
  176. className="rounded-full"
  177. />
  178. <p className="text-xs text-vscode-descriptionForeground">{t("worktrees:pathHint")}</p>
  179. </div>
  180. {/* Error message */}
  181. {error && (
  182. <div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-vscode-inputValidation-errorBackground border border-vscode-inputValidation-errorBorder text-sm">
  183. <span className="codicon codicon-error text-vscode-errorForeground flex-shrink-0" />
  184. <p className="text-vscode-errorForeground">{error}</p>
  185. </div>
  186. )}
  187. </div>
  188. <DialogFooter>
  189. <Button variant="secondary" onClick={onClose}>
  190. {t("worktrees:cancel")}
  191. </Button>
  192. <Button onClick={handleCreate} disabled={!isValid || isCreating}>
  193. {isCreating ? (
  194. <>
  195. <span className="codicon codicon-loading codicon-modifier-spin mr-2" />
  196. {t("worktrees:creating")}
  197. </>
  198. ) : (
  199. t("worktrees:create")
  200. )}
  201. </Button>
  202. </DialogFooter>
  203. </DialogContent>
  204. </Dialog>
  205. )
  206. }