Add a button to delete user messages

This commit is contained in:
Matt Rubens
2024-12-30 13:53:53 -08:00
parent 6f0030d6e6
commit 90ed3a4582
8 changed files with 73 additions and 14 deletions

View File

@@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Add a button to delete user messages

View File

@@ -5,6 +5,7 @@ A fork of Cline, an autonomous coding agent, with some additional experimental f
## Experimental Features ## Experimental Features
- Drag and drop images into chats - Drag and drop images into chats
- Delete messages from chats
- "Enhance prompt" button (OpenRouter models only for now) - "Enhance prompt" button (OpenRouter models only for now)
- Sound effects for feedback - Sound effects for feedback
- Option to use browsers of different sizes and adjust screenshot quality - Option to use browsers of different sizes and adjust screenshot quality

View File

@@ -70,7 +70,7 @@ export class Cline {
diffStrategy?: DiffStrategy diffStrategy?: DiffStrategy
diffEnabled: boolean = false diffEnabled: boolean = false
apiConversationHistory: Anthropic.MessageParam[] = [] apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = []
clineMessages: ClineMessage[] = [] clineMessages: ClineMessage[] = []
private askResponse?: ClineAskResponse private askResponse?: ClineAskResponse
private askResponseText?: string private askResponseText?: string
@@ -165,11 +165,12 @@ export class Cline {
} }
private async addToApiConversationHistory(message: Anthropic.MessageParam) { private async addToApiConversationHistory(message: Anthropic.MessageParam) {
this.apiConversationHistory.push(message) const messageWithTs = { ...message, ts: Date.now() }
this.apiConversationHistory.push(messageWithTs)
await this.saveApiConversationHistory() await this.saveApiConversationHistory()
} }
private async overwriteApiConversationHistory(newHistory: Anthropic.MessageParam[]) { async overwriteApiConversationHistory(newHistory: Anthropic.MessageParam[]) {
this.apiConversationHistory = newHistory this.apiConversationHistory = newHistory
await this.saveApiConversationHistory() await this.saveApiConversationHistory()
} }
@@ -205,7 +206,7 @@ export class Cline {
await this.saveClineMessages() await this.saveClineMessages()
} }
private async overwriteClineMessages(newMessages: ClineMessage[]) { public async overwriteClineMessages(newMessages: ClineMessage[]) {
this.clineMessages = newMessages this.clineMessages = newMessages
await this.saveClineMessages() await this.saveClineMessages()
} }
@@ -460,6 +461,11 @@ export class Cline {
await this.overwriteClineMessages(modifiedClineMessages) await this.overwriteClineMessages(modifiedClineMessages)
this.clineMessages = await this.getSavedClineMessages() this.clineMessages = await this.getSavedClineMessages()
// need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with cline messages
let existingApiConversationHistory: Anthropic.Messages.MessageParam[] =
await this.getSavedApiConversationHistory()
// Now present the cline messages to the user and ask if they want to resume // Now present the cline messages to the user and ask if they want to resume
const lastClineMessage = this.clineMessages const lastClineMessage = this.clineMessages
@@ -493,11 +499,6 @@ export class Cline {
responseImages = images responseImages = images
} }
// need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with cline messages
let existingApiConversationHistory: Anthropic.Messages.MessageParam[] =
await this.getSavedApiConversationHistory()
// v2.0 xml tags refactor caveat: since we don't use tools anymore, we need to replace all tool use blocks with a text block since the API disallows conversations with tool uses and no tool schema // v2.0 xml tags refactor caveat: since we don't use tools anymore, we need to replace all tool use blocks with a text block since the API disallows conversations with tool uses and no tool schema
const conversationWithoutToolBlocks = existingApiConversationHistory.map((message) => { const conversationWithoutToolBlocks = existingApiConversationHistory.map((message) => {
if (Array.isArray(message.content)) { if (Array.isArray(message.content)) {

View File

@@ -642,6 +642,28 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("writeDelayMs", message.value) await this.updateGlobalState("writeDelayMs", message.value)
await this.postStateToWebview() await this.postStateToWebview()
break break
case "deleteMessage": {
const answer = await vscode.window.showInformationMessage(
"Are you sure you want to delete this message and all subsequent messages?",
{ modal: true },
"Yes",
"No"
)
if (answer === "Yes" && this.cline && typeof message.value === 'number' && message.value) {
const timeCutoff = message.value - 1000; // 1 second buffer before the message to delete
const messageIndex = this.cline.clineMessages.findIndex(msg => msg.ts && msg.ts >= timeCutoff)
const apiConversationHistoryIndex = this.cline.apiConversationHistory.findIndex(msg => msg.ts && msg.ts >= timeCutoff)
if (messageIndex !== -1) {
const { historyItem } = await this.getTaskWithId(this.cline.taskId)
await this.cline.overwriteClineMessages(this.cline.clineMessages.slice(0, messageIndex))
if (apiConversationHistoryIndex !== -1) {
await this.cline.overwriteApiConversationHistory(this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex))
}
await this.initClineWithHistoryItem(historyItem)
}
}
break
}
case "screenshotQuality": case "screenshotQuality":
await this.updateGlobalState("screenshotQuality", message.value) await this.updateGlobalState("screenshotQuality", message.value)
await this.postStateToWebview() await this.postStateToWebview()

View File

@@ -47,6 +47,7 @@ export interface WebviewMessage {
| "enhancePrompt" | "enhancePrompt"
| "enhancedPrompt" | "enhancedPrompt"
| "draggedImages" | "draggedImages"
| "deleteMessage"
text?: string text?: string
disabled?: boolean disabled?: boolean
askResponse?: ClineAskResponse askResponse?: ClineAskResponse

View File

@@ -20,6 +20,7 @@ interface BrowserSessionRowProps {
lastModifiedMessage?: ClineMessage lastModifiedMessage?: ClineMessage
isLast: boolean isLast: boolean
onHeightChange: (isTaller: boolean) => void onHeightChange: (isTaller: boolean) => void
isStreaming: boolean
} }
const BrowserSessionRow = memo((props: BrowserSessionRowProps) => { const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
@@ -408,6 +409,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
interface BrowserSessionRowContentProps extends Omit<BrowserSessionRowProps, "messages"> { interface BrowserSessionRowContentProps extends Omit<BrowserSessionRowProps, "messages"> {
message: ClineMessage message: ClineMessage
setMaxActionHeight: (height: number) => void setMaxActionHeight: (height: number) => void
isStreaming: boolean
} }
const BrowserSessionRowContent = ({ const BrowserSessionRowContent = ({
@@ -417,6 +419,7 @@ const BrowserSessionRowContent = ({
lastModifiedMessage, lastModifiedMessage,
isLast, isLast,
setMaxActionHeight, setMaxActionHeight,
isStreaming,
}: BrowserSessionRowContentProps) => { }: BrowserSessionRowContentProps) => {
const headerStyle: React.CSSProperties = { const headerStyle: React.CSSProperties = {
display: "flex", display: "flex",
@@ -443,6 +446,7 @@ const BrowserSessionRowContent = ({
}} }}
lastModifiedMessage={lastModifiedMessage} lastModifiedMessage={lastModifiedMessage}
isLast={isLast} isLast={isLast}
isStreaming={isStreaming}
/> />
</div> </div>
) )

View File

@@ -1,4 +1,4 @@
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal" import deepEqual from "fast-deep-equal"
import React, { memo, useEffect, useMemo, useRef } from "react" import React, { memo, useEffect, useMemo, useRef } from "react"
import { useSize } from "react-use" import { useSize } from "react-use"
@@ -27,6 +27,7 @@ interface ChatRowProps {
lastModifiedMessage?: ClineMessage lastModifiedMessage?: ClineMessage
isLast: boolean isLast: boolean
onHeightChange: (isTaller: boolean) => void onHeightChange: (isTaller: boolean) => void
isStreaming: boolean
} }
interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {} interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {}
@@ -75,6 +76,7 @@ export const ChatRowContent = ({
onToggleExpand, onToggleExpand,
lastModifiedMessage, lastModifiedMessage,
isLast, isLast,
isStreaming,
}: ChatRowContentProps) => { }: ChatRowContentProps) => {
const { mcpServers } = useExtensionState() const { mcpServers } = useExtensionState()
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => { const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
@@ -475,10 +477,9 @@ export const ChatRowContent = ({
msUserSelect: "none", msUserSelect: "none",
}} }}
onClick={onToggleExpand}> onClick={onToggleExpand}>
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}> <div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
{icon} {icon}
{title} {title}
{/* Need to render this everytime since it affects height of row by 2px */}
<VSCodeBadge style={{ opacity: cost != null && cost > 0 ? 1 : 0 }}> <VSCodeBadge style={{ opacity: cost != null && cost > 0 ? 1 : 0 }}>
${Number(cost || 0)?.toFixed(4)} ${Number(cost || 0)?.toFixed(4)}
</VSCodeBadge> </VSCodeBadge>
@@ -570,7 +571,29 @@ export const ChatRowContent = ({
whiteSpace: "pre-line", whiteSpace: "pre-line",
wordWrap: "break-word", wordWrap: "break-word",
}}> }}>
<span style={{ display: "block" }}>{highlightMentions(message.text)}</span> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: "10px" }}>
<span style={{ display: "block", flexGrow: 1 }}>{highlightMentions(message.text)}</span>
<VSCodeButton
appearance="icon"
style={{
padding: "3px",
flexShrink: 0,
height: "24px",
marginTop: "-6px",
marginRight: "-6px"
}}
disabled={isStreaming}
onClick={(e) => {
e.stopPropagation();
vscode.postMessage({
type: "deleteMessage",
value: message.ts
});
}}
>
<span className="codicon codicon-trash"></span>
</VSCodeButton>
</div>
{message.images && message.images.length > 0 && ( {message.images && message.images.length > 0 && (
<Thumbnails images={message.images} style={{ marginTop: "8px" }} /> <Thumbnails images={message.images} style={{ marginTop: "8px" }} />
)} )}

View File

@@ -787,6 +787,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
isLast={index === groupedMessages.length - 1} isLast={index === groupedMessages.length - 1}
lastModifiedMessage={modifiedMessages.at(-1)} lastModifiedMessage={modifiedMessages.at(-1)}
onHeightChange={handleRowHeightChange} onHeightChange={handleRowHeightChange}
isStreaming={isStreaming}
// Pass handlers for each message in the group // Pass handlers for each message in the group
isExpanded={(messageTs: number) => expandedRows[messageTs] ?? false} isExpanded={(messageTs: number) => expandedRows[messageTs] ?? false}
onToggleExpand={(messageTs: number) => { onToggleExpand={(messageTs: number) => {
@@ -809,10 +810,11 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
lastModifiedMessage={modifiedMessages.at(-1)} lastModifiedMessage={modifiedMessages.at(-1)}
isLast={index === groupedMessages.length - 1} isLast={index === groupedMessages.length - 1}
onHeightChange={handleRowHeightChange} onHeightChange={handleRowHeightChange}
isStreaming={isStreaming}
/> />
) )
}, },
[expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange], [expandedRows, modifiedMessages, groupedMessages.length, handleRowHeightChange, isStreaming, toggleRowExpansion],
) )
useEffect(() => { useEffect(() => {