From 0261154a5eaae4933af23ac717063af75731c4d3 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Feb 2026 18:32:54 +0000 Subject: [PATCH] feat(ui): add delete action for message parts --- packages/ui/src/components/message-block.tsx | 178 ++++++++++++++++-- packages/ui/src/components/message-item.tsx | 107 ++++++++++- packages/ui/src/components/message-part.tsx | 31 +-- .../ui/src/lib/i18n/messages/en/messaging.ts | 10 + .../ui/src/lib/i18n/messages/es/messaging.ts | 10 + .../ui/src/lib/i18n/messages/fr/messaging.ts | 10 + .../ui/src/lib/i18n/messages/ja/messaging.ts | 10 + .../ui/src/lib/i18n/messages/ru/messaging.ts | 10 + .../lib/i18n/messages/zh-Hans/messaging.ts | 10 + packages/ui/src/stores/session-actions.ts | 23 +++ 10 files changed, 363 insertions(+), 36 deletions(-) diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index a9a01eea..0a2de252 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -11,6 +11,8 @@ import { messageStoreBus } from "../stores/message-v2/bus" import { formatTokenTotal } from "../lib/formatters" import { sessions, setActiveParentSession, setActiveSession } from "../stores/sessions" import { setActiveInstanceId } from "../stores/instances" +import { showAlertDialog } from "../stores/alerts" +import { deleteMessagePart } from "../stores/session-actions" import { useI18n } from "../lib/i18n" const TOOL_ICON = "🔧" @@ -302,6 +304,7 @@ interface ToolCallItemProps { function ToolCallItem(props: ToolCallItemProps) { const { t } = useI18n() + const [deleting, setDeleting] = createSignal(false) const record = createMemo(() => props.store().getMessage(props.messageId)) const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId)) @@ -318,6 +321,14 @@ function ToolCallItem(props: ToolCallItemProps) { const messageVersion = createMemo(() => record()?.revision ?? 0) const partVersion = createMemo(() => partEntry()?.revision ?? 0) + const deleteDisabled = createMemo(() => { + if (deleting()) return true + // Avoid deleting while a tool is actively running to prevent confusing UI states. + if (isToolStateRunning(toolState())) return true + // Avoid deleting permission prompts from here; those are interactive. + return Boolean(toolPart()?.pendingPermission) + }) + const taskSessionId = createMemo(() => { const state = toolState() if (!state) return "" @@ -341,6 +352,26 @@ function ToolCallItem(props: ToolCallItemProps) { navigateToTaskSession(location) } + const handleDeleteToolPart = async (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + + if (deleteDisabled()) return + + setDeleting(true) + try { + await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId) + } catch (error) { + showAlertDialog(t("messageBlock.tool.deletePart.failed.message"), { + title: t("messageBlock.tool.deletePart.failed.title"), + detail: error instanceof Error ? error.message : String(error), + variant: "error", + }) + } finally { + setDeleting(false) + } + } + return ( {(resolvedToolPart) => ( @@ -351,17 +382,30 @@ function ToolCallItem(props: ToolCallItemProps) { {t("messageBlock.tool.header")} {toolName() || t("messageBlock.tool.unknown")} - + +
+ + + + - +
- + - + @@ -689,8 +758,19 @@ interface StepCardProps { borderColor?: string } -function CompactionCard(props: { part: ClientPart; messageInfo?: MessageInfo; borderColor?: string }) { +interface CompactionCardProps { + part: ClientPart + messageInfo?: MessageInfo + borderColor?: string + instanceId: string + sessionId: string + messageId: string + partId: string +} + +function CompactionCard(props: CompactionCardProps) { const { t } = useI18n() + const [deleting, setDeleting] = 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) @@ -698,13 +778,43 @@ function CompactionCard(props: { part: ClientPart; messageInfo?: MessageInfo; bo const containerClass = () => `message-compaction-card ${isAuto() ? "message-compaction-card--auto" : "message-compaction-card--manual"}` + const canDelete = () => Boolean(props.partId) && !deleting() + + const handleDelete = async (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + if (!canDelete()) return + setDeleting(true) + try { + await deleteMessagePart(props.instanceId, props.sessionId, props.messageId, props.partId) + } catch (error) { + showAlertDialog(t("messagePart.actions.deleteFailedMessage"), { + title: t("messagePart.actions.deleteFailedTitle"), + detail: error instanceof Error ? error.message : String(error), + variant: "error", + }) + } finally { + setDeleting(false) + } + } + return (
+ +
@@ -337,6 +413,19 @@ export default function MessageItem(props: MessageItemProps) { + +
{name} diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 1256d353..3f8405d4 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -15,7 +15,7 @@ interface MessagePartProps { sessionId: string onRendered?: () => void } - export default function MessagePart(props: MessagePartProps) { + export default function MessagePart(props: MessagePartProps) { const { isDark } = useTheme() const { preferences } = useConfig() @@ -32,6 +32,7 @@ interface MessagePartProps { return Boolean((part as any).synthetic) && props.messageType !== "user" } + const plainTextContent = () => { const part = props.part @@ -103,21 +104,21 @@ interface MessagePartProps {
- {plainTextContent()}} - > - - + {plainTextContent()}} + > + + -
+
diff --git a/packages/ui/src/lib/i18n/messages/en/messaging.ts b/packages/ui/src/lib/i18n/messages/en/messaging.ts index 1da24bb1..b2c8568a 100644 --- a/packages/ui/src/lib/i18n/messages/en/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/en/messaging.ts @@ -38,6 +38,11 @@ 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.deleting": "Deleting...", + "messageBlock.tool.deletePart.title": "Delete this tool call output", + "messageBlock.tool.deletePart.failed.title": "Delete failed", + "messageBlock.tool.deletePart.failed.message": "Failed to delete tool call output", "messageBlock.compaction.ariaLabel": "Session compaction", "messageBlock.compaction.autoLabel": "Session auto-compacted", @@ -73,6 +78,11 @@ export const messagingMessages = { "messageItem.status.generating": "Generating...", "messageItem.status.sending": "Sending...", "messageItem.status.failedToSend": "Message failed to send", + "messagePart.actions.delete": "Delete", + "messagePart.actions.deleting": "Deleting...", + "messagePart.actions.deleteTitle": "Delete this item", + "messagePart.actions.deleteFailedTitle": "Delete failed", + "messagePart.actions.deleteFailedMessage": "Failed to delete item", "messageItem.attachment.defaultName": "attachment", "messageItem.attachment.downloadAriaLabel": "Download {name}", "messageItem.agentMeta.agentLabel": "Agent: {agent}", diff --git a/packages/ui/src/lib/i18n/messages/es/messaging.ts b/packages/ui/src/lib/i18n/messages/es/messaging.ts index 5cca187a..1c1da653 100644 --- a/packages/ui/src/lib/i18n/messages/es/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/es/messaging.ts @@ -38,6 +38,11 @@ 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.deleting": "Eliminando...", + "messageBlock.tool.deletePart.title": "Eliminar esta salida de herramienta", + "messageBlock.tool.deletePart.failed.title": "Error al eliminar", + "messageBlock.tool.deletePart.failed.message": "No se pudo eliminar la salida de herramienta", "messageBlock.compaction.ariaLabel": "Compactación de sesión", "messageBlock.compaction.autoLabel": "Sesión compactada automáticamente", @@ -73,6 +78,11 @@ export const messagingMessages = { "messageItem.status.generating": "Generando...", "messageItem.status.sending": "Enviando...", "messageItem.status.failedToSend": "No se pudo enviar el mensaje", + "messagePart.actions.delete": "Eliminar", + "messagePart.actions.deleting": "Eliminando...", + "messagePart.actions.deleteTitle": "Eliminar este elemento", + "messagePart.actions.deleteFailedTitle": "Error al eliminar", + "messagePart.actions.deleteFailedMessage": "No se pudo eliminar el elemento", "messageItem.attachment.defaultName": "adjunto", "messageItem.attachment.downloadAriaLabel": "Descargar {name}", "messageItem.agentMeta.agentLabel": "Agente: {agent}", diff --git a/packages/ui/src/lib/i18n/messages/fr/messaging.ts b/packages/ui/src/lib/i18n/messages/fr/messaging.ts index 0e786925..ef8fecf1 100644 --- a/packages/ui/src/lib/i18n/messages/fr/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/fr/messaging.ts @@ -38,6 +38,11 @@ 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.deleting": "Suppression...", + "messageBlock.tool.deletePart.title": "Supprimer cette sortie d'outil", + "messageBlock.tool.deletePart.failed.title": "Échec de suppression", + "messageBlock.tool.deletePart.failed.message": "Impossible de supprimer la sortie d'outil", "messageBlock.compaction.ariaLabel": "Compaction de la session", "messageBlock.compaction.autoLabel": "Session compactée automatiquement", @@ -73,6 +78,11 @@ export const messagingMessages = { "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.deleting": "Suppression...", + "messagePart.actions.deleteTitle": "Supprimer cet élément", + "messagePart.actions.deleteFailedTitle": "Échec de suppression", + "messagePart.actions.deleteFailedMessage": "Impossible de supprimer l'élément", "messageItem.attachment.defaultName": "piece-jointe", "messageItem.attachment.downloadAriaLabel": "Télécharger {name}", "messageItem.agentMeta.agentLabel": "Agent : {agent}", diff --git a/packages/ui/src/lib/i18n/messages/ja/messaging.ts b/packages/ui/src/lib/i18n/messages/ja/messaging.ts index f9aa26ed..25823eb9 100644 --- a/packages/ui/src/lib/i18n/messages/ja/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ja/messaging.ts @@ -38,6 +38,11 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "セッションへ移動", "messageBlock.tool.goToSession.title": "セッションへ移動", "messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません", + "messageBlock.tool.deletePart.label": "削除", + "messageBlock.tool.deletePart.deleting": "削除中...", + "messageBlock.tool.deletePart.title": "このツール出力を削除", + "messageBlock.tool.deletePart.failed.title": "削除に失敗しました", + "messageBlock.tool.deletePart.failed.message": "ツール出力の削除に失敗しました", "messageBlock.compaction.ariaLabel": "セッションのコンパクト化", "messageBlock.compaction.autoLabel": "セッションを自動でコンパクト化しました", @@ -73,6 +78,11 @@ export const messagingMessages = { "messageItem.status.generating": "生成中...", "messageItem.status.sending": "送信中...", "messageItem.status.failedToSend": "メッセージの送信に失敗しました", + "messagePart.actions.delete": "削除", + "messagePart.actions.deleting": "削除中...", + "messagePart.actions.deleteTitle": "この項目を削除", + "messagePart.actions.deleteFailedTitle": "削除に失敗しました", + "messagePart.actions.deleteFailedMessage": "項目の削除に失敗しました", "messageItem.attachment.defaultName": "添付ファイル", "messageItem.attachment.downloadAriaLabel": "{name} をダウンロード", "messageItem.agentMeta.agentLabel": "エージェント: {agent}", diff --git a/packages/ui/src/lib/i18n/messages/ru/messaging.ts b/packages/ui/src/lib/i18n/messages/ru/messaging.ts index eeabe2f9..3fe9e15d 100644 --- a/packages/ui/src/lib/i18n/messages/ru/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ru/messaging.ts @@ -38,6 +38,11 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "Перейти к сессии", "messageBlock.tool.goToSession.title": "Перейти к сессии", "messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна", + "messageBlock.tool.deletePart.label": "Удалить", + "messageBlock.tool.deletePart.deleting": "Удаление...", + "messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента", + "messageBlock.tool.deletePart.failed.title": "Ошибка удаления", + "messageBlock.tool.deletePart.failed.message": "Не удалось удалить вывод инструмента", "messageBlock.compaction.ariaLabel": "Компактация сессии", "messageBlock.compaction.autoLabel": "Сессия автоматически компактирована", @@ -73,6 +78,11 @@ export const messagingMessages = { "messageItem.status.generating": "Генерация…", "messageItem.status.sending": "Отправка…", "messageItem.status.failedToSend": "Не удалось отправить сообщение", + "messagePart.actions.delete": "Удалить", + "messagePart.actions.deleting": "Удаление...", + "messagePart.actions.deleteTitle": "Удалить этот элемент", + "messagePart.actions.deleteFailedTitle": "Ошибка удаления", + "messagePart.actions.deleteFailedMessage": "Не удалось удалить элемент", "messageItem.attachment.defaultName": "вложение", "messageItem.attachment.downloadAriaLabel": "Скачать {name}", "messageItem.agentMeta.agentLabel": "Агент: {agent}", 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 1946d076..93f7a15d 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts @@ -38,6 +38,11 @@ export const messagingMessages = { "messageBlock.tool.goToSession.label": "前往会话", "messageBlock.tool.goToSession.title": "前往会话", "messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用", + "messageBlock.tool.deletePart.label": "删除", + "messageBlock.tool.deletePart.deleting": "正在删除...", + "messageBlock.tool.deletePart.title": "删除此工具输出", + "messageBlock.tool.deletePart.failed.title": "删除失败", + "messageBlock.tool.deletePart.failed.message": "删除工具输出失败", "messageBlock.compaction.ariaLabel": "会话压缩", "messageBlock.compaction.autoLabel": "会话已自动压缩", @@ -73,6 +78,11 @@ export const messagingMessages = { "messageItem.status.generating": "正在生成...", "messageItem.status.sending": "正在发送...", "messageItem.status.failedToSend": "消息发送失败", + "messagePart.actions.delete": "删除", + "messagePart.actions.deleting": "正在删除...", + "messagePart.actions.deleteTitle": "删除此项", + "messagePart.actions.deleteFailedTitle": "删除失败", + "messagePart.actions.deleteFailedMessage": "删除失败", "messageItem.attachment.defaultName": "附件", "messageItem.attachment.downloadAriaLabel": "下载 {name}", "messageItem.agentMeta.agentLabel": "智能体:{agent}", diff --git a/packages/ui/src/stores/session-actions.ts b/packages/ui/src/stores/session-actions.ts index 29f21129..ff8e6a2f 100644 --- a/packages/ui/src/stores/session-actions.ts +++ b/packages/ui/src/stores/session-actions.ts @@ -6,6 +6,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 { getLogger } from "../lib/logger" import { requestData } from "../lib/opencode-api" @@ -395,8 +396,30 @@ async function renameSession(instanceId: string, sessionId: string, nextTitle: s }) } +async function deleteMessagePart(instanceId: string, sessionId: string, messageId: string, partId: string): Promise { + if (!instanceId || !sessionId || !messageId || !partId) return + const instance = instances().get(instanceId) + if (!instance || !instance.client) { + throw new Error("Instance not ready") + } + + await requestData( + instance.client.part.delete({ + sessionID: sessionId, + messageID: messageId, + partID: partId, + }), + "part.delete", + ) + + // Optimistic removal; SSE will also broadcast a part-removed event. + removeMessagePartV2(instanceId, messageId, partId) + updateSessionInfo(instanceId, sessionId) +} + export { abortSession, + deleteMessagePart, executeCustomCommand, renameSession, runShellCommand,