CheckpointMenu.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { useState, useCallback } from "react"
  2. import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"
  3. import { useTranslation } from "react-i18next"
  4. import { Button, Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
  5. import { useRooPortal } from "@/components/ui/hooks"
  6. import { vscode } from "@src/utils/vscode"
  7. import { Checkpoint } from "./schema"
  8. type CheckpointMenuBaseProps = {
  9. ts: number
  10. commitHash: string
  11. currentHash?: string
  12. checkpoint: Checkpoint
  13. }
  14. type CheckpointMenuControlledProps = {
  15. open: boolean
  16. onOpenChange: (open: boolean) => void
  17. }
  18. type CheckpointMenuUncontrolledProps = {
  19. open?: undefined
  20. onOpenChange?: undefined
  21. }
  22. type CheckpointMenuProps = CheckpointMenuBaseProps & (CheckpointMenuControlledProps | CheckpointMenuUncontrolledProps)
  23. export const CheckpointMenu = ({
  24. ts,
  25. commitHash,
  26. currentHash,
  27. checkpoint,
  28. open,
  29. onOpenChange,
  30. }: CheckpointMenuProps) => {
  31. const { t } = useTranslation()
  32. const [internalOpen, setInternalOpen] = useState(false)
  33. const [isConfirming, setIsConfirming] = useState(false)
  34. const portalContainer = useRooPortal("roo-portal")
  35. const isCurrent = currentHash === commitHash
  36. const previousCommitHash = checkpoint?.from
  37. const isOpen = open ?? internalOpen
  38. const setOpen = onOpenChange ?? setInternalOpen
  39. const onCheckpointDiff = useCallback(() => {
  40. vscode.postMessage({
  41. type: "checkpointDiff",
  42. payload: { ts, previousCommitHash, commitHash, mode: "checkpoint" },
  43. })
  44. }, [ts, previousCommitHash, commitHash])
  45. const onPreview = useCallback(() => {
  46. vscode.postMessage({ type: "checkpointRestore", payload: { ts, commitHash, mode: "preview" } })
  47. setOpen(false)
  48. }, [ts, commitHash, setOpen])
  49. const onRestore = useCallback(() => {
  50. vscode.postMessage({ type: "checkpointRestore", payload: { ts, commitHash, mode: "restore" } })
  51. setOpen(false)
  52. }, [ts, commitHash, setOpen])
  53. const handleOpenChange = useCallback(
  54. (open: boolean) => {
  55. setOpen(open)
  56. if (!open) {
  57. setIsConfirming(false)
  58. }
  59. },
  60. [setOpen],
  61. )
  62. return (
  63. <div className="flex flex-row gap-1">
  64. <StandardTooltip content={t("chat:checkpoint.menu.viewDiff")}>
  65. <Button variant="ghost" size="icon" onClick={onCheckpointDiff}>
  66. <span className="codicon codicon-diff-single" />
  67. </Button>
  68. </StandardTooltip>
  69. <Popover open={isOpen} onOpenChange={handleOpenChange}>
  70. <StandardTooltip content={t("chat:checkpoint.menu.restore")}>
  71. <PopoverTrigger asChild>
  72. <Button variant="ghost" size="icon" aria-label={t("chat:checkpoint.menu.restore")}>
  73. <span className="codicon codicon-history" />
  74. </Button>
  75. </PopoverTrigger>
  76. </StandardTooltip>
  77. <PopoverContent align="end" container={portalContainer}>
  78. <div className="flex flex-col gap-2">
  79. {!isCurrent && (
  80. <div className="flex flex-col gap-1 group hover:text-foreground">
  81. <Button variant="secondary" onClick={onPreview} data-testid="restore-files-btn">
  82. {t("chat:checkpoint.menu.restoreFiles")}
  83. </Button>
  84. <div className="text-muted transition-colors group-hover:text-foreground">
  85. {t("chat:checkpoint.menu.restoreFilesDescription")}
  86. </div>
  87. </div>
  88. )}
  89. {!isCurrent && (
  90. <div className="flex flex-col gap-1 group hover:text-foreground">
  91. <div className="flex flex-col gap-1 group hover:text-foreground">
  92. {!isConfirming ? (
  93. <Button
  94. variant="secondary"
  95. onClick={() => setIsConfirming(true)}
  96. data-testid="restore-files-and-task-btn">
  97. {t("chat:checkpoint.menu.restoreFilesAndTask")}
  98. </Button>
  99. ) : (
  100. <>
  101. <Button
  102. variant="default"
  103. onClick={onRestore}
  104. className="grow"
  105. data-testid="confirm-restore-btn">
  106. <div className="flex flex-row gap-1">
  107. <CheckIcon />
  108. <div>{t("chat:checkpoint.menu.confirm")}</div>
  109. </div>
  110. </Button>
  111. <Button variant="secondary" onClick={() => setIsConfirming(false)}>
  112. <div className="flex flex-row gap-1">
  113. <Cross2Icon />
  114. <div>{t("chat:checkpoint.menu.cancel")}</div>
  115. </div>
  116. </Button>
  117. </>
  118. )}
  119. {isConfirming ? (
  120. <div
  121. data-testid="checkpoint-confirm-warning"
  122. className="text-destructive font-bold">
  123. {t("chat:checkpoint.menu.cannotUndo")}
  124. </div>
  125. ) : (
  126. <div className="text-muted transition-colors group-hover:text-foreground">
  127. {t("chat:checkpoint.menu.restoreFilesAndTaskDescription")}
  128. </div>
  129. )}
  130. </div>
  131. </div>
  132. )}
  133. </div>
  134. </PopoverContent>
  135. </Popover>
  136. </div>
  137. )
  138. }