remove option to skip notif

This commit is contained in:
Will Li 2025-07-10 17:26:36 -07:00
parent 67fd5a78ad
commit f89d768101
9 changed files with 22 additions and 366 deletions

View file

@ -110,10 +110,6 @@ 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

@ -1164,11 +1164,6 @@ describe("ClineProvider", () => {
describe("deleteMessage", () => {
beforeEach(async () => {
await provider.resolveWebviewView(mockWebviewView)
// Mock that skipDeleteMessageConfirmation is false by default
;(mockContext.globalState.get as any).mockImplementation((key: string) => {
if (key === "skipDeleteMessageConfirmation") return false
return undefined
})
})
test("handles deletion with confirmation dialog", async () => {
@ -1231,63 +1226,6 @@ describe("ClineProvider", () => {
expect((provider as any).initClineWithHistoryItem).toHaveBeenCalledWith({ id: "test-task-id" })
})
test("handles deletion with skipDeleteMessageConfirmation enabled", async () => {
// Mock that skipDeleteMessageConfirmation is true
const contextProxy = (provider as any).contextProxy
const getValueSpy = vi.spyOn(contextProxy, "getValue")
getValueSpy.mockImplementation((key: any) => {
if (key === "skipDeleteMessageConfirmation") return true
return undefined
})
// Setup mock messages
const mockMessages = [
{ ts: 1000, type: "say", say: "user_feedback" },
{ ts: 2000, type: "say", say: "text", value: 3000 }, // Message to delete
{ ts: 3000, type: "say", say: "user_feedback" },
{ ts: 4000, type: "say", say: "user_feedback" },
] as ClineMessage[]
const mockApiHistory = [
{ ts: 1000 },
{ ts: 2000 },
{ ts: 3000 },
{ ts: 4000 },
] as (Anthropic.MessageParam & {
ts?: number
})[]
// Setup Cline instance with auto-mock from the top of the file
const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
mockCline.clineMessages = mockMessages
mockCline.apiConversationHistory = mockApiHistory
await provider.addClineToStack(mockCline)
// Mock getTaskWithId
;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
historyItem: { id: "test-task-id" },
})
// Mock initClineWithHistoryItem
;(provider as any).initClineWithHistoryItem = vi.fn()
// Trigger message deletion
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
await messageHandler({ type: "deleteMessage", value: 3000 })
// Verify that NO dialog message was sent to webview (skipped)
expect(mockPostMessage).not.toHaveBeenCalledWith({
type: "showDeleteMessageDialog",
messageTs: 3000,
})
// Verify only messages before the deleted message were kept
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
// Verify only API messages before the deleted message were kept
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([mockApiHistory[0]])
})
test("handles case when no current task exists", async () => {
// Clear the cline stack
;(provider as any).clineStack = []
@ -1308,11 +1246,6 @@ describe("ClineProvider", () => {
describe("editMessage", () => {
beforeEach(async () => {
await provider.resolveWebviewView(mockWebviewView)
// Mock that skipEditMessageConfirmation is false by default
;(mockContext.globalState.get as any).mockImplementation((key: string) => {
if (key === "skipEditMessageConfirmation") return false
return undefined
})
})
test("handles edit with confirmation dialog", async () => {
@ -1390,56 +1323,6 @@ describe("ClineProvider", () => {
// We need to verify the recursive call happened by checking if the handler was called again
expect((mockWebviewView.webview.onDidReceiveMessage as any).mock.calls.length).toBeGreaterThanOrEqual(1)
})
test("handles edit with skipEditMessageConfirmation enabled", async () => {
// Mock that skipEditMessageConfirmation is true
const contextProxy = (provider as any).contextProxy
const getValueSpy = vi.spyOn(contextProxy, "getValue")
getValueSpy.mockImplementation((key: any) => {
if (key === "skipEditMessageConfirmation") return true
return undefined
})
// Setup mock messages
const mockMessages = [
{ ts: 1000, type: "say", say: "user_feedback" },
{ ts: 2000, type: "say", say: "text", value: 3000 }, // Message to edit
{ ts: 3000, type: "say", say: "user_feedback" },
] as ClineMessage[]
const mockApiHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }] as (Anthropic.MessageParam & {
ts?: number
})[]
// Setup Task instance
const mockCline = new Task(defaultTaskOptions)
mockCline.clineMessages = mockMessages
mockCline.apiConversationHistory = mockApiHistory
mockCline.overwriteClineMessages = vi.fn()
mockCline.overwriteApiConversationHistory = vi.fn()
mockCline.handleWebviewAskResponse = vi.fn()
await provider.addClineToStack(mockCline)
// Trigger message edit
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
await messageHandler({
type: "submitEditedMessage",
value: 3000,
editedMessageContent: "Edited message content",
})
// Verify that NO dialog message was sent to webview (skipped)
expect(mockPostMessage).not.toHaveBeenCalledWith({
type: "showEditMessageDialog",
messageTs: 3000,
text: "Edited message content",
})
// Verify messages were edited directly
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([mockApiHistory[0]])
})
})
describe("getSystemPrompt", () => {
@ -2784,12 +2667,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
describe("Edit Messages with Images and Attachments", () => {
beforeEach(async () => {
await provider.resolveWebviewView(mockWebviewView)
// Mock that skip confirmations are false by default
;(mockContext.globalState.get as any).mockImplementation((key: string) => {
if (key === "skipEditMessageConfirmation") return false
if (key === "skipDeleteMessageConfirmation") return false
return undefined
})
})
test("handles editing messages containing images", async () => {

View file

@ -500,71 +500,8 @@ describe("webviewMessageHandler - message dialog preferences", () => {
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)
it("should always show dialog for delete confirmation", async () => {
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists
await webviewMessageHandler(mockClineProvider, {
@ -577,36 +514,10 @@ describe("webviewMessageHandler - message dialog preferences", () => {
messageTs: 123456789,
})
})
it("should skip dialog and directly delete when skipDeleteMessageConfirmation is true", async () => {
vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(true)
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({
clineMessages: [{ ts: 123456789, text: "test message" }],
apiConversationHistory: [{ ts: 123456789, text: "test message" }],
} as any) // Mock current cline with required properties
// Mock the necessary functions for deletion
vi.mocked(mockClineProvider.getTaskWithId).mockResolvedValue({
historyItem: { id: "test-history-id" },
} as any)
await webviewMessageHandler(mockClineProvider, {
type: "deleteMessage",
value: 123456789, // Changed from messageTs to value
})
// 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)
it("should always show dialog for edit confirmation", async () => {
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists
await webviewMessageHandler(mockClineProvider, {
@ -621,26 +532,5 @@ describe("webviewMessageHandler - message dialog preferences", () => {
text: "edited content",
})
})
it("should skip dialog and directly edit when skipEditMessageConfirmation is true", async () => {
vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(true)
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({
clineMessages: [{ ts: 123456789, text: "test message" }],
apiConversationHistory: [{ ts: 123456789, text: "test message" }],
} as any) // Mock current cline with required properties
await webviewMessageHandler(mockClineProvider, {
type: "submitEditedMessage",
value: 123456789, // messageTs as number
editedMessageContent: "edited content", // text content in editedMessageContent field
})
// Should not show dialog
expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalledWith(
expect.objectContaining({
type: "showEditMessageDialog",
}),
)
})
})
})

View file

@ -99,19 +99,11 @@ export const webviewMessageHandler = async (
* Handles message deletion operations with user confirmation
*/
const handleDeleteOperation = async (messageTs: number): Promise<void> => {
// Check if user has opted to skip the confirmation
const skipDeleteMessageConfirmation = getGlobalState("skipDeleteMessageConfirmation")
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,
})
}
// Send message to webview to show delete confirmation dialog
await provider.postMessageToWebview({
type: "showDeleteMessageDialog",
messageTs,
})
}
/**
@ -146,20 +138,12 @@ export const webviewMessageHandler = async (
* Handles message editing operations with user confirmation
*/
const handleEditOperation = async (messageTs: number, editedContent: string): Promise<void> => {
// Check if user has opted to skip the confirmation
const skipEditMessageConfirmation = getGlobalState("skipEditMessageConfirmation")
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,
})
}
// Send message to webview to show edit confirmation dialog
await provider.postMessageToWebview({
type: "showEditMessageDialog",
messageTs,
text: editedContent,
})
}
/**
@ -1216,14 +1200,6 @@ 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") ?? {}

View file

@ -185,8 +185,6 @@ export type ExtensionState = Pick<
| "alwaysAllowSubtasks"
| "alwaysAllowExecute"
| "alwaysAllowUpdateTodoList"
| "skipEditMessageConfirmation"
| "skipDeleteMessageConfirmation"
| "allowedCommands"
| "allowedMaxRequests"
| "browserToolEnabled"

View file

@ -80,8 +80,6 @@ export interface WebviewMessage {
| "allowedMaxRequests"
| "alwaysAllowSubtasks"
| "alwaysAllowUpdateTodoList"
| "skipEditMessageConfirmation"
| "skipDeleteMessageConfirmation"
| "autoCondenseContext"
| "autoCondenseContextPercent"
| "condensingApiConfigId"

View file

@ -49,10 +49,6 @@ const App = () => {
cloudApiUrl,
renderContext,
mdmCompliant,
skipEditMessageConfirmation,
setSkipEditMessageConfirmation,
skipDeleteMessageConfirmation,
setSkipDeleteMessageConfirmation,
} = useExtensionState()
// Create a persistent state manager
@ -145,37 +141,18 @@ const App = () => {
}
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 })
}
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 })
}
setEditMessageDialogState({ isOpen: true, messageTs: message.messageTs, text: message.text })
}
if (message.type === "acceptInput") {
chatViewRef.current?.acceptInput()
}
},
[switchTab, skipDeleteMessageConfirmation, skipEditMessageConfirmation],
[switchTab],
)
useEvent("message", onMessage)
@ -260,15 +237,7 @@ const App = () => {
<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,
})
}
onConfirm={() => {
vscode.postMessage({
type: "deleteMessageConfirm",
messageTs: deleteMessageDialogState.messageTs,
@ -279,15 +248,7 @@ const App = () => {
<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,
})
}
onConfirm={() => {
vscode.postMessage({
type: "editMessageConfirm",
messageTs: editMessageDialogState.messageTs,

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"
import React from "react"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import {
AlertDialog,
@ -9,13 +9,12 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Checkbox,
} from "@src/components/ui"
interface MessageModificationConfirmationDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onConfirm: (dontShowAgain: boolean) => void
onConfirm: () => void
type: "edit" | "delete"
}
@ -26,49 +25,24 @@ export const MessageModificationConfirmationDialog: React.FC<MessageModification
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}>
<AlertDialog open={open} onOpenChange={onOpenChange}>
<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}
onClick={onConfirm}
className="bg-vscode-button-background hover:bg-vscode-button-hoverBackground text-vscode-button-foreground border-vscode-button-border">
{t("common:confirmation.proceed")}
</AlertDialogAction>

View file

@ -131,10 +131,6 @@ 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)
@ -228,8 +224,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
},
codebaseIndexModels: { ollama: {}, openai: {} },
alwaysAllowUpdateTodoList: true,
skipEditMessageConfirmation: false,
skipDeleteMessageConfirmation: false,
})
const [didHydrateState, setDidHydrateState] = useState(false)
@ -472,14 +466,6 @@ 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>