mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-24 00:51:34 +00:00
162 lines
5.7 KiB
TypeScript
162 lines
5.7 KiB
TypeScript
import { useCallback, useEffect, useState } from "react"
|
|
import { ClipboardCopy, Timer } from "lucide-react"
|
|
|
|
import { Button, StandardTooltip } from "@/components/ui"
|
|
|
|
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
|
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
|
import { SuggestionItem } from "@roo-code/types"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const DEFAULT_FOLLOWUP_TIMEOUT_MS = 60000
|
|
const COUNTDOWN_INTERVAL_MS = 1000
|
|
|
|
interface FollowUpSuggestProps {
|
|
suggestions?: SuggestionItem[]
|
|
onSuggestionClick?: (suggestion: SuggestionItem, event?: React.MouseEvent) => void
|
|
ts: number
|
|
onCancelAutoApproval?: () => void
|
|
isAnswered?: boolean
|
|
isFollowUpAutoApprovalPaused?: boolean
|
|
}
|
|
|
|
export const FollowUpSuggest = ({
|
|
suggestions = [],
|
|
onSuggestionClick,
|
|
ts = 1,
|
|
onCancelAutoApproval,
|
|
isAnswered = false,
|
|
isFollowUpAutoApprovalPaused = false,
|
|
}: FollowUpSuggestProps) => {
|
|
const { autoApprovalEnabled, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs } = useExtensionState()
|
|
const [countdown, setCountdown] = useState<number | null>(null)
|
|
const [suggestionSelected, setSuggestionSelected] = useState(false)
|
|
const { t } = useAppTranslation()
|
|
|
|
// Start countdown timer when auto-approval is enabled for follow-up questions
|
|
useEffect(() => {
|
|
// Only start countdown if auto-approval is enabled for follow-up questions and no suggestion has been selected
|
|
// Also stop countdown if the question has been answered or auto-approval is paused (user is typing)
|
|
if (
|
|
autoApprovalEnabled &&
|
|
alwaysAllowFollowupQuestions &&
|
|
suggestions.length > 0 &&
|
|
!suggestionSelected &&
|
|
!isAnswered &&
|
|
!isFollowUpAutoApprovalPaused
|
|
) {
|
|
// Start with the configured timeout in seconds
|
|
const timeoutMs =
|
|
typeof followupAutoApproveTimeoutMs === "number" && !isNaN(followupAutoApproveTimeoutMs)
|
|
? followupAutoApproveTimeoutMs
|
|
: DEFAULT_FOLLOWUP_TIMEOUT_MS
|
|
|
|
// Convert milliseconds to seconds for the countdown
|
|
setCountdown(Math.floor(timeoutMs / 1000))
|
|
|
|
// Update countdown every second
|
|
const intervalId = setInterval(() => {
|
|
setCountdown((prevCountdown) => {
|
|
if (prevCountdown === null || prevCountdown <= 1) {
|
|
clearInterval(intervalId)
|
|
return null
|
|
}
|
|
return prevCountdown - 1
|
|
})
|
|
}, COUNTDOWN_INTERVAL_MS)
|
|
|
|
// Clean up interval on unmount and notify parent component
|
|
return () => {
|
|
clearInterval(intervalId)
|
|
// Notify parent component that this component is unmounting
|
|
// so it can clear any related timeouts
|
|
onCancelAutoApproval?.()
|
|
}
|
|
} else {
|
|
setCountdown(null)
|
|
}
|
|
}, [
|
|
autoApprovalEnabled,
|
|
alwaysAllowFollowupQuestions,
|
|
suggestions,
|
|
followupAutoApproveTimeoutMs,
|
|
suggestionSelected,
|
|
onCancelAutoApproval,
|
|
isAnswered,
|
|
isFollowUpAutoApprovalPaused,
|
|
])
|
|
const handleSuggestionClick = useCallback(
|
|
(suggestion: SuggestionItem, event: React.MouseEvent) => {
|
|
// Mark a suggestion as selected if it's not a shift-click (which just copies to input)
|
|
if (!event.shiftKey) {
|
|
setSuggestionSelected(true)
|
|
// Also notify parent component to cancel auto-approval timeout
|
|
// This prevents race conditions between visual countdown and actual timeout
|
|
onCancelAutoApproval?.()
|
|
}
|
|
|
|
// Pass the suggestion object to the parent component
|
|
// The parent component will handle mode switching if needed
|
|
onSuggestionClick?.(suggestion, event)
|
|
},
|
|
[onSuggestionClick, onCancelAutoApproval],
|
|
)
|
|
|
|
// Don't render if there are no suggestions or no click handler.
|
|
if (!suggestions?.length || !onSuggestionClick) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="flex mb-2 flex-col h-full gap-2">
|
|
{suggestions.map((suggestion, index) => {
|
|
const isFirstSuggestion = index === 0
|
|
|
|
return (
|
|
<div key={`${suggestion.answer}-${ts}`} className="w-full relative group">
|
|
<Button
|
|
variant="outline"
|
|
className={cn(
|
|
"text-left whitespace-normal break-words w-full h-auto px-3 py-2 justify-start pr-8 rounded-xl",
|
|
isFirstSuggestion &&
|
|
countdown !== null &&
|
|
!suggestionSelected &&
|
|
!isAnswered &&
|
|
"border-vscode-foreground/60 rounded-b-none -mb-1",
|
|
)}
|
|
onClick={(event) => handleSuggestionClick(suggestion, event)}
|
|
aria-label={suggestion.answer}>
|
|
{suggestion.answer}
|
|
</Button>
|
|
{isFirstSuggestion && countdown !== null && !suggestionSelected && !isAnswered && (
|
|
<p className="rounded-b-xl border-1 border-t-0 border-vscode-foreground/60 text-vscode-descriptionForeground text-xs m-0 mt-1 px-3 pt-2 pb-2">
|
|
<Timer className="size-3 inline-block -mt-0.5 mr-1 animate-pulse" />
|
|
{t("chat:followUpSuggest.timerPrefix", { seconds: countdown })}
|
|
</p>
|
|
)}
|
|
{suggestion.mode && (
|
|
<div className="absolute bottom-0 right-0 text-[10px] text-vscode-badge-foreground pl-1 pr-2.5 pt-0.5 pb-1.5 flex items-center gap-0.5 bg-transparent rounded-xl">
|
|
<span className="codicon codicon-arrow-right" style={{ fontSize: "8px" }} />
|
|
{suggestion.mode}
|
|
</div>
|
|
)}
|
|
<StandardTooltip content={t("chat:followUpSuggest.copyToInput")}>
|
|
<div
|
|
className="absolute cursor-pointer top-1.5 right-1.5 opacity-0 group-hover:opacity-100 transition-opacity bg-vscode-input-background px-0.5 rounded"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
// Cancel the auto-approve timer when edit button is clicked
|
|
setSuggestionSelected(true)
|
|
onCancelAutoApproval?.()
|
|
// Simulate shift-click by directly calling the handler with shiftKey=true.
|
|
onSuggestionClick?.(suggestion, { ...e, shiftKey: true })
|
|
}}>
|
|
<ClipboardCopy className="w-4" />
|
|
</div>
|
|
</StandardTooltip>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|