feat(ui): add delete message action to stream

This commit is contained in:
Shantur Rathore
2026-02-25 22:49:14 +00:00
parent 5834d2df1b
commit 362105fe78
9 changed files with 295 additions and 39 deletions

View File

@@ -1,5 +1,5 @@
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, untrack } from "solid-js" 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 MessageItem from "./message-item"
import ToolCall from "./tool-call" import ToolCall from "./tool-call"
import type { InstanceMessageStore } from "../stores/message-v2/instance-store" 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 { setActiveInstanceId } from "../stores/instances"
import { showAlertDialog } from "../stores/alerts" import { showAlertDialog } from "../stores/alerts"
import { deleteMessagePart } from "../stores/session-actions" import { deleteMessagePart } from "../stores/session-actions"
import { deleteMessage } from "../stores/session-actions"
import { useI18n } from "../lib/i18n" import { useI18n } from "../lib/i18n"
const TOOL_ICON = "🔧" const TOOL_ICON = "🔧"
@@ -196,6 +197,7 @@ interface MessageContentItemProps {
onRevert?: (messageId: string) => void onRevert?: (messageId: string) => void
onFork?: (messageId?: string) => void onFork?: (messageId?: string) => void
onContentRendered?: () => void onContentRendered?: () => void
showDeleteMessage?: boolean
} }
function isSupportedPartType(part: unknown): boolean { function isSupportedPartType(part: unknown): boolean {
@@ -282,6 +284,7 @@ function MessageContentItem(props: MessageContentItemProps) {
sessionId={props.sessionId} sessionId={props.sessionId}
isQueued={isQueued()} isQueued={isQueued()}
showAgentMeta={showAgentMeta()} showAgentMeta={showAgentMeta()}
showDeleteMessage={props.showDeleteMessage}
onRevert={props.onRevert} onRevert={props.onRevert}
onFork={props.onFork} onFork={props.onFork}
onContentRendered={props.onContentRendered} onContentRendered={props.onContentRendered}
@@ -298,11 +301,13 @@ interface ToolCallItemProps {
messageId: string messageId: string
partId: string partId: string
onContentRendered?: () => void onContentRendered?: () => void
showDeleteMessage?: boolean
} }
function ToolCallItem(props: ToolCallItemProps) { function ToolCallItem(props: ToolCallItemProps) {
const { t } = useI18n() const { t } = useI18n()
const [deleting, setDeleting] = createSignal(false) const [deleting, setDeleting] = createSignal(false)
const [deletingMessage, setDeletingMessage] = createSignal(false)
const record = createMemo(() => props.store().getMessage(props.messageId)) const record = createMemo(() => props.store().getMessage(props.messageId))
const messageInfo = createMemo(() => props.store().getMessageInfo(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 ( return (
<Show when={toolPart()}> <Show when={toolPart()}>
{(resolvedToolPart) => ( {(resolvedToolPart) => (
@@ -381,7 +407,7 @@ function ToolCallItem(props: ToolCallItemProps) {
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span> <span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Show when={taskSessionId()}> <Show when={taskSessionId()}>
<button <button
class="tool-call-header-button" class="tool-call-header-button"
@@ -395,18 +421,31 @@ function ToolCallItem(props: ToolCallItemProps) {
</button> </button>
</Show> </Show>
<button <button
class="tool-call-header-button" class="tool-call-header-button"
type="button" type="button"
disabled={deleteDisabled()} disabled={deleteDisabled()}
onClick={handleDeleteToolPart} onClick={handleDeleteToolPart}
title={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")} title={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
aria-label={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")} aria-label={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
> >
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" /> <Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
</button> </button>
<Show when={props.showDeleteMessage}>
<button
class="tool-call-header-button"
type="button"
disabled={deletingMessage()}
onClick={handleDeleteMessage}
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>
</div>
</div> </div>
</div>
<ToolCall <ToolCall
toolCall={resolvedToolPart()} toolCall={resolvedToolPart()}
@@ -670,7 +709,7 @@ export default function MessageBlock(props: MessageBlockProps) {
{(resolvedBlock) => ( {(resolvedBlock) => (
<div class="message-stream-block" data-message-id={resolvedBlock().record.id}> <div class="message-stream-block" data-message-id={resolvedBlock().record.id}>
<For each={resolvedBlock().items}> <For each={resolvedBlock().items}>
{(item) => ( {(item, index) => (
<Switch> <Switch>
<Match when={item.type === "content"}> <Match when={item.type === "content"}>
<MessageContentItem <MessageContentItem
@@ -681,6 +720,7 @@ export default function MessageBlock(props: MessageBlockProps) {
startPartId={(item as ContentDisplayItem).startPartId} startPartId={(item as ContentDisplayItem).startPartId}
messageIndex={props.messageIndex} messageIndex={props.messageIndex}
lastAssistantIndex={props.lastAssistantIndex} lastAssistantIndex={props.lastAssistantIndex}
showDeleteMessage={index() === 0}
onRevert={props.onRevert} onRevert={props.onRevert}
onFork={props.onFork} onFork={props.onFork}
onContentRendered={props.onContentRendered} onContentRendered={props.onContentRendered}
@@ -697,6 +737,7 @@ export default function MessageBlock(props: MessageBlockProps) {
store={props.store} store={props.store}
messageId={toolItem.messageId} messageId={toolItem.messageId}
partId={toolItem.partId} partId={toolItem.partId}
showDeleteMessage={index() === 0}
onContentRendered={props.onContentRendered} onContentRendered={props.onContentRendered}
/> />
</div> </div>
@@ -709,6 +750,10 @@ export default function MessageBlock(props: MessageBlockProps) {
part={(item as StepDisplayItem).part} part={(item as StepDisplayItem).part}
messageInfo={(item as StepDisplayItem).messageInfo} messageInfo={(item as StepDisplayItem).messageInfo}
showAgentMeta showAgentMeta
showDeleteMessage={index() === 0}
instanceId={props.instanceId}
sessionId={props.sessionId}
messageId={props.messageId}
/> />
</Match> </Match>
<Match when={item.type === "step-finish"}> <Match when={item.type === "step-finish"}>
@@ -718,6 +763,10 @@ export default function MessageBlock(props: MessageBlockProps) {
messageInfo={(item as StepDisplayItem).messageInfo} messageInfo={(item as StepDisplayItem).messageInfo}
showUsage={props.showUsageMetrics()} showUsage={props.showUsageMetrics()}
borderColor={(item as StepDisplayItem).accentColor} borderColor={(item as StepDisplayItem).accentColor}
showDeleteMessage={index() === 0}
instanceId={props.instanceId}
sessionId={props.sessionId}
messageId={props.messageId}
/> />
</Match> </Match>
<Match when={item.type === "compaction"}> <Match when={item.type === "compaction"}>
@@ -729,6 +778,7 @@ export default function MessageBlock(props: MessageBlockProps) {
sessionId={props.sessionId} sessionId={props.sessionId}
messageId={(item as CompactionDisplayItem).messageId} messageId={(item as CompactionDisplayItem).messageId}
partId={(item as CompactionDisplayItem).partId} partId={(item as CompactionDisplayItem).partId}
showDeleteMessage={index() === 0}
/> />
</Match> </Match>
<Match when={item.type === "reasoning"}> <Match when={item.type === "reasoning"}>
@@ -741,6 +791,7 @@ export default function MessageBlock(props: MessageBlockProps) {
partId={(item as ReasoningDisplayItem).partId} partId={(item as ReasoningDisplayItem).partId}
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta} showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded} defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
showDeleteMessage={index() === 0}
/> />
</Match> </Match>
</Switch> </Switch>
@@ -759,6 +810,10 @@ interface StepCardProps {
showAgentMeta?: boolean showAgentMeta?: boolean
showUsage?: boolean showUsage?: boolean
borderColor?: string borderColor?: string
showDeleteMessage?: boolean
instanceId?: string
sessionId?: string
messageId?: string
} }
interface CompactionCardProps { interface CompactionCardProps {
@@ -769,11 +824,13 @@ interface CompactionCardProps {
sessionId: string sessionId: string
messageId: string messageId: string
partId: string partId: string
showDeleteMessage?: boolean
} }
function CompactionCard(props: CompactionCardProps) { function CompactionCard(props: CompactionCardProps) {
const { t } = useI18n() const { t } = useI18n()
const [deleting, setDeleting] = createSignal(false) const [deleting, setDeleting] = createSignal(false)
const [deletingMessage, setDeletingMessage] = createSignal(false)
const isAuto = () => Boolean((props.part as any)?.auto) const isAuto = () => Boolean((props.part as any)?.auto)
const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel")) const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel"))
const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR) 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 ( return (
<div <div
class={`${containerClass()} relative`} class={`${containerClass()} relative`}
@@ -808,15 +886,31 @@ function CompactionCard(props: CompactionCardProps) {
role="status" role="status"
aria-label={t("messageBlock.compaction.ariaLabel")} aria-label={t("messageBlock.compaction.ariaLabel")}
> >
<button <div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
type="button" <Show when={props.showDeleteMessage}>
class="tool-call-header-button absolute right-2 top-1/2 -translate-y-1/2" <button
disabled={!canDelete()} type="button"
onClick={handleDelete} class="tool-call-header-button"
title={t("messagePart.actions.deleteTitle")} disabled={!canDeleteMessage()}
> onClick={handleDeleteMessage}
{deleting() ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")} title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
</button> aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>
<button
type="button"
class="tool-call-header-button"
disabled={!canDelete()}
onClick={handleDelete}
title={t("messagePart.actions.deleteTitle")}
aria-label={t("messagePart.actions.deleteTitle")}
>
{deleting() ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
</button>
</div>
<div class="message-compaction-row"> <div class="message-compaction-row">
<FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" /> <FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" />
@@ -828,6 +922,7 @@ function CompactionCard(props: CompactionCardProps) {
function StepCard(props: StepCardProps) { function StepCard(props: StepCardProps) {
const { t } = useI18n() const { t } = useI18n()
const [deletingMessage, setDeletingMessage] = createSignal(false)
const timestamp = () => { const timestamp = () => {
const value = props.messageInfo?.time?.created ?? (props.part as any)?.time?.start ?? Date.now() const value = props.messageInfo?.time?.created ?? (props.part as any)?.time?.start ?? Date.now()
const date = new Date(value) const date = new Date(value)
@@ -872,6 +967,27 @@ function StepCard(props: StepCardProps) {
const finishStyle = () => (props.borderColor ? { "border-left-color": props.borderColor } : undefined) const finishStyle = () => (props.borderColor ? { "border-left-color": props.borderColor } : undefined)
const canDeleteMessage = () =>
Boolean(props.showDeleteMessage && props.instanceId && props.sessionId && props.messageId) && !deletingMessage()
const handleDeleteMessage = async (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
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)
}
}
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => { const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
const entries = [ const entries = [
@@ -902,7 +1018,20 @@ function StepCard(props: StepCardProps) {
return null return null
} }
return ( return (
<div class={`message-step-card message-step-finish message-step-finish-flush`} style={finishStyle()}> <div class={`message-step-card message-step-finish message-step-finish-flush relative`} style={finishStyle()}>
<Show when={props.showDeleteMessage}>
<button
type="button"
class="message-action-button absolute right-2 top-1/2 -translate-y-1/2"
disabled={!canDeleteMessage()}
onClick={handleDeleteMessage}
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>
{renderUsageChips(usage)} {renderUsageChips(usage)}
</div> </div>
) )
@@ -942,12 +1071,14 @@ interface ReasoningCardProps {
partId: string partId: string
showAgentMeta?: boolean showAgentMeta?: boolean
defaultExpanded?: boolean defaultExpanded?: boolean
showDeleteMessage?: boolean
} }
function ReasoningCard(props: ReasoningCardProps) { function ReasoningCard(props: ReasoningCardProps) {
const { t } = useI18n() const { t } = useI18n()
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded)) const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
const [deleting, setDeleting] = createSignal(false) const [deleting, setDeleting] = createSignal(false)
const [deletingMessage, setDeletingMessage] = createSignal(false)
createEffect(() => { createEffect(() => {
setExpanded(Boolean(props.defaultExpanded)) setExpanded(Boolean(props.defaultExpanded))
@@ -1035,6 +1166,27 @@ function ReasoningCard(props: ReasoningCardProps) {
} }
} }
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 ( return (
<div class="message-reasoning-card"> <div class="message-reasoning-card">
<div class="message-reasoning-header"> <div class="message-reasoning-header">
@@ -1094,6 +1246,19 @@ function ReasoningCard(props: ReasoningCardProps) {
</button> </button>
</Show> </Show>
<Show when={props.showDeleteMessage}>
<button
type="button"
class="message-action-button"
onClick={handleDeleteMessage}
disabled={!canDeleteMessage()}
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>
<span class="message-reasoning-time">{timestamp()}</span> <span class="message-reasoning-time">{timestamp()}</span>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { For, Show, createSignal } from "solid-js" import { For, Show, createSignal } from "solid-js"
import { Copy, ExternalLink, Split, Trash2, Undo } from "lucide-solid" import { Copy, MessageSquareX, Split, Trash2, Undo } from "lucide-solid"
import type { MessageInfo, ClientPart, SDKAssistantMessageV2 } from "../types/message" import type { MessageInfo, ClientPart, SDKAssistantMessageV2 } from "../types/message"
import { partHasRenderableText } from "../types/message" import { partHasRenderableText } from "../types/message"
import type { MessageRecord } from "../stores/message-v2/types" import type { MessageRecord } from "../stores/message-v2/types"
@@ -7,7 +7,7 @@ import MessagePart from "./message-part"
import { copyToClipboard } from "../lib/clipboard" import { copyToClipboard } from "../lib/clipboard"
import { useI18n } from "../lib/i18n" import { useI18n } from "../lib/i18n"
import { showAlertDialog } from "../stores/alerts" import { showAlertDialog } from "../stores/alerts"
import { deleteMessagePart } from "../stores/session-actions" import { deleteMessage, deleteMessagePart } from "../stores/session-actions"
import { isTauriHost } from "../lib/runtime-env" import { isTauriHost } from "../lib/runtime-env"
interface MessageItemProps { interface MessageItemProps {
@@ -21,12 +21,14 @@ interface MessageItemProps {
onFork?: (messageId?: string) => void onFork?: (messageId?: string) => void
showAgentMeta?: boolean showAgentMeta?: boolean
onContentRendered?: () => void onContentRendered?: () => void
showDeleteMessage?: boolean
} }
export default function MessageItem(props: MessageItemProps) { export default function MessageItem(props: MessageItemProps) {
const { t } = useI18n() const { t } = useI18n()
const [copied, setCopied] = createSignal(false) const [copied, setCopied] = createSignal(false)
const [deletingParts, setDeletingParts] = createSignal<Set<string>>(new Set()) const [deletingParts, setDeletingParts] = createSignal<Set<string>>(new Set())
const [deletingMessage, setDeletingMessage] = createSignal(false)
const isUser = () => props.record.role === "user" const isUser = () => props.record.role === "user"
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
@@ -234,6 +236,22 @@ export default function MessageItem(props: MessageItemProps) {
} }
} }
const handleDeleteMessage = async () => {
if (deletingMessage()) return
setDeletingMessage(true)
try {
await deleteMessage(props.instanceId, props.sessionId, props.record.id)
} 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)
}
}
if (!isUser() && !hasContent() && !isGenerating()) { if (!isUser() && !hasContent() && !isGenerating()) {
return null return null
} }
@@ -326,6 +344,18 @@ export default function MessageItem(props: MessageItemProps) {
> >
<Copy class="w-3.5 h-3.5" aria-hidden="true" /> <Copy class="w-3.5 h-3.5" aria-hidden="true" />
</button> </button>
<Show when={props.showDeleteMessage}>
<button
class="message-action-button"
onClick={handleDeleteMessage}
disabled={deletingMessage()}
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>
</div> </div>
</Show> </Show>
<Show when={!isUser()}> <Show when={!isUser()}>
@@ -352,6 +382,18 @@ export default function MessageItem(props: MessageItemProps) {
</button> </button>
)} )}
</Show> </Show>
<Show when={props.showDeleteMessage}>
<button
class="message-action-button"
onClick={handleDeleteMessage}
disabled={deletingMessage()}
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>
<MessageSquareX class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>
</div> </div>
</Show> </Show>
<time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time> <time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time>

View File

@@ -41,7 +41,7 @@ export const messagingMessages = {
"messageBlock.tool.goToSession.label": "Go to Session", "messageBlock.tool.goToSession.label": "Go to Session",
"messageBlock.tool.goToSession.title": "Go to session", "messageBlock.tool.goToSession.title": "Go to session",
"messageBlock.tool.goToSession.unavailableTitle": "Session not available yet", "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.deleting": "Deleting...",
"messageBlock.tool.deletePart.title": "Delete this tool call output", "messageBlock.tool.deletePart.title": "Delete this tool call output",
"messageBlock.tool.deletePart.failed.title": "Delete failed", "messageBlock.tool.deletePart.failed.title": "Delete failed",
@@ -77,11 +77,15 @@ export const messagingMessages = {
"messageItem.actions.copy": "Copy", "messageItem.actions.copy": "Copy",
"messageItem.actions.copyTitle": "Copy message", "messageItem.actions.copyTitle": "Copy message",
"messageItem.actions.copied": "Copied!", "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.queued": "QUEUED",
"messageItem.status.generating": "Generating...", "messageItem.status.generating": "Generating...",
"messageItem.status.sending": "Sending...", "messageItem.status.sending": "Sending...",
"messageItem.status.failedToSend": "Message failed to send", "messageItem.status.failedToSend": "Message failed to send",
"messagePart.actions.delete": "Delete", "messagePart.actions.delete": "Delete Part",
"messagePart.actions.deleting": "Deleting...", "messagePart.actions.deleting": "Deleting...",
"messagePart.actions.deleteTitle": "Delete this item", "messagePart.actions.deleteTitle": "Delete this item",
"messagePart.actions.deleteFailedTitle": "Delete failed", "messagePart.actions.deleteFailedTitle": "Delete failed",

View File

@@ -41,7 +41,7 @@ export const messagingMessages = {
"messageBlock.tool.goToSession.label": "Ir a sesión", "messageBlock.tool.goToSession.label": "Ir a sesión",
"messageBlock.tool.goToSession.title": "Ir a la 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.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.deleting": "Eliminando...",
"messageBlock.tool.deletePart.title": "Eliminar esta salida de herramienta", "messageBlock.tool.deletePart.title": "Eliminar esta salida de herramienta",
"messageBlock.tool.deletePart.failed.title": "Error al eliminar", "messageBlock.tool.deletePart.failed.title": "Error al eliminar",
@@ -77,11 +77,15 @@ export const messagingMessages = {
"messageItem.actions.copy": "Copiar", "messageItem.actions.copy": "Copiar",
"messageItem.actions.copyTitle": "Copiar mensaje", "messageItem.actions.copyTitle": "Copiar mensaje",
"messageItem.actions.copied": "¡Copiado!", "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.queued": "EN COLA",
"messageItem.status.generating": "Generando...", "messageItem.status.generating": "Generando...",
"messageItem.status.sending": "Enviando...", "messageItem.status.sending": "Enviando...",
"messageItem.status.failedToSend": "No se pudo enviar el mensaje", "messageItem.status.failedToSend": "No se pudo enviar el mensaje",
"messagePart.actions.delete": "Eliminar", "messagePart.actions.delete": "Eliminar parte",
"messagePart.actions.deleting": "Eliminando...", "messagePart.actions.deleting": "Eliminando...",
"messagePart.actions.deleteTitle": "Eliminar este elemento", "messagePart.actions.deleteTitle": "Eliminar este elemento",
"messagePart.actions.deleteFailedTitle": "Error al eliminar", "messagePart.actions.deleteFailedTitle": "Error al eliminar",

View File

@@ -41,7 +41,7 @@ export const messagingMessages = {
"messageBlock.tool.goToSession.label": "Aller à la session", "messageBlock.tool.goToSession.label": "Aller à la session",
"messageBlock.tool.goToSession.title": "Aller à la session", "messageBlock.tool.goToSession.title": "Aller à la session",
"messageBlock.tool.goToSession.unavailableTitle": "Session pas encore disponible", "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.deleting": "Suppression...",
"messageBlock.tool.deletePart.title": "Supprimer cette sortie d'outil", "messageBlock.tool.deletePart.title": "Supprimer cette sortie d'outil",
"messageBlock.tool.deletePart.failed.title": "Échec de suppression", "messageBlock.tool.deletePart.failed.title": "Échec de suppression",
@@ -77,11 +77,15 @@ export const messagingMessages = {
"messageItem.actions.copy": "Copier", "messageItem.actions.copy": "Copier",
"messageItem.actions.copyTitle": "Copier le message", "messageItem.actions.copyTitle": "Copier le message",
"messageItem.actions.copied": "Copié !", "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.queued": "EN FILE",
"messageItem.status.generating": "Génération...", "messageItem.status.generating": "Génération...",
"messageItem.status.sending": "Envoi...", "messageItem.status.sending": "Envoi...",
"messageItem.status.failedToSend": "Échec de l'envoi du message", "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.deleting": "Suppression...",
"messagePart.actions.deleteTitle": "Supprimer cet élément", "messagePart.actions.deleteTitle": "Supprimer cet élément",
"messagePart.actions.deleteFailedTitle": "Échec de suppression", "messagePart.actions.deleteFailedTitle": "Échec de suppression",

View File

@@ -41,7 +41,7 @@ export const messagingMessages = {
"messageBlock.tool.goToSession.label": "セッションへ移動", "messageBlock.tool.goToSession.label": "セッションへ移動",
"messageBlock.tool.goToSession.title": "セッションへ移動", "messageBlock.tool.goToSession.title": "セッションへ移動",
"messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません", "messageBlock.tool.goToSession.unavailableTitle": "セッションはまだ利用できません",
"messageBlock.tool.deletePart.label": "削除", "messageBlock.tool.deletePart.label": "パートを削除",
"messageBlock.tool.deletePart.deleting": "削除中...", "messageBlock.tool.deletePart.deleting": "削除中...",
"messageBlock.tool.deletePart.title": "このツール出力を削除", "messageBlock.tool.deletePart.title": "このツール出力を削除",
"messageBlock.tool.deletePart.failed.title": "削除に失敗しました", "messageBlock.tool.deletePart.failed.title": "削除に失敗しました",
@@ -77,11 +77,15 @@ export const messagingMessages = {
"messageItem.actions.copy": "コピー", "messageItem.actions.copy": "コピー",
"messageItem.actions.copyTitle": "メッセージをコピー", "messageItem.actions.copyTitle": "メッセージをコピー",
"messageItem.actions.copied": "コピーしました!", "messageItem.actions.copied": "コピーしました!",
"messageItem.actions.deleteMessage": "メッセージを削除",
"messageItem.actions.deletingMessage": "削除中...",
"messageItem.actions.deleteMessageFailedTitle": "削除に失敗しました",
"messageItem.actions.deleteMessageFailedMessage": "メッセージの削除に失敗しました",
"messageItem.status.queued": "待機中", "messageItem.status.queued": "待機中",
"messageItem.status.generating": "生成中...", "messageItem.status.generating": "生成中...",
"messageItem.status.sending": "送信中...", "messageItem.status.sending": "送信中...",
"messageItem.status.failedToSend": "メッセージの送信に失敗しました", "messageItem.status.failedToSend": "メッセージの送信に失敗しました",
"messagePart.actions.delete": "削除", "messagePart.actions.delete": "パートを削除",
"messagePart.actions.deleting": "削除中...", "messagePart.actions.deleting": "削除中...",
"messagePart.actions.deleteTitle": "この項目を削除", "messagePart.actions.deleteTitle": "この項目を削除",
"messagePart.actions.deleteFailedTitle": "削除に失敗しました", "messagePart.actions.deleteFailedTitle": "削除に失敗しました",

View File

@@ -41,7 +41,7 @@ export const messagingMessages = {
"messageBlock.tool.goToSession.label": "Перейти к сессии", "messageBlock.tool.goToSession.label": "Перейти к сессии",
"messageBlock.tool.goToSession.title": "Перейти к сессии", "messageBlock.tool.goToSession.title": "Перейти к сессии",
"messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна", "messageBlock.tool.goToSession.unavailableTitle": "Сессия пока недоступна",
"messageBlock.tool.deletePart.label": "Удалить", "messageBlock.tool.deletePart.label": "Удалить часть",
"messageBlock.tool.deletePart.deleting": "Удаление...", "messageBlock.tool.deletePart.deleting": "Удаление...",
"messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента", "messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента",
"messageBlock.tool.deletePart.failed.title": "Ошибка удаления", "messageBlock.tool.deletePart.failed.title": "Ошибка удаления",
@@ -77,11 +77,15 @@ export const messagingMessages = {
"messageItem.actions.copy": "Копировать", "messageItem.actions.copy": "Копировать",
"messageItem.actions.copyTitle": "Копировать сообщение", "messageItem.actions.copyTitle": "Копировать сообщение",
"messageItem.actions.copied": "Скопировано!", "messageItem.actions.copied": "Скопировано!",
"messageItem.actions.deleteMessage": "Удалить сообщение",
"messageItem.actions.deletingMessage": "Удаление...",
"messageItem.actions.deleteMessageFailedTitle": "Ошибка удаления",
"messageItem.actions.deleteMessageFailedMessage": "Не удалось удалить сообщение",
"messageItem.status.queued": "В ОЧЕРЕДИ", "messageItem.status.queued": "В ОЧЕРЕДИ",
"messageItem.status.generating": "Генерация…", "messageItem.status.generating": "Генерация…",
"messageItem.status.sending": "Отправка…", "messageItem.status.sending": "Отправка…",
"messageItem.status.failedToSend": "Не удалось отправить сообщение", "messageItem.status.failedToSend": "Не удалось отправить сообщение",
"messagePart.actions.delete": "Удалить", "messagePart.actions.delete": "Удалить часть",
"messagePart.actions.deleting": "Удаление...", "messagePart.actions.deleting": "Удаление...",
"messagePart.actions.deleteTitle": "Удалить этот элемент", "messagePart.actions.deleteTitle": "Удалить этот элемент",
"messagePart.actions.deleteFailedTitle": "Ошибка удаления", "messagePart.actions.deleteFailedTitle": "Ошибка удаления",

View File

@@ -41,7 +41,7 @@ export const messagingMessages = {
"messageBlock.tool.goToSession.label": "前往会话", "messageBlock.tool.goToSession.label": "前往会话",
"messageBlock.tool.goToSession.title": "前往会话", "messageBlock.tool.goToSession.title": "前往会话",
"messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用", "messageBlock.tool.goToSession.unavailableTitle": "会话尚不可用",
"messageBlock.tool.deletePart.label": "删除", "messageBlock.tool.deletePart.label": "删除部分",
"messageBlock.tool.deletePart.deleting": "正在删除...", "messageBlock.tool.deletePart.deleting": "正在删除...",
"messageBlock.tool.deletePart.title": "删除此工具输出", "messageBlock.tool.deletePart.title": "删除此工具输出",
"messageBlock.tool.deletePart.failed.title": "删除失败", "messageBlock.tool.deletePart.failed.title": "删除失败",
@@ -77,11 +77,15 @@ export const messagingMessages = {
"messageItem.actions.copy": "复制", "messageItem.actions.copy": "复制",
"messageItem.actions.copyTitle": "复制消息", "messageItem.actions.copyTitle": "复制消息",
"messageItem.actions.copied": "已复制!", "messageItem.actions.copied": "已复制!",
"messageItem.actions.deleteMessage": "删除消息",
"messageItem.actions.deletingMessage": "正在删除...",
"messageItem.actions.deleteMessageFailedTitle": "删除失败",
"messageItem.actions.deleteMessageFailedMessage": "无法删除消息",
"messageItem.status.queued": "排队中", "messageItem.status.queued": "排队中",
"messageItem.status.generating": "正在生成...", "messageItem.status.generating": "正在生成...",
"messageItem.status.sending": "正在发送...", "messageItem.status.sending": "正在发送...",
"messageItem.status.failedToSend": "消息发送失败", "messageItem.status.failedToSend": "消息发送失败",
"messagePart.actions.delete": "删除", "messagePart.actions.delete": "删除部分",
"messagePart.actions.deleting": "正在删除...", "messagePart.actions.deleting": "正在删除...",
"messagePart.actions.deleteTitle": "删除此项", "messagePart.actions.deleteTitle": "删除此项",
"messagePart.actions.deleteFailedTitle": "删除失败", "messagePart.actions.deleteFailedTitle": "删除失败",

View File

@@ -7,7 +7,7 @@ import { providers, sessions, withSession } from "./session-state"
import { getDefaultModel, isModelValid } from "./session-models" import { getDefaultModel, isModelValid } from "./session-models"
import { updateSessionInfo } from "./message-v2/session-info" import { updateSessionInfo } from "./message-v2/session-info"
import { messageStoreBus } from "./message-v2/bus" 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 { getLogger } from "../lib/logger"
import { requestData } from "../lib/opencode-api" import { requestData } from "../lib/opencode-api"
@@ -439,8 +439,33 @@ async function deleteMessagePart(instanceId: string, sessionId: string, messageI
updateSessionInfo(instanceId, sessionId) updateSessionInfo(instanceId, sessionId)
} }
async function deleteMessage(instanceId: string, sessionId: string, messageId: string): Promise<void> {
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 { export {
abortSession, abortSession,
deleteMessage,
deleteMessagePart, deleteMessagePart,
executeCustomCommand, executeCustomCommand,
renameSession, renameSession,