mirror of
https://github.com/cline/cline.git
synced 2026-09-16 06:32:31 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87c43732a4 | ||
|
|
61bf6dd98f | ||
|
|
7b706308f1 | ||
|
|
b1681ebeb4 | ||
|
|
f86ec8a2e8 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix race condition in model switching
|
||||
@@ -35,7 +35,7 @@ export function AlertDialogContent({ className, children, ...props }: React.HTML
|
||||
className={`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">
|
||||
<div className="bg-[var(--vscode-editor-background)] rounded-sm gap-3 border border-[var(--vscode-panel-border)] p-6 shadow-lg sm:max-w-lg">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@ export function AlertDialogHeader({ className, ...props }: React.HTMLAttributes<
|
||||
}
|
||||
|
||||
export function AlertDialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={`flex flex-row justify-end gap-2 mt-4 ${className}`} {...props} />
|
||||
return <div className={`flex flex-row justify-end gap-3 mt-6 ${className}`} {...props} />
|
||||
}
|
||||
|
||||
export function AlertDialogTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
||||
@@ -76,11 +76,23 @@ export function UnsavedChangesDialog({
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onSave,
|
||||
title = "Unsaved Changes",
|
||||
description = "You have unsaved changes. Are you sure you want to discard them?",
|
||||
confirmText = "Discard Changes",
|
||||
saveText = "Save & Continue",
|
||||
showSaveOption = false,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
onSave?: () => void
|
||||
title?: string
|
||||
description?: string
|
||||
confirmText?: string
|
||||
saveText?: string
|
||||
showSaveOption?: boolean
|
||||
}) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -88,15 +100,16 @@ export function UnsavedChangesDialog({
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<AlertTriangle className="w-5 h-5 text-[var(--vscode-errorForeground)]" />
|
||||
Unsaved Changes
|
||||
{title}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You have unsaved changes. Are you sure you want to discard them?
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm}>Discard Changes</AlertDialogAction>
|
||||
{showSaveOption && onSave && <AlertDialogAction onClick={onSave}>{saveText}</AlertDialogAction>}
|
||||
<AlertDialogAction onClick={onConfirm} appearance={showSaveOption ? "secondary" : "primary"}>
|
||||
{confirmText}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@@ -15,25 +15,36 @@ import { StateServiceClient } from "@/services/grpc-client"
|
||||
import { PlanActMode, TogglePlanActModeRequest } from "@shared/proto/state"
|
||||
|
||||
// Styled component for Act Mode text with more specific styling
|
||||
const ActModeHighlight: React.FC = () => (
|
||||
<span
|
||||
onClick={() => {
|
||||
StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: PlanActMode.ACT,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}}
|
||||
title="Click to toggle to Act Mode"
|
||||
className="text-[var(--vscode-textLink-foreground)] hover:opacity-90 cursor-pointer inline-flex items-center gap-1">
|
||||
<div className="p-1 rounded-[12px] bg-[var(--vscode-editor-background)] flex items-center justify-end w-4 border-[1px] border-[var(--vscode-input-border)]">
|
||||
<div className="rounded-full bg-[var(--vscode-textLink-foreground)] w-2 h-2" />
|
||||
</div>
|
||||
Act Mode (⌘⇧A)
|
||||
</span>
|
||||
)
|
||||
const ActModeHighlight: React.FC = () => {
|
||||
const { chatSettings } = useExtensionState()
|
||||
|
||||
return (
|
||||
<span
|
||||
onClick={() => {
|
||||
// Only toggle to Act mode if we're currently in Plan mode
|
||||
if (chatSettings.mode === "plan") {
|
||||
StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: PlanActMode.ACT,
|
||||
preferredLanguage: chatSettings.preferredLanguage,
|
||||
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}}
|
||||
title={chatSettings.mode === "plan" ? "Click to toggle to Act Mode" : "Already in Act Mode"}
|
||||
className={`text-[var(--vscode-textLink-foreground)] inline-flex items-center gap-1 ${
|
||||
chatSettings.mode === "plan" ? "hover:opacity-90 cursor-pointer" : "cursor-default opacity-60"
|
||||
}`}>
|
||||
<div className="p-1 rounded-[12px] bg-[var(--vscode-editor-background)] flex items-center justify-end w-4 border-[1px] border-[var(--vscode-input-border)]">
|
||||
<div className="rounded-full bg-[var(--vscode-textLink-foreground)] w-2 h-2" />
|
||||
</div>
|
||||
Act Mode (⌘⇧A)
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
interface MarkdownBlockProps {
|
||||
markdown?: string
|
||||
|
||||
@@ -113,19 +113,21 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
const StyledTabButton = styled.button<{ isActive: boolean }>`
|
||||
const StyledTabButton = styled.button<{ isActive: boolean; disabled?: boolean }>`
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")};
|
||||
color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
|
||||
font-size: 13px;
|
||||
margin-bottom: -1px;
|
||||
font-family: inherit;
|
||||
opacity: ${(props) => (props.disabled ? 0.6 : 1)};
|
||||
pointer-events: ${(props) => (props.disabled ? "none" : "auto")};
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-foreground);
|
||||
color: ${(props) => (props.disabled ? "var(--vscode-descriptionForeground)" : "var(--vscode-foreground)")};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -133,12 +135,16 @@ export const TabButton = ({
|
||||
children,
|
||||
isActive,
|
||||
onClick,
|
||||
disabled,
|
||||
style,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
isActive: boolean
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
style?: React.CSSProperties
|
||||
}) => (
|
||||
<StyledTabButton isActive={isActive} onClick={onClick}>
|
||||
<StyledTabButton isActive={isActive} onClick={onClick} disabled={disabled} style={style}>
|
||||
{children}
|
||||
</StyledTabButton>
|
||||
)
|
||||
|
||||
@@ -112,6 +112,10 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
const [isUnsavedChangesDialogOpen, setIsUnsavedChangesDialogOpen] = useState(false)
|
||||
// Store the action to perform after confirmation
|
||||
const pendingAction = useRef<() => void>()
|
||||
// Track if we're currently switching modes
|
||||
const [isSwitchingMode, setIsSwitchingMode] = useState(false)
|
||||
// Track pending mode switch when there are unsaved changes
|
||||
const [pendingModeSwitch, setPendingModeSwitch] = useState<"plan" | "act" | null>(null)
|
||||
const {
|
||||
apiConfiguration,
|
||||
version,
|
||||
@@ -260,8 +264,32 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
setModelIdErrorMessage(undefined)
|
||||
}, [apiConfiguration])
|
||||
|
||||
// Track the previous mode to detect mode switches
|
||||
const previousMode = useRef(chatSettings.mode)
|
||||
|
||||
// Update original state when mode changes
|
||||
useEffect(() => {
|
||||
// Detect if the mode has changed
|
||||
if (previousMode.current !== chatSettings.mode) {
|
||||
// Mode has changed, update the original state immediately to reflect the new apiConfiguration and chatSettings
|
||||
originalState.current = {
|
||||
...originalState.current,
|
||||
apiConfiguration: apiConfiguration,
|
||||
chatSettings: chatSettings,
|
||||
}
|
||||
|
||||
// Update the previous mode reference
|
||||
previousMode.current = chatSettings.mode
|
||||
}
|
||||
}, [chatSettings.mode, apiConfiguration, chatSettings])
|
||||
|
||||
// Check for unsaved changes by comparing current state with original state
|
||||
useEffect(() => {
|
||||
// Don't check for changes while switching modes
|
||||
if (isSwitchingMode) {
|
||||
return
|
||||
}
|
||||
|
||||
const hasChanges =
|
||||
JSON.stringify(apiConfiguration) !== JSON.stringify(originalState.current.apiConfiguration) ||
|
||||
telemetrySetting !== originalState.current.telemetrySetting ||
|
||||
@@ -271,7 +299,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
mcpRichDisplayEnabled !== originalState.current.mcpRichDisplayEnabled ||
|
||||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
|
||||
mcpResponsesCollapsed !== originalState.current.mcpResponsesCollapsed ||
|
||||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
|
||||
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
|
||||
terminalOutputLineLimit !== originalState.current.terminalOutputLineLimit ||
|
||||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled ||
|
||||
@@ -292,7 +319,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
localBrowserSettings,
|
||||
isSwitchingMode,
|
||||
])
|
||||
|
||||
// Handle cancel button click
|
||||
@@ -368,17 +395,134 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
])
|
||||
|
||||
// Handle confirmation dialog actions
|
||||
const handleConfirmDiscard = useCallback(() => {
|
||||
const handleConfirmDiscard = useCallback(async () => {
|
||||
setIsUnsavedChangesDialogOpen(false)
|
||||
if (pendingAction.current) {
|
||||
|
||||
// Check if this is for a mode switch
|
||||
if (pendingModeSwitch) {
|
||||
// Reset all state to original values (discard changes)
|
||||
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,
|
||||
)
|
||||
}
|
||||
if (typeof setMcpRichDisplayEnabled === "function") {
|
||||
setMcpRichDisplayEnabled(
|
||||
typeof originalState.current.mcpRichDisplayEnabled === "boolean"
|
||||
? originalState.current.mcpRichDisplayEnabled
|
||||
: true,
|
||||
)
|
||||
}
|
||||
// Reset terminal settings
|
||||
if (typeof setShellIntegrationTimeout === "function") {
|
||||
setShellIntegrationTimeout(originalState.current.shellIntegrationTimeout)
|
||||
}
|
||||
if (typeof setTerminalOutputLineLimit === "function") {
|
||||
setTerminalOutputLineLimit(originalState.current.terminalOutputLineLimit)
|
||||
}
|
||||
if (typeof setTerminalReuseEnabled === "function") {
|
||||
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
|
||||
}
|
||||
if (typeof setDefaultTerminalProfile === "function") {
|
||||
setDefaultTerminalProfile(originalState.current.defaultTerminalProfile ?? "default")
|
||||
}
|
||||
if (typeof setMcpResponsesCollapsed === "function") {
|
||||
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
|
||||
}
|
||||
|
||||
// Now perform the mode switch
|
||||
const targetMode = pendingModeSwitch
|
||||
setPendingModeSwitch(null)
|
||||
setIsSwitchingMode(true)
|
||||
|
||||
try {
|
||||
await StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: targetMode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
|
||||
preferredLanguage: chatSettings.preferredLanguage,
|
||||
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
|
||||
},
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle Plan/Act mode:", error)
|
||||
} finally {
|
||||
setIsSwitchingMode(false)
|
||||
}
|
||||
} else if (pendingAction.current) {
|
||||
// Regular cancel button flow
|
||||
pendingAction.current()
|
||||
pendingAction.current = undefined
|
||||
}
|
||||
}, [])
|
||||
}, [
|
||||
pendingModeSwitch,
|
||||
setTelemetrySetting,
|
||||
setPlanActSeparateModelsSetting,
|
||||
setChatSettings,
|
||||
setApiConfiguration,
|
||||
setEnableCheckpointsSetting,
|
||||
setMcpMarketplaceEnabled,
|
||||
setMcpRichDisplayEnabled,
|
||||
setShellIntegrationTimeout,
|
||||
setTerminalOutputLineLimit,
|
||||
setTerminalReuseEnabled,
|
||||
setDefaultTerminalProfile,
|
||||
setMcpResponsesCollapsed,
|
||||
chatSettings.preferredLanguage,
|
||||
chatSettings.openAIReasoningEffort,
|
||||
])
|
||||
|
||||
// Handle save and switch for mode changes
|
||||
const handleSaveAndSwitch = useCallback(async () => {
|
||||
setIsUnsavedChangesDialogOpen(false)
|
||||
|
||||
if (pendingModeSwitch) {
|
||||
// Save the current settings first
|
||||
await handleSubmit(true)
|
||||
|
||||
// Now perform the mode switch
|
||||
const targetMode = pendingModeSwitch
|
||||
setPendingModeSwitch(null)
|
||||
setIsSwitchingMode(true)
|
||||
|
||||
try {
|
||||
await StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: targetMode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
|
||||
preferredLanguage: chatSettings.preferredLanguage,
|
||||
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
|
||||
},
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle Plan/Act mode:", error)
|
||||
} finally {
|
||||
setIsSwitchingMode(false)
|
||||
}
|
||||
}
|
||||
}, [pendingModeSwitch, handleSubmit, chatSettings.preferredLanguage, chatSettings.openAIReasoningEffort])
|
||||
|
||||
const handleCancelDiscard = useCallback(() => {
|
||||
setIsUnsavedChangesDialogOpen(false)
|
||||
pendingAction.current = undefined
|
||||
setPendingModeSwitch(null)
|
||||
}, [])
|
||||
|
||||
// validate as soon as the component is mounted
|
||||
@@ -446,14 +590,25 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
}
|
||||
|
||||
const handlePlanActModeChange = async (tab: "plan" | "act") => {
|
||||
if (tab === chatSettings.mode) {
|
||||
// Prevent switching if already in that mode or if currently switching
|
||||
if (tab === chatSettings.mode || isSwitchingMode) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update settings first to ensure any changes to the current tab are saved
|
||||
await handleSubmit(true)
|
||||
// Check if there are unsaved changes
|
||||
if (hasUnsavedChanges) {
|
||||
// Store the pending mode switch
|
||||
setPendingModeSwitch(tab)
|
||||
// Show the unsaved changes dialog
|
||||
setIsUnsavedChangesDialogOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
// No unsaved changes, proceed with the switch
|
||||
setIsSwitchingMode(true)
|
||||
|
||||
try {
|
||||
// Perform the mode switch
|
||||
await StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
@@ -465,6 +620,9 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle Plan/Act mode:", error)
|
||||
} finally {
|
||||
// Always re-enable mode switching, even on error
|
||||
setIsSwitchingMode(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,13 +770,27 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
<div className="flex gap-[1px] mb-[10px] -mt-2 border-0 border-b border-solid border-[var(--vscode-panel-border)]">
|
||||
<TabButton
|
||||
isActive={chatSettings.mode === "plan"}
|
||||
onClick={() => handlePlanActModeChange("plan")}>
|
||||
Plan Mode
|
||||
onClick={() => handlePlanActModeChange("plan")}
|
||||
disabled={isSwitchingMode}
|
||||
style={{
|
||||
opacity: isSwitchingMode ? 0.6 : 1,
|
||||
cursor: isSwitchingMode ? "not-allowed" : "pointer",
|
||||
}}>
|
||||
{isSwitchingMode && chatSettings.mode === "act"
|
||||
? "Switching..."
|
||||
: "Plan Mode"}
|
||||
</TabButton>
|
||||
<TabButton
|
||||
isActive={chatSettings.mode === "act"}
|
||||
onClick={() => handlePlanActModeChange("act")}>
|
||||
Act Mode
|
||||
onClick={() => handlePlanActModeChange("act")}
|
||||
disabled={isSwitchingMode}
|
||||
style={{
|
||||
opacity: isSwitchingMode ? 0.6 : 1,
|
||||
cursor: isSwitchingMode ? "not-allowed" : "pointer",
|
||||
}}>
|
||||
{isSwitchingMode && chatSettings.mode === "plan"
|
||||
? "Switching..."
|
||||
: "Act Mode"}
|
||||
</TabButton>
|
||||
</div>
|
||||
|
||||
@@ -787,6 +959,16 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
onOpenChange={setIsUnsavedChangesDialogOpen}
|
||||
onConfirm={handleConfirmDiscard}
|
||||
onCancel={handleCancelDiscard}
|
||||
onSave={pendingModeSwitch ? handleSaveAndSwitch : undefined}
|
||||
title={pendingModeSwitch ? "Save Changes?" : "Unsaved Changes"}
|
||||
description={
|
||||
pendingModeSwitch
|
||||
? `Do you want to save your changes to ${chatSettings.mode === "plan" ? "Plan" : "Act"} mode before switching to ${pendingModeSwitch === "plan" ? "Plan" : "Act"} mode?`
|
||||
: "You have unsaved changes. Are you sure you want to discard them?"
|
||||
}
|
||||
confirmText={pendingModeSwitch ? "Switch Without Saving" : "Discard Changes"}
|
||||
saveText="Save & Switch"
|
||||
showSaveOption={!!pendingModeSwitch}
|
||||
/>
|
||||
</Tab>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user