diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 79a09ff017..2bff4cfb9a 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -110,6 +110,10 @@ export const globalSettingsSchema = z.object({ hasOpenedModeSelector: z.boolean().optional(), lastModeExportPath: z.string().optional(), lastModeImportPath: z.string().optional(), + + // Message modification confirmation preferences + skipEditMessageConfirmation: z.boolean().optional(), + skipDeleteMessageConfirmation: z.boolean().optional(), }) export type GlobalSettings = z.infer diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 2f356aef55..ed23e4c80e 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -28,9 +28,13 @@ const mockClineProvider = { globalStorageUri: { fsPath: "/mock/global/storage" }, }, setValue: vi.fn(), + getValue: vi.fn(), }, log: vi.fn(), postStateToWebview: vi.fn(), + getCurrentCline: vi.fn(), + getTaskWithId: vi.fn(), + initClineWithHistoryItem: vi.fn(), } as unknown as ClineProvider import { t } from "../../../i18n" @@ -482,3 +486,151 @@ describe("webviewMessageHandler - deleteCustomMode", () => { expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalled() }) }) + +describe("webviewMessageHandler - message dialog preferences", () => { + beforeEach(() => { + vi.clearAllMocks() + // Mock a current Cline instance + vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({ + taskId: "test-task-id", + apiConversationHistory: [], + clineMessages: [], + } as any) + // Reset getValue mock + vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(false) + }) + + describe("skipEditMessageConfirmation", () => { + it("should save edit message confirmation preference when set to true", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "skipEditMessageConfirmation", + bool: true, + }) + + expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("skipEditMessageConfirmation", true) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) + + it("should save edit message confirmation preference when set to false", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "skipEditMessageConfirmation", + bool: false, + }) + + expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("skipEditMessageConfirmation", false) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) + + it("should default to false when bool is not provided", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "skipEditMessageConfirmation", + }) + + expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("skipEditMessageConfirmation", false) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) + }) + + describe("skipDeleteMessageConfirmation", () => { + it("should save delete message confirmation preference when set to true", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "skipDeleteMessageConfirmation", + bool: true, + }) + + expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("skipDeleteMessageConfirmation", true) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) + + it("should save delete message confirmation preference when set to false", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "skipDeleteMessageConfirmation", + bool: false, + }) + + expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("skipDeleteMessageConfirmation", false) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) + + it("should default to false when bool is not provided", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "skipDeleteMessageConfirmation", + }) + + expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("skipDeleteMessageConfirmation", false) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) + }) + + describe("deleteMessage", () => { + it("should show dialog when skipDeleteMessageConfirmation is false", async () => { + vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(false) + + await webviewMessageHandler(mockClineProvider, { + type: "deleteMessage", + messageTs: 123456789, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "showDeleteMessageDialog", + messageTs: 123456789, + }) + }) + + it("should skip dialog and directly delete when skipDeleteMessageConfirmation is true", async () => { + vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(true) + + // Mock the necessary functions for deletion + vi.mocked(mockClineProvider.getTaskWithId).mockResolvedValue({ + historyItem: { id: "test-history-id" }, + } as any) + + await webviewMessageHandler(mockClineProvider, { + type: "deleteMessage", + messageTs: 123456789, + }) + + // Should not show dialog + expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalledWith( + expect.objectContaining({ + type: "showDeleteMessageDialog", + }), + ) + }) + }) + + describe("submitEditedMessage", () => { + it("should show dialog when skipEditMessageConfirmation is false", async () => { + vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(false) + + await webviewMessageHandler(mockClineProvider, { + type: "submitEditedMessage", + messageTs: 123456789, + text: "edited content", + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "showEditMessageDialog", + messageTs: 123456789, + text: "edited content", + }) + }) + + it("should skip dialog and directly edit when skipEditMessageConfirmation is true", async () => { + vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(true) + + await webviewMessageHandler(mockClineProvider, { + type: "submitEditedMessage", + messageTs: 123456789, + text: "edited content", + }) + + // Should not show dialog + expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalledWith( + expect.objectContaining({ + type: "showEditMessageDialog", + }), + ) + }) + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a6577fb2fb..b19951e154 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -77,55 +77,6 @@ export const webviewMessageHandler = async ( return { messageIndex, apiConversationHistoryIndex } } - /** - * Removes just the target message, preserving messages after the next user message - */ - const removeMessagesJustThis = async ( - currentCline: any, - messageIndex: number, - apiConversationHistoryIndex: number, - ) => { - // Find the next user message first - const nextUserMessage = currentCline.clineMessages - .slice(messageIndex + 1) - .find((msg: ClineMessage) => msg.type === "say" && msg.say === "user_feedback") - - // Handle UI messages - if (nextUserMessage) { - // Find absolute index of next user message - const nextUserMessageIndex = currentCline.clineMessages.findIndex( - (msg: ClineMessage) => msg === nextUserMessage, - ) - - // Keep messages before current message and after next user message - await currentCline.overwriteClineMessages([ - ...currentCline.clineMessages.slice(0, messageIndex), - ...currentCline.clineMessages.slice(nextUserMessageIndex), - ]) - } else { - // If no next user message, keep only messages before current message - await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex)) - } - - // Handle API messages - if (apiConversationHistoryIndex !== -1) { - if (nextUserMessage && nextUserMessage.ts) { - // Keep messages before current API message and after next user message - await currentCline.overwriteApiConversationHistory([ - ...currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex), - ...currentCline.apiConversationHistory.filter( - (msg: ApiMessage) => msg.ts && msg.ts >= nextUserMessage.ts, - ), - ]) - } else { - // If no next user message, keep only messages before current API message - await currentCline.overwriteApiConversationHistory( - currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex), - ) - } - } - } - /** * Removes the target message and all subsequent messages */ @@ -148,19 +99,27 @@ export const webviewMessageHandler = async ( * Handles message deletion operations with user confirmation */ const handleDeleteOperation = async (messageTs: number): Promise => { - const options = [ - t("common:confirmation.delete_just_this_message"), - t("common:confirmation.delete_this_and_subsequent"), - ] + // Check if user has opted to skip the confirmation + const skipDeleteMessageConfirmation = getGlobalState("skipDeleteMessageConfirmation") - const answer = await vscode.window.showInformationMessage( - t("common:confirmation.delete_message"), - { modal: true }, - ...options, - ) + if (skipDeleteMessageConfirmation) { + // Directly handle the deletion without showing dialog + await handleDeleteMessageConfirm(messageTs) + } else { + // Send message to webview to show delete confirmation dialog + await provider.postMessageToWebview({ + type: "showDeleteMessageDialog", + messageTs, + }) + } + } - // Only proceed if user selected one of the options and we have a current cline - if (answer && options.includes(answer) && provider.getCurrentCline()) { + /** + * Handles confirmed message deletion from webview dialog + */ + const handleDeleteMessageConfirm = async (messageTs: number): Promise => { + // Only proceed if we have a current cline + if (provider.getCurrentCline()) { const currentCline = provider.getCurrentCline()! const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline) @@ -168,14 +127,8 @@ export const webviewMessageHandler = async ( try { const { historyItem } = await provider.getTaskWithId(currentCline.taskId) - // Check which option the user selected - if (answer === options[0]) { - // Delete just this message - await removeMessagesJustThis(currentCline, messageIndex, apiConversationHistoryIndex) - } else if (answer === options[1]) { - // Delete this message and all subsequent - await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex) - } + // Delete this message and all subsequent messages + await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex) // Initialize with history item after deletion await provider.initClineWithHistoryItem(historyItem) @@ -193,14 +146,28 @@ export const webviewMessageHandler = async ( * Handles message editing operations with user confirmation */ const handleEditOperation = async (messageTs: number, editedContent: string): Promise => { - const answer = await vscode.window.showWarningMessage( - t("common:confirmation.edit_warning"), - { modal: true }, - t("common:confirmation.proceed"), - ) + // Check if user has opted to skip the confirmation + const skipEditMessageConfirmation = getGlobalState("skipEditMessageConfirmation") - // Only proceed if user selected "Proceed" and we have a current cline - if (answer === t("common:confirmation.proceed") && provider.getCurrentCline()) { + if (skipEditMessageConfirmation) { + // Directly handle the edit without showing dialog + await handleEditMessageConfirm(messageTs, editedContent) + } else { + // Send message to webview to show edit confirmation dialog + await provider.postMessageToWebview({ + type: "showEditMessageDialog", + messageTs, + text: editedContent, + }) + } + } + + /** + * Handles confirmed message editing from webview dialog + */ + const handleEditMessageConfirm = async (messageTs: number, editedContent: string): Promise => { + // Only proceed if we have a current cline + if (provider.getCurrentCline()) { const currentCline = provider.getCurrentCline()! // Use findMessageIndices to find messages based on timestamp @@ -1249,6 +1216,14 @@ export const webviewMessageHandler = async ( await updateGlobalState("historyPreviewCollapsed", message.bool ?? false) // No need to call postStateToWebview here as the UI already updated optimistically break + case "skipEditMessageConfirmation": + await updateGlobalState("skipEditMessageConfirmation", message.bool ?? false) + await provider.postStateToWebview() + break + case "skipDeleteMessageConfirmation": + await updateGlobalState("skipDeleteMessageConfirmation", message.bool ?? false) + await provider.postStateToWebview() + break case "toggleApiConfigPin": if (message.text) { const currentPinned = getGlobalState("pinnedApiConfigs") ?? {} @@ -1526,6 +1501,16 @@ export const webviewMessageHandler = async ( } } break + case "deleteMessageConfirm": + if (message.messageTs) { + await handleDeleteMessageConfirm(message.messageTs) + } + break + case "editMessageConfirm": + if (message.messageTs && message.text) { + await handleEditMessageConfirm(message.messageTs, message.text) + } + break case "getListApiConfiguration": try { const listApiConfig = await provider.providerSettingsManager.listConfig() diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index f907a5745b..ebf1448363 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -17,12 +17,7 @@ "confirmation": { "reset_state": "Are you sure you want to reset all state and secret storage in the extension? This cannot be undone.", "delete_config_profile": "Are you sure you want to delete this configuration profile?", - "delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}", - "delete_message": "What would you like to delete?", - "edit_warning": "Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?", - "delete_just_this_message": "Just this message", - "delete_this_and_subsequent": "This and all subsequent messages", - "proceed": "Proceed" + "delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Invalid data URI format", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 953c0c1070..e19d2368d7 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -105,6 +105,8 @@ export interface ExtensionMessage { | "shareTaskSuccess" | "codeIndexSettingsSaved" | "codeIndexSecretStatus" + | "showDeleteMessageDialog" + | "showEditMessageDialog" text?: string payload?: any // Add a generic payload for now, can refine later action?: @@ -157,6 +159,7 @@ export interface ExtensionMessage { visibility?: ShareVisibility rulesFolderPath?: string settings?: any + messageTs?: number } export type ExtensionState = Pick< @@ -182,6 +185,8 @@ export type ExtensionState = Pick< | "alwaysAllowSubtasks" | "alwaysAllowExecute" | "alwaysAllowUpdateTodoList" + | "skipEditMessageConfirmation" + | "skipDeleteMessageConfirmation" | "allowedCommands" | "allowedMaxRequests" | "browserToolEnabled" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index fa9fb67310..30fae8bd7b 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -80,6 +80,8 @@ export interface WebviewMessage { | "allowedMaxRequests" | "alwaysAllowSubtasks" | "alwaysAllowUpdateTodoList" + | "skipEditMessageConfirmation" + | "skipDeleteMessageConfirmation" | "autoCondenseContext" | "autoCondenseContextPercent" | "condensingApiConfigId" @@ -110,7 +112,9 @@ export interface WebviewMessage { | "enhancedPrompt" | "draggedImages" | "deleteMessage" + | "deleteMessageConfirm" | "submitEditedMessage" + | "editMessageConfirm" | "terminalOutputLineLimit" | "terminalShellIntegrationTimeout" | "terminalShellIntegrationDisabled" @@ -225,6 +229,7 @@ export interface WebviewMessage { ids?: string[] hasSystemPromptOverride?: boolean terminalOperation?: "continue" | "abort" + messageTs?: number historyPreviewCollapsed?: boolean filters?: { type?: string; search?: string; tags?: string[] } url?: string // For openExternal diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 332ef18511..4e6d0a4694 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -18,6 +18,7 @@ import McpView from "./components/mcp/McpView" import { MarketplaceView } from "./components/marketplace/MarketplaceView" import ModesView from "./components/modes/ModesView" import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog" +import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog" import { AccountView } from "./components/account/AccountView" import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick" import { TooltipProvider } from "./components/ui/tooltip" @@ -48,6 +49,10 @@ const App = () => { cloudApiUrl, renderContext, mdmCompliant, + skipEditMessageConfirmation, + setSkipEditMessageConfirmation, + skipDeleteMessageConfirmation, + setSkipDeleteMessageConfirmation, } = useExtensionState() // Create a persistent state manager @@ -66,6 +71,24 @@ const App = () => { promptText: "", }) + const [deleteMessageDialogState, setDeleteMessageDialogState] = useState<{ + isOpen: boolean + messageTs: number + }>({ + isOpen: false, + messageTs: 0, + }) + + const [editMessageDialogState, setEditMessageDialogState] = useState<{ + isOpen: boolean + messageTs: number + text: string + }>({ + isOpen: false, + messageTs: 0, + text: "", + }) + const settingsRef = useRef(null) const chatViewRef = useRef(null) @@ -121,11 +144,38 @@ const App = () => { setHumanRelayDialogState({ isOpen: true, requestId, promptText }) } + if (message.type === "showDeleteMessageDialog" && message.messageTs) { + // Check if user has opted to skip the confirmation + if (skipDeleteMessageConfirmation) { + // Directly send the confirmation without showing dialog + vscode.postMessage({ + type: "deleteMessageConfirm", + messageTs: message.messageTs, + }) + } else { + setDeleteMessageDialogState({ isOpen: true, messageTs: message.messageTs }) + } + } + + if (message.type === "showEditMessageDialog" && message.messageTs && message.text) { + // Check if user has opted to skip the confirmation + if (skipEditMessageConfirmation) { + // Directly send the confirmation without showing dialog + vscode.postMessage({ + type: "editMessageConfirm", + messageTs: message.messageTs, + text: message.text, + }) + } else { + setEditMessageDialogState({ isOpen: true, messageTs: message.messageTs, text: message.text }) + } + } + if (message.type === "acceptInput") { chatViewRef.current?.acceptInput() } }, - [switchTab], + [switchTab, skipDeleteMessageConfirmation, skipEditMessageConfirmation], ) useEvent("message", onMessage) @@ -207,6 +257,45 @@ const App = () => { onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })} onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })} /> + setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))} + onConfirm={(dontShowAgain) => { + // Save the preference if checkbox was checked + if (dontShowAgain) { + setSkipDeleteMessageConfirmation(true) + vscode.postMessage({ + type: "skipDeleteMessageConfirmation", + bool: true, + }) + } + vscode.postMessage({ + type: "deleteMessageConfirm", + messageTs: deleteMessageDialogState.messageTs, + }) + setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false })) + }} + /> + setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))} + onConfirm={(dontShowAgain) => { + // Save the preference if checkbox was checked + if (dontShowAgain) { + setSkipEditMessageConfirmation(true) + vscode.postMessage({ + type: "skipEditMessageConfirmation", + bool: true, + }) + } + vscode.postMessage({ + type: "editMessageConfirm", + messageTs: editMessageDialogState.messageTs, + text: editMessageDialogState.text, + }) + setEditMessageDialogState((prev) => ({ ...prev, isOpen: false })) + }} + /> ) } diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index b12e9b1961..b7da0a6ce1 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -981,7 +981,7 @@ const ChatTextArea = forwardRef(
void + onConfirm: (dontShowAgain: boolean) => void + type: "edit" | "delete" +} + +export const MessageModificationConfirmationDialog: React.FC = ({ + open, + onOpenChange, + onConfirm, + type, +}) => { + const { t } = useAppTranslation() + const [dontShowAgain, setDontShowAgain] = useState(false) + + const isEdit = type === "edit" + const title = isEdit ? t("common:confirmation.edit_message") : t("common:confirmation.delete_message") + const description = isEdit ? t("common:confirmation.edit_warning") : t("common:confirmation.delete_warning") + + const handleConfirm = () => { + onConfirm(dontShowAgain) + setDontShowAgain(false) // Reset for next time + } + + const handleOpenChange = (open: boolean) => { + if (!open) { + setDontShowAgain(false) // Reset when dialog closes + } + onOpenChange(open) + } + + return ( + + + + {title} + {description} + +
+ setDontShowAgain(checked as boolean)} + /> + +
+ + + {t("common:answers.cancel")} + + + {t("common:confirmation.proceed")} + + +
+
+ ) +} + +// Export convenience components for backward compatibility +export const EditMessageDialog: React.FC> = (props) => ( + +) + +export const DeleteMessageDialog: React.FC> = (props) => ( + +) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index bf927211c2..f85faab530 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -131,6 +131,10 @@ export interface ExtensionStateContextType extends ExtensionState { routerModels?: RouterModels alwaysAllowUpdateTodoList?: boolean setAlwaysAllowUpdateTodoList: (value: boolean) => void + skipEditMessageConfirmation?: boolean + setSkipEditMessageConfirmation: (value: boolean) => void + skipDeleteMessageConfirmation?: boolean + setSkipDeleteMessageConfirmation: (value: boolean) => void } export const ExtensionStateContext = createContext(undefined) @@ -224,6 +228,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }, codebaseIndexModels: { ollama: {}, openai: {} }, alwaysAllowUpdateTodoList: true, + skipEditMessageConfirmation: false, + skipDeleteMessageConfirmation: false, }) const [didHydrateState, setDidHydrateState] = useState(false) @@ -466,6 +472,14 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAlwaysAllowUpdateTodoList: (value) => { setState((prevState) => ({ ...prevState, alwaysAllowUpdateTodoList: value })) }, + skipEditMessageConfirmation: state.skipEditMessageConfirmation, + setSkipEditMessageConfirmation: (value) => { + setState((prevState) => ({ ...prevState, skipEditMessageConfirmation: value })) + }, + skipDeleteMessageConfirmation: state.skipDeleteMessageConfirmation, + setSkipDeleteMessageConfirmation: (value) => { + setState((prevState) => ({ ...prevState, skipDeleteMessageConfirmation: value })) + }, } return {children} diff --git a/webview-ui/src/i18n/locales/en/common.json b/webview-ui/src/i18n/locales/en/common.json index 1488f00f46..d73ad3ad2e 100644 --- a/webview-ui/src/i18n/locales/en/common.json +++ b/webview-ui/src/i18n/locales/en/common.json @@ -51,5 +51,13 @@ "success": { "imageDataUriCopied": "Image data URI copied to clipboard" } + }, + "confirmation": { + "delete_message": "Delete Message", + "delete_warning": "Deleting this message will delete all subsequent messages in the conversation. Do you want to proceed?", + "edit_message": "Edit Message", + "edit_warning": "Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?", + "proceed": "Proceed", + "dont_show_again": "Don't show this again" } }