working functionality

This commit is contained in:
Will Li 2025-07-09 15:32:50 -07:00
parent d926a771bc
commit 24a8c10048
11 changed files with 428 additions and 83 deletions

View file

@ -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<typeof globalSettingsSchema>

View file

@ -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",
}),
)
})
})
})

View file

@ -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<void> => {
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<void> => {
// 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<void> => {
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<void> => {
// 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()

View file

@ -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",

View file

@ -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"

View file

@ -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

View file

@ -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<SettingsViewRef>(null)
const chatViewRef = useRef<ChatViewRef>(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 })}
/>
<DeleteMessageDialog
open={deleteMessageDialogState.isOpen}
onOpenChange={(open) => 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 }))
}}
/>
<EditMessageDialog
open={editMessageDialogState.isOpen}
onOpenChange={(open) => 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 }))
}}
/>
</>
)
}

View file

@ -981,7 +981,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
<div className="absolute bottom-1 right-1 z-30">
<IconButton
iconClass="codicon-edit"
title={t("chat:save")}
title={t("chat:save.tooltip")}
disabled={sendingDisabled}
onClick={onSend}
className="opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground"

View file

@ -0,0 +1,88 @@
import React, { useState } from "react"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Checkbox,
} from "@src/components/ui"
interface MessageModificationConfirmationDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onConfirm: (dontShowAgain: boolean) => void
type: "edit" | "delete"
}
export const MessageModificationConfirmationDialog: React.FC<MessageModificationConfirmationDialogProps> = ({
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 (
<AlertDialog open={open} onOpenChange={handleOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="text-lg">{title}</AlertDialogTitle>
<AlertDialogDescription className="text-base">{description}</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex items-center space-x-2 px-6 py-1">
<Checkbox
id="dont-show-again"
checked={dontShowAgain}
onCheckedChange={(checked) => setDontShowAgain(checked as boolean)}
/>
<label
htmlFor="dont-show-again"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer">
{t("common:confirmation.dont_show_again")}
</label>
</div>
<AlertDialogFooter className="flex-col gap-2">
<AlertDialogCancel className="bg-vscode-button-secondaryBackground hover:bg-vscode-button-secondaryHoverBackground text-vscode-button-secondaryForeground border-vscode-button-border">
{t("common:answers.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirm}
className="bg-vscode-button-background hover:bg-vscode-button-hoverBackground text-vscode-button-foreground border-vscode-button-border">
{t("common:confirmation.proceed")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
// Export convenience components for backward compatibility
export const EditMessageDialog: React.FC<Omit<MessageModificationConfirmationDialogProps, "type">> = (props) => (
<MessageModificationConfirmationDialog {...props} type="edit" />
)
export const DeleteMessageDialog: React.FC<Omit<MessageModificationConfirmationDialogProps, "type">> = (props) => (
<MessageModificationConfirmationDialog {...props} type="delete" />
)

View file

@ -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<ExtensionStateContextType | undefined>(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 <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

View file

@ -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"
}
}