From 362105fe7828c74d84cf07c65501e97a0fadde30 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Wed, 25 Feb 2026 22:49:14 +0000 Subject: [PATCH] feat(ui): add delete message action to stream --- packages/ui/src/components/message-block.tsx | 213 ++++++++++++++++-- packages/ui/src/components/message-item.tsx | 46 +++- .../ui/src/lib/i18n/messages/en/messaging.ts | 8 +- .../ui/src/lib/i18n/messages/es/messaging.ts | 8 +- .../ui/src/lib/i18n/messages/fr/messaging.ts | 8 +- .../ui/src/lib/i18n/messages/ja/messaging.ts | 8 +- .../ui/src/lib/i18n/messages/ru/messaging.ts | 8 +- .../lib/i18n/messages/zh-Hans/messaging.ts | 8 +- packages/ui/src/stores/session-actions.ts | 27 ++- 9 files changed, 295 insertions(+), 39 deletions(-) diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index 50f9f4b9..e065be82 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -1,5 +1,5 @@ import { For, Match, Show, Switch, createEffect, createMemo, createSignal, untrack } from "solid-js" -import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, Trash2 } from "lucide-solid" +import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, MessageSquareX, Trash2 } from "lucide-solid" import MessageItem from "./message-item" import ToolCall from "./tool-call" import type { InstanceMessageStore } from "../stores/message-v2/instance-store" @@ -13,6 +13,7 @@ import { sessions, setActiveParentSession, setActiveSession } from "../stores/se import { setActiveInstanceId } from "../stores/instances" import { showAlertDialog } from "../stores/alerts" import { deleteMessagePart } from "../stores/session-actions" +import { deleteMessage } from "../stores/session-actions" import { useI18n } from "../lib/i18n" const TOOL_ICON = "🔧" @@ -196,6 +197,7 @@ interface MessageContentItemProps { onRevert?: (messageId: string) => void onFork?: (messageId?: string) => void onContentRendered?: () => void + showDeleteMessage?: boolean } function isSupportedPartType(part: unknown): boolean { @@ -282,6 +284,7 @@ function MessageContentItem(props: MessageContentItemProps) { sessionId={props.sessionId} isQueued={isQueued()} showAgentMeta={showAgentMeta()} + showDeleteMessage={props.showDeleteMessage} onRevert={props.onRevert} onFork={props.onFork} onContentRendered={props.onContentRendered} @@ -298,11 +301,13 @@ interface ToolCallItemProps { messageId: string partId: string onContentRendered?: () => void + showDeleteMessage?: boolean } function ToolCallItem(props: ToolCallItemProps) { const { t } = useI18n() const [deleting, setDeleting] = createSignal(false) + const [deletingMessage, setDeletingMessage] = createSignal(false) const record = createMemo(() => props.store().getMessage(props.messageId)) const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId)) @@ -370,6 +375,27 @@ function ToolCallItem(props: ToolCallItemProps) { } } + const handleDeleteMessage = async (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + + if (!props.showDeleteMessage) return + if (deletingMessage()) return + + setDeletingMessage(true) + try { + await deleteMessage(props.instanceId, props.sessionId, props.messageId) + } catch (error) { + showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), { + title: t("messageItem.actions.deleteMessageFailedTitle"), + detail: error instanceof Error ? error.message : String(error), + variant: "error", + }) + } finally { + setDeletingMessage(false) + } + } + return ( {(resolvedToolPart) => ( @@ -381,7 +407,7 @@ function ToolCallItem(props: ToolCallItemProps) { {toolName() || t("messageBlock.tool.unknown")} -
+
+ + + + + +
- (
- {(item) => ( + {(item, index) => (
@@ -709,6 +750,10 @@ export default function MessageBlock(props: MessageBlockProps) { part={(item as StepDisplayItem).part} messageInfo={(item as StepDisplayItem).messageInfo} showAgentMeta + showDeleteMessage={index() === 0} + instanceId={props.instanceId} + sessionId={props.sessionId} + messageId={props.messageId} /> @@ -718,6 +763,10 @@ export default function MessageBlock(props: MessageBlockProps) { messageInfo={(item as StepDisplayItem).messageInfo} showUsage={props.showUsageMetrics()} borderColor={(item as StepDisplayItem).accentColor} + showDeleteMessage={index() === 0} + instanceId={props.instanceId} + sessionId={props.sessionId} + messageId={props.messageId} /> @@ -729,6 +778,7 @@ export default function MessageBlock(props: MessageBlockProps) { sessionId={props.sessionId} messageId={(item as CompactionDisplayItem).messageId} partId={(item as CompactionDisplayItem).partId} + showDeleteMessage={index() === 0} /> @@ -741,6 +791,7 @@ export default function MessageBlock(props: MessageBlockProps) { partId={(item as ReasoningDisplayItem).partId} showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta} defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded} + showDeleteMessage={index() === 0} /> @@ -759,6 +810,10 @@ interface StepCardProps { showAgentMeta?: boolean showUsage?: boolean borderColor?: string + showDeleteMessage?: boolean + instanceId?: string + sessionId?: string + messageId?: string } interface CompactionCardProps { @@ -769,11 +824,13 @@ interface CompactionCardProps { sessionId: string messageId: string partId: string + showDeleteMessage?: boolean } function CompactionCard(props: CompactionCardProps) { const { t } = useI18n() const [deleting, setDeleting] = createSignal(false) + const [deletingMessage, setDeletingMessage] = createSignal(false) const isAuto = () => Boolean((props.part as any)?.auto) const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel")) const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR) @@ -801,6 +858,27 @@ function CompactionCard(props: CompactionCardProps) { } } + const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage() + + const handleDeleteMessage = async (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + if (!props.showDeleteMessage) return + if (!canDeleteMessage()) return + setDeletingMessage(true) + try { + await deleteMessage(props.instanceId, props.sessionId, props.messageId) + } catch (error) { + showAlertDialog(t("messageItem.actions.deleteMessageFailedMessage"), { + title: t("messageItem.actions.deleteMessageFailedTitle"), + detail: error instanceof Error ? error.message : String(error), + variant: "error", + }) + } finally { + setDeletingMessage(false) + } + } + return (
- +
+ + + + + +
diff --git a/packages/ui/src/lib/i18n/messages/en/messaging.ts b/packages/ui/src/lib/i18n/messages/en/messaging.ts index 61783e6f..1763ec71 100644 --- a/packages/ui/src/lib/i18n/messages/en/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/en/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Go to Session", "messageBlock.tool.goToSession.title": "Go to session", "messageBlock.tool.goToSession.unavailableTitle": "Session not available yet", - "messageBlock.tool.deletePart.label": "Delete", + "messageBlock.tool.deletePart.label": "Delete Part", "messageBlock.tool.deletePart.deleting": "Deleting...", "messageBlock.tool.deletePart.title": "Delete this tool call output", "messageBlock.tool.deletePart.failed.title": "Delete failed", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "Copy", "messageItem.actions.copyTitle": "Copy message", "messageItem.actions.copied": "Copied!", + "messageItem.actions.deleteMessage": "Delete message", + "messageItem.actions.deletingMessage": "Deleting...", + "messageItem.actions.deleteMessageFailedTitle": "Delete failed", + "messageItem.actions.deleteMessageFailedMessage": "Failed to delete message", "messageItem.status.queued": "QUEUED", "messageItem.status.generating": "Generating...", "messageItem.status.sending": "Sending...", "messageItem.status.failedToSend": "Message failed to send", - "messagePart.actions.delete": "Delete", + "messagePart.actions.delete": "Delete Part", "messagePart.actions.deleting": "Deleting...", "messagePart.actions.deleteTitle": "Delete this item", "messagePart.actions.deleteFailedTitle": "Delete failed", diff --git a/packages/ui/src/lib/i18n/messages/es/messaging.ts b/packages/ui/src/lib/i18n/messages/es/messaging.ts index dd10f351..8391aa8e 100644 --- a/packages/ui/src/lib/i18n/messages/es/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/es/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Ir a sesión", "messageBlock.tool.goToSession.title": "Ir a la sesión", "messageBlock.tool.goToSession.unavailableTitle": "La sesión aún no está disponible", - "messageBlock.tool.deletePart.label": "Eliminar", + "messageBlock.tool.deletePart.label": "Eliminar parte", "messageBlock.tool.deletePart.deleting": "Eliminando...", "messageBlock.tool.deletePart.title": "Eliminar esta salida de herramienta", "messageBlock.tool.deletePart.failed.title": "Error al eliminar", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "Copiar", "messageItem.actions.copyTitle": "Copiar mensaje", "messageItem.actions.copied": "¡Copiado!", + "messageItem.actions.deleteMessage": "Eliminar mensaje", + "messageItem.actions.deletingMessage": "Eliminando...", + "messageItem.actions.deleteMessageFailedTitle": "Error al eliminar", + "messageItem.actions.deleteMessageFailedMessage": "No se pudo eliminar el mensaje", "messageItem.status.queued": "EN COLA", "messageItem.status.generating": "Generando...", "messageItem.status.sending": "Enviando...", "messageItem.status.failedToSend": "No se pudo enviar el mensaje", - "messagePart.actions.delete": "Eliminar", + "messagePart.actions.delete": "Eliminar parte", "messagePart.actions.deleting": "Eliminando...", "messagePart.actions.deleteTitle": "Eliminar este elemento", "messagePart.actions.deleteFailedTitle": "Error al eliminar", diff --git a/packages/ui/src/lib/i18n/messages/fr/messaging.ts b/packages/ui/src/lib/i18n/messages/fr/messaging.ts index 6bb886ae..ca678355 100644 --- a/packages/ui/src/lib/i18n/messages/fr/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/fr/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Aller à la session", "messageBlock.tool.goToSession.title": "Aller à la session", "messageBlock.tool.goToSession.unavailableTitle": "Session pas encore disponible", - "messageBlock.tool.deletePart.label": "Supprimer", + "messageBlock.tool.deletePart.label": "Supprimer la partie", "messageBlock.tool.deletePart.deleting": "Suppression...", "messageBlock.tool.deletePart.title": "Supprimer cette sortie d'outil", "messageBlock.tool.deletePart.failed.title": "Échec de suppression", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "Copier", "messageItem.actions.copyTitle": "Copier le message", "messageItem.actions.copied": "Copié !", + "messageItem.actions.deleteMessage": "Supprimer le message", + "messageItem.actions.deletingMessage": "Suppression...", + "messageItem.actions.deleteMessageFailedTitle": "Échec de suppression", + "messageItem.actions.deleteMessageFailedMessage": "Impossible de supprimer le message", "messageItem.status.queued": "EN FILE", "messageItem.status.generating": "Génération...", "messageItem.status.sending": "Envoi...", "messageItem.status.failedToSend": "Échec de l'envoi du message", - "messagePart.actions.delete": "Supprimer", + "messagePart.actions.delete": "Supprimer la partie", "messagePart.actions.deleting": "Suppression...", "messagePart.actions.deleteTitle": "Supprimer cet élément", "messagePart.actions.deleteFailedTitle": "Échec de suppression", diff --git a/packages/ui/src/lib/i18n/messages/ja/messaging.ts b/packages/ui/src/lib/i18n/messages/ja/messaging.ts index 456d9f4b..ebbeb3c0 100644 --- a/packages/ui/src/lib/i18n/messages/ja/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ja/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "セッションへ移動", "messageBlock.tool.goToSession.title": "セッションへ移動", "messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません", - "messageBlock.tool.deletePart.label": "削除", + "messageBlock.tool.deletePart.label": "パートを削除", "messageBlock.tool.deletePart.deleting": "削除中...", "messageBlock.tool.deletePart.title": "このツール出力を削除", "messageBlock.tool.deletePart.failed.title": "削除に失敗しました", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "コピー", "messageItem.actions.copyTitle": "メッセージをコピー", "messageItem.actions.copied": "コピーしました!", + "messageItem.actions.deleteMessage": "メッセージを削除", + "messageItem.actions.deletingMessage": "削除中...", + "messageItem.actions.deleteMessageFailedTitle": "削除に失敗しました", + "messageItem.actions.deleteMessageFailedMessage": "メッセージの削除に失敗しました", "messageItem.status.queued": "待機中", "messageItem.status.generating": "生成中...", "messageItem.status.sending": "送信中...", "messageItem.status.failedToSend": "メッセージの送信に失敗しました", - "messagePart.actions.delete": "削除", + "messagePart.actions.delete": "パートを削除", "messagePart.actions.deleting": "削除中...", "messagePart.actions.deleteTitle": "この項目を削除", "messagePart.actions.deleteFailedTitle": "削除に失敗しました", diff --git a/packages/ui/src/lib/i18n/messages/ru/messaging.ts b/packages/ui/src/lib/i18n/messages/ru/messaging.ts index 4021c18b..a88c665e 100644 --- a/packages/ui/src/lib/i18n/messages/ru/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ru/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Перейти к сессии", "messageBlock.tool.goToSession.title": "Перейти к сессии", "messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна", - "messageBlock.tool.deletePart.label": "Удалить", + "messageBlock.tool.deletePart.label": "Удалить часть", "messageBlock.tool.deletePart.deleting": "Удаление...", "messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента", "messageBlock.tool.deletePart.failed.title": "Ошибка удаления", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "Копировать", "messageItem.actions.copyTitle": "Копировать сообщение", "messageItem.actions.copied": "Скопировано!", + "messageItem.actions.deleteMessage": "Удалить сообщение", + "messageItem.actions.deletingMessage": "Удаление...", + "messageItem.actions.deleteMessageFailedTitle": "Ошибка удаления", + "messageItem.actions.deleteMessageFailedMessage": "Не удалось удалить сообщение", "messageItem.status.queued": "В ОЧЕРЕДИ", "messageItem.status.generating": "Генерация…", "messageItem.status.sending": "Отправка…", "messageItem.status.failedToSend": "Не удалось отправить сообщение", - "messagePart.actions.delete": "Удалить", + "messagePart.actions.delete": "Удалить часть", "messagePart.actions.deleting": "Удаление...", "messagePart.actions.deleteTitle": "Удалить этот элемент", "messagePart.actions.deleteFailedTitle": "Ошибка удаления", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts index e8a252af..8fe6f71b 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts @@ -41,7 +41,7 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "前往会话", "messageBlock.tool.goToSession.title": "前往会话", "messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用", - "messageBlock.tool.deletePart.label": "删除", + "messageBlock.tool.deletePart.label": "删除部分", "messageBlock.tool.deletePart.deleting": "正在删除...", "messageBlock.tool.deletePart.title": "删除此工具输出", "messageBlock.tool.deletePart.failed.title": "删除失败", @@ -77,11 +77,15 @@ export const messagingMessages = { "messageItem.actions.copy": "复制", "messageItem.actions.copyTitle": "复制消息", "messageItem.actions.copied": "已复制!", + "messageItem.actions.deleteMessage": "删除消息", + "messageItem.actions.deletingMessage": "正在删除...", + "messageItem.actions.deleteMessageFailedTitle": "删除失败", + "messageItem.actions.deleteMessageFailedMessage": "无法删除消息", "messageItem.status.queued": "排队中", "messageItem.status.generating": "正在生成...", "messageItem.status.sending": "正在发送...", "messageItem.status.failedToSend": "消息发送失败", - "messagePart.actions.delete": "删除", + "messagePart.actions.delete": "删除部分", "messagePart.actions.deleting": "正在删除...", "messagePart.actions.deleteTitle": "删除此项", "messagePart.actions.deleteFailedTitle": "删除失败", diff --git a/packages/ui/src/stores/session-actions.ts b/packages/ui/src/stores/session-actions.ts index 456f5456..f94f6ce7 100644 --- a/packages/ui/src/stores/session-actions.ts +++ b/packages/ui/src/stores/session-actions.ts @@ -7,7 +7,7 @@ import { providers, sessions, withSession } from "./session-state" import { getDefaultModel, isModelValid } from "./session-models" import { updateSessionInfo } from "./message-v2/session-info" import { messageStoreBus } from "./message-v2/bus" -import { removeMessagePartV2 } from "./message-v2/bridge" +import { removeMessagePartV2, removeMessageV2 } from "./message-v2/bridge" import { getLogger } from "../lib/logger" import { requestData } from "../lib/opencode-api" @@ -439,8 +439,33 @@ async function deleteMessagePart(instanceId: string, sessionId: string, messageI updateSessionInfo(instanceId, sessionId) } +async function deleteMessage(instanceId: string, sessionId: string, messageId: string): Promise { + if (!instanceId || !sessionId || !messageId) return + const instance = instances().get(instanceId) + if (!instance || !instance.client) { + throw new Error("Instance not ready") + } + + const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId) + const client = getOrCreateWorktreeClient(instanceId, worktreeSlug) + + // The SDK generator does not currently expose a typed method for deleting a message, + // but the API is available at DELETE /session/:sessionID/message/:messageID. + await requestData( + (client as any).client.delete({ + url: `/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(messageId)}`, + }), + "session.message.delete", + ) + + // Optimistic removal; SSE will also broadcast a message-removed event. + removeMessageV2(instanceId, messageId) + updateSessionInfo(instanceId, sessionId) +} + export { abortSession, + deleteMessage, deleteMessagePart, executeCustomCommand, renameSession,