Compare commits

...
Author SHA1 Message Date
Cline Evaluation 4fb61a4a14 settings cancel 2025-05-22 10:00:30 -07:00
Cline Evaluation d4adea9fbd settings cancel 2025-05-22 09:51:07 -07:00
3 changed files with 234 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
added cancel button with alert dialogue popup to settings view
@@ -0,0 +1,111 @@
import React, { ReactNode } from "react"
import { cn } from "@/utils/cn"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { AlertTriangle } from "lucide-react"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "../settings/OpenRouterModelPicker"
interface AlertDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
children: ReactNode
}
export function AlertDialog({ open, onOpenChange, children }: AlertDialogProps) {
if (!open) return null
// Close the dialog when clicking on the backdrop
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onOpenChange(false)
}
}
return (
<div
className={cn(`fixed inset-0 bg-black/50 flex items-center justify-center`)}
onClick={handleBackdropClick}
style={{ zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 50 }}>
{children}
</div>
)
}
export function AlertDialogContent({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
`fixed top-[50%] left-[50%] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%]`,
className,
)}
onClick={(e) => e.stopPropagation()}
{...props}>
<div className="bg-[var(--vscode-editor-background)] rounded-sm gap-3 border border-[var(--vscode-panel-border)] p-4 shadow-lg sm:max-w-md">
{children}
</div>
</div>
)
}
export function AlertDialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("flex flex-col gap-1 text-left", className)} {...props} />
}
export function AlertDialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("flex flex-row justify-end gap-2 mt-4", className)} {...props} />
}
export function AlertDialogTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h2
className={cn(
"text-base font-medium text-[var(--vscode-editor-foreground)] flex items-center gap-2 text-left",
className,
)}
{...props}
/>
)
}
export function AlertDialogDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
return <p className={cn("text-[var(--vscode-descriptionForeground)] text-sm text-left", className)} {...props} />
}
export function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof VSCodeButton>) {
return <VSCodeButton appearance="primary" {...props} />
}
export function AlertDialogCancel({ className, ...props }: React.ComponentProps<typeof VSCodeButton>) {
return <VSCodeButton appearance="secondary" {...props} />
}
export function UnsavedChangesDialog({
open,
onOpenChange,
onConfirm,
onCancel,
}: {
open: boolean
onOpenChange: (open: boolean) => void
onConfirm: () => void
onCancel: () => void
}) {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<AlertTriangle className="w-5 h-5 text-[var(--vscode-errorForeground)]" />
Unsaved Changes
</AlertDialogTitle>
<AlertDialogDescription>
You have unsaved changes. Are you sure you want to discard them?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>Discard Changes</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
@@ -22,6 +22,7 @@ import {
LucideIcon,
} from "lucide-react"
import HeroTooltip from "@/components/common/HeroTooltip"
import { UnsavedChangesDialog } from "@/components/common/AlertDialog"
import SectionHeader from "./SectionHeader"
import Section from "./Section"
import PreferredLanguageSetting from "./PreferredLanguageSetting" // Added import
@@ -124,6 +125,12 @@ type SettingsViewProps = {
}
const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
// Track if there are unsaved changes
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
// State for the unsaved changes dialog
const [isUnsavedChangesDialogOpen, setIsUnsavedChangesDialogOpen] = useState(false)
// Store the action to perform after confirmation
const pendingAction = useRef<() => void>()
const {
apiConfiguration,
version,
@@ -137,8 +144,22 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
planActSeparateModelsSetting,
setPlanActSeparateModelsSetting,
enableCheckpointsSetting,
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
setApiConfiguration,
} = useExtensionState()
// Store the original state to detect changes
const originalState = useRef({
apiConfiguration,
customInstructions,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
chatSettings,
})
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
const [pendingTabChange, setPendingTabChange] = useState<"plan" | "act" | null>(null)
@@ -191,6 +212,89 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setModelIdErrorMessage(undefined)
}, [apiConfiguration])
// Check for unsaved changes by comparing current state with original state
useEffect(() => {
const hasChanges =
JSON.stringify(apiConfiguration) !== JSON.stringify(originalState.current.apiConfiguration) ||
customInstructions !== originalState.current.customInstructions ||
telemetrySetting !== originalState.current.telemetrySetting ||
planActSeparateModelsSetting !== originalState.current.planActSeparateModelsSetting ||
enableCheckpointsSetting !== originalState.current.enableCheckpointsSetting ||
mcpMarketplaceEnabled !== originalState.current.mcpMarketplaceEnabled ||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings)
setHasUnsavedChanges(hasChanges)
}, [
apiConfiguration,
customInstructions,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
chatSettings,
])
// Handle cancel button click
const handleCancel = useCallback(() => {
if (hasUnsavedChanges) {
// Show confirmation dialog
setIsUnsavedChangesDialogOpen(true)
pendingAction.current = () => {
// Reset all tracked state to original values
setCustomInstructions(originalState.current.customInstructions)
setTelemetrySetting(originalState.current.telemetrySetting)
setPlanActSeparateModelsSetting(originalState.current.planActSeparateModelsSetting)
setChatSettings(originalState.current.chatSettings)
if (typeof setApiConfiguration === "function") {
setApiConfiguration(originalState.current.apiConfiguration ?? {})
}
if (typeof setEnableCheckpointsSetting === "function") {
setEnableCheckpointsSetting(
typeof originalState.current.enableCheckpointsSetting === "boolean"
? originalState.current.enableCheckpointsSetting
: false,
)
}
if (typeof setMcpMarketplaceEnabled === "function") {
setMcpMarketplaceEnabled(
typeof originalState.current.mcpMarketplaceEnabled === "boolean"
? originalState.current.mcpMarketplaceEnabled
: false,
)
}
// Close settings view
onDone()
}
} else {
// No changes, just close
onDone()
}
}, [
hasUnsavedChanges,
onDone,
setCustomInstructions,
setTelemetrySetting,
setPlanActSeparateModelsSetting,
setChatSettings,
setApiConfiguration,
setEnableCheckpointsSetting,
setMcpMarketplaceEnabled,
])
// Handle confirmation dialog actions
const handleConfirmDiscard = useCallback(() => {
setIsUnsavedChangesDialogOpen(false)
if (pendingAction.current) {
pendingAction.current()
pendingAction.current = undefined
}
}, [])
const handleCancelDiscard = useCallback(() => {
setIsUnsavedChangesDialogOpen(false)
pendingAction.current = undefined
}, [])
// validate as soon as the component is mounted
/*
useEffect will use stale values of variables if they are not included in the dependency array.
@@ -328,7 +432,12 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
<h3 className="text-[var(--vscode-foreground)] m-0">Settings</h3>
</div>
<div className="flex gap-2">
<VSCodeButton onClick={() => handleSubmit(false)}>Save</VSCodeButton>
<VSCodeButton appearance="secondary" onClick={handleCancel}>
Cancel
</VSCodeButton>
<VSCodeButton onClick={() => handleSubmit(false)} disabled={!hasUnsavedChanges}>
Save
</VSCodeButton>
</div>
</TabHeader>
@@ -593,6 +702,14 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
)
})()}
</div>
{/* Unsaved Changes Dialog */}
<UnsavedChangesDialog
open={isUnsavedChangesDialogOpen}
onOpenChange={setIsUnsavedChangesDialogOpen}
onConfirm={handleConfirmDiscard}
onCancel={handleCancelDiscard}
/>
</Tab>
)
}