Skip to content

Commit 12f51bb

Browse files
committed
feat: Add delete functionality for AI messages and subsequent nodes
- Added delete button to AI text messages, API request messages, and completion messages - Implemented right-click context menu for delete operation - Reuses existing backend deletion logic that removes selected message and all subsequent messages - Only shows delete controls when message is not streaming Fixes #9035
1 parent 54745fc commit 12f51bb

File tree

1 file changed

+141
-47
lines changed

1 file changed

+141
-47
lines changed

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 141 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import { findMatchingResourceOrTemplate } from "@src/utils/mcp"
1616
import { vscode } from "@src/utils/vscode"
1717
import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
1818
import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
19+
import {
20+
DropdownMenu,
21+
DropdownMenuContent,
22+
DropdownMenuItem,
23+
DropdownMenuTrigger,
24+
} from "@src/components/ui/dropdown-menu"
1925

2026
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
2127
import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
@@ -1002,29 +1008,63 @@ export const ChatRowContent = ({
10021008

10031009
return (
10041010
<>
1005-
<div
1006-
className={`group text-sm transition-opacity ${
1007-
isApiRequestInProgress ? "opacity-100" : "opacity-40 hover:opacity-100"
1008-
}`}
1009-
style={{
1010-
...headerStyle,
1011-
marginBottom:
1012-
((cost === null || cost === undefined) && apiRequestFailedMessage) ||
1013-
apiReqStreamingFailedMessage
1014-
? 10
1015-
: 0,
1016-
justifyContent: "space-between",
1017-
}}>
1018-
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
1019-
{icon}
1020-
{title}
1021-
</div>
1022-
<div
1023-
className="text-xs text-vscode-dropdown-foreground border-vscode-dropdown-border/50 border px-1.5 py-0.5 rounded-lg"
1024-
style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}>
1025-
${Number(cost || 0)?.toFixed(4)}
1026-
</div>
1027-
</div>
1011+
<DropdownMenu>
1012+
<DropdownMenuTrigger asChild>
1013+
<div
1014+
className={`group text-sm transition-opacity ${
1015+
isApiRequestInProgress ? "opacity-100" : "opacity-40 hover:opacity-100"
1016+
}`}
1017+
style={{
1018+
...headerStyle,
1019+
marginBottom:
1020+
((cost === null || cost === undefined) && apiRequestFailedMessage) ||
1021+
apiReqStreamingFailedMessage
1022+
? 10
1023+
: 0,
1024+
justifyContent: "space-between",
1025+
}}
1026+
onContextMenu={(e) => e.preventDefault()}>
1027+
<div
1028+
style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
1029+
{icon}
1030+
{title}
1031+
</div>
1032+
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
1033+
<div
1034+
className="text-xs text-vscode-dropdown-foreground border-vscode-dropdown-border/50 border px-1.5 py-0.5 rounded-lg"
1035+
style={{
1036+
opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0,
1037+
}}>
1038+
${Number(cost || 0)?.toFixed(4)}
1039+
</div>
1040+
<div
1041+
className="cursor-pointer shrink-0 opacity-0 group-hover:opacity-30 hover:!opacity-100 transition-opacity"
1042+
style={{ visibility: isApiRequestInProgress ? "hidden" : "visible" }}
1043+
onClick={(e) => {
1044+
e.stopPropagation()
1045+
vscode.postMessage({ type: "deleteMessage", value: message.ts })
1046+
}}
1047+
title={t("chat:deleteMessageAndSubsequent")}>
1048+
<Trash2 className="w-4 shrink-0" aria-label="Delete message icon" />
1049+
</div>
1050+
</div>
1051+
</div>
1052+
</DropdownMenuTrigger>
1053+
<DropdownMenuContent>
1054+
<DropdownMenuItem
1055+
onClick={() => vscode.postMessage({ type: "deleteMessage", value: message.ts })}
1056+
className="flex items-center gap-2"
1057+
disabled={isApiRequestInProgress}>
1058+
<Trash2 className="w-4 h-4" />
1059+
<span>
1060+
{t(
1061+
"chat:deleteMessageAndSubsequent",
1062+
"Delete message and subsequent nodes",
1063+
)}
1064+
</span>
1065+
</DropdownMenuItem>
1066+
</DropdownMenuContent>
1067+
</DropdownMenu>
10281068
{(((cost === null || cost === undefined) && apiRequestFailedMessage) ||
10291069
apiReqStreamingFailedMessage) && (
10301070
<ErrorRow
@@ -1053,22 +1093,49 @@ export const ChatRowContent = ({
10531093
return null // we should never see this message type
10541094
case "text":
10551095
return (
1056-
<div>
1057-
<div style={headerStyle}>
1058-
<MessageCircle className="w-4 shrink-0" aria-label="Speech bubble icon" />
1059-
<span style={{ fontWeight: "bold" }}>{t("chat:text.rooSaid")}</span>
1060-
</div>
1061-
<div className="pl-6">
1062-
<Markdown markdown={message.text} partial={message.partial} />
1063-
{message.images && message.images.length > 0 && (
1064-
<div style={{ marginTop: "10px" }}>
1065-
{message.images.map((image, index) => (
1066-
<ImageBlock key={index} imageData={image} />
1067-
))}
1096+
<DropdownMenu>
1097+
<DropdownMenuTrigger asChild>
1098+
<div className="group" onContextMenu={(e) => e.preventDefault()}>
1099+
<div style={{ ...headerStyle, justifyContent: "space-between" }}>
1100+
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
1101+
<MessageCircle className="w-4 shrink-0" aria-label="Speech bubble icon" />
1102+
<span style={{ fontWeight: "bold" }}>{t("chat:text.rooSaid")}</span>
1103+
</div>
1104+
<div
1105+
className="cursor-pointer shrink-0 opacity-0 group-hover:opacity-30 hover:!opacity-100 transition-opacity"
1106+
style={{ visibility: isStreaming ? "hidden" : "visible" }}
1107+
onClick={(e) => {
1108+
e.stopPropagation()
1109+
vscode.postMessage({ type: "deleteMessage", value: message.ts })
1110+
}}
1111+
title={t("chat:deleteMessageAndSubsequent")}>
1112+
<Trash2 className="w-4 shrink-0" aria-label="Delete message icon" />
1113+
</div>
10681114
</div>
1069-
)}
1070-
</div>
1071-
</div>
1115+
<div className="pl-6">
1116+
<Markdown markdown={message.text} partial={message.partial} />
1117+
{message.images && message.images.length > 0 && (
1118+
<div style={{ marginTop: "10px" }}>
1119+
{message.images.map((image, index) => (
1120+
<ImageBlock key={index} imageData={image} />
1121+
))}
1122+
</div>
1123+
)}
1124+
</div>
1125+
</div>
1126+
</DropdownMenuTrigger>
1127+
<DropdownMenuContent>
1128+
<DropdownMenuItem
1129+
onClick={() => vscode.postMessage({ type: "deleteMessage", value: message.ts })}
1130+
className="flex items-center gap-2"
1131+
disabled={isStreaming}>
1132+
<Trash2 className="w-4 h-4" />
1133+
<span>
1134+
{t("chat:deleteMessageAndSubsequent", "Delete message and subsequent nodes")}
1135+
</span>
1136+
</DropdownMenuItem>
1137+
</DropdownMenuContent>
1138+
</DropdownMenu>
10721139
)
10731140
case "user_feedback":
10741141
return (
@@ -1162,15 +1229,42 @@ export const ChatRowContent = ({
11621229
return <ErrorRow type="error" message={message.text || ""} />
11631230
case "completion_result":
11641231
return (
1165-
<>
1166-
<div style={headerStyle}>
1167-
{icon}
1168-
{title}
1169-
</div>
1170-
<div className="border-l border-green-600/30 ml-2 pl-4 pb-1">
1171-
<Markdown markdown={message.text} />
1172-
</div>
1173-
</>
1232+
<DropdownMenu>
1233+
<DropdownMenuTrigger asChild>
1234+
<div className="group" onContextMenu={(e) => e.preventDefault()}>
1235+
<div style={{ ...headerStyle, justifyContent: "space-between" }}>
1236+
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
1237+
{icon}
1238+
{title}
1239+
</div>
1240+
<div
1241+
className="cursor-pointer shrink-0 opacity-0 group-hover:opacity-30 hover:!opacity-100 transition-opacity"
1242+
style={{ visibility: isStreaming ? "hidden" : "visible" }}
1243+
onClick={(e) => {
1244+
e.stopPropagation()
1245+
vscode.postMessage({ type: "deleteMessage", value: message.ts })
1246+
}}
1247+
title={t("chat:deleteMessageAndSubsequent")}>
1248+
<Trash2 className="w-4 shrink-0" aria-label="Delete message icon" />
1249+
</div>
1250+
</div>
1251+
<div className="border-l border-green-600/30 ml-2 pl-4 pb-1">
1252+
<Markdown markdown={message.text} />
1253+
</div>
1254+
</div>
1255+
</DropdownMenuTrigger>
1256+
<DropdownMenuContent>
1257+
<DropdownMenuItem
1258+
onClick={() => vscode.postMessage({ type: "deleteMessage", value: message.ts })}
1259+
className="flex items-center gap-2"
1260+
disabled={isStreaming}>
1261+
<Trash2 className="w-4 h-4" />
1262+
<span>
1263+
{t("chat:deleteMessageAndSubsequent", "Delete message and subsequent nodes")}
1264+
</span>
1265+
</DropdownMenuItem>
1266+
</DropdownMenuContent>
1267+
</DropdownMenu>
11741268
)
11751269
case "shell_integration_warning":
11761270
return <CommandExecutionError />

0 commit comments

Comments
 (0)