feat(ui): add delete action for message parts

This commit is contained in:
Shantur Rathore
2026-02-03 18:32:54 +00:00
parent d2b68159be
commit 0261154a5e
10 changed files with 363 additions and 36 deletions

View File

@@ -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 (
<Show when={toolPart()}>
{(resolvedToolPart) => (
@@ -351,17 +382,30 @@ function ToolCallItem(props: ToolCallItemProps) {
<span>{t("messageBlock.tool.header")}</span>
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
</div>
<Show when={taskSessionId()}>
<div class="flex items-center gap-2">
<Show when={taskSessionId()}>
<button
class="tool-call-header-button"
type="button"
disabled={!taskLocation()}
onClick={handleGoToTaskSession}
title={!taskLocation() ? t("messageBlock.tool.goToSession.unavailableTitle") : t("messageBlock.tool.goToSession.title")}
>
{t("messageBlock.tool.goToSession.label")}
</button>
</Show>
<button
class="tool-call-header-button"
type="button"
disabled={!taskLocation()}
onClick={handleGoToTaskSession}
title={!taskLocation() ? t("messageBlock.tool.goToSession.unavailableTitle") : t("messageBlock.tool.goToSession.title")}
disabled={deleteDisabled()}
onClick={handleDeleteToolPart}
title={t("messageBlock.tool.deletePart.title")}
>
{t("messageBlock.tool.goToSession.label")}
{deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
</button>
</Show>
</div>
</div>
<ToolCall
@@ -395,6 +439,8 @@ type ReasoningDisplayItem = {
messageInfo?: MessageInfo
showAgentMeta?: boolean
defaultExpanded: boolean
messageId: string
partId: string
}
type CompactionDisplayItem = {
@@ -403,6 +449,8 @@ type CompactionDisplayItem = {
part: ClientPart
messageInfo?: MessageInfo
accentColor?: string
messageId: string
partId: string
}
type MessageBlockItem = ContentDisplayItem | ToolDisplayItem | StepDisplayItem | ReasoningDisplayItem | CompactionDisplayItem
@@ -530,7 +578,8 @@ export default function MessageBlock(props: MessageBlockProps) {
if (part.type === "compaction") {
flushContent()
const key = `${current.id}:${part.id ?? partIndex}:compaction`
const partId = part.id ?? ""
const key = `${current.id}:${partId || partIndex}:compaction`
const isAuto = Boolean((part as any)?.auto)
items.push({
type: "compaction",
@@ -538,6 +587,8 @@ export default function MessageBlock(props: MessageBlockProps) {
part,
messageInfo: info,
accentColor: isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR,
messageId: current.id,
partId,
})
lastAccentColor = isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR
return
@@ -562,7 +613,8 @@ export default function MessageBlock(props: MessageBlockProps) {
if (part.type === "reasoning") {
flushContent()
if (props.showThinking() && reasoningHasRenderableContent(part)) {
const key = `${current.id}:${part.id ?? partIndex}:reasoning`
const partId = part.id ?? ""
const key = `${current.id}:${partId || partIndex}:reasoning`
const showAgentMeta = current.role === "assistant" && !agentMetaAttached
if (showAgentMeta) {
agentMetaAttached = true
@@ -574,6 +626,8 @@ export default function MessageBlock(props: MessageBlockProps) {
messageInfo: info,
showAgentMeta,
defaultExpanded: props.thinkingDefaultExpanded(),
messageId: current.id,
partId,
})
lastAccentColor = ASSISTANT_BORDER_COLOR
}
@@ -647,7 +701,12 @@ export default function MessageBlock(props: MessageBlockProps) {
})()}
</Match>
<Match when={item.type === "step-start"}>
<StepCard kind="start" part={(item as StepDisplayItem).part} messageInfo={(item as StepDisplayItem).messageInfo} showAgentMeta />
<StepCard
kind="start"
part={(item as StepDisplayItem).part}
messageInfo={(item as StepDisplayItem).messageInfo}
showAgentMeta
/>
</Match>
<Match when={item.type === "step-finish"}>
<StepCard
@@ -659,7 +718,15 @@ export default function MessageBlock(props: MessageBlockProps) {
/>
</Match>
<Match when={item.type === "compaction"}>
<CompactionCard part={(item as CompactionDisplayItem).part} messageInfo={(item as CompactionDisplayItem).messageInfo} borderColor={(item as CompactionDisplayItem).accentColor} />
<CompactionCard
part={(item as CompactionDisplayItem).part}
messageInfo={(item as CompactionDisplayItem).messageInfo}
borderColor={(item as CompactionDisplayItem).accentColor}
instanceId={props.instanceId}
sessionId={props.sessionId}
messageId={(item as CompactionDisplayItem).messageId}
partId={(item as CompactionDisplayItem).partId}
/>
</Match>
<Match when={item.type === "reasoning"}>
<ReasoningCard
@@ -667,6 +734,8 @@ export default function MessageBlock(props: MessageBlockProps) {
messageInfo={(item as ReasoningDisplayItem).messageInfo}
instanceId={props.instanceId}
sessionId={props.sessionId}
messageId={(item as ReasoningDisplayItem).messageId}
partId={(item as ReasoningDisplayItem).partId}
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
/>
@@ -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 (
<div
class={containerClass()}
class={`${containerClass()} relative`}
style={{ "border-left": `4px solid ${borderColor()}` }}
role="status"
aria-label={t("messageBlock.compaction.ariaLabel")}
>
<button
type="button"
class="tool-call-header-button absolute right-2 top-1/2 -translate-y-1/2"
disabled={!canDelete()}
onClick={handleDelete}
title={t("messagePart.actions.deleteTitle")}
>
{deleting() ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
</button>
<div class="message-compaction-row">
<FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" />
<span class="message-compaction-label">{label()}</span>
@@ -759,6 +869,7 @@ function StepCard(props: StepCardProps) {
const finishStyle = () => (props.borderColor ? { "border-left-color": props.borderColor } : undefined)
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
const entries = [
{ label: t("messageBlock.usage.input"), value: usage.input, formatter: formatTokenTotal },
@@ -824,6 +935,8 @@ interface ReasoningCardProps {
messageInfo?: MessageInfo
instanceId: string
sessionId: string
messageId: string
partId: string
showAgentMeta?: boolean
defaultExpanded?: boolean
}
@@ -831,6 +944,7 @@ interface ReasoningCardProps {
function ReasoningCard(props: ReasoningCardProps) {
const { t } = useI18n()
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
const [deleting, setDeleting] = createSignal(false)
createEffect(() => {
setExpanded(Boolean(props.defaultExpanded))
@@ -894,6 +1008,27 @@ function ReasoningCard(props: ReasoningCardProps) {
const toggle = () => setExpanded((prev) => !prev)
const hasDeleteTarget = () => Boolean(props.partId)
const canDelete = () => hasDeleteTarget() && !deleting()
const handleDelete = async (event: Event) => {
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 (
<div class="message-reasoning-card">
<button
@@ -924,6 +1059,25 @@ function ReasoningCard(props: ReasoningCardProps) {
<span class="message-reasoning-indicator">
{expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")}
</span>
<Show when={hasDeleteTarget()}>
<span
class={`message-reasoning-indicator${canDelete() ? "" : " opacity-50 pointer-events-none"}`}
role="button"
tabIndex={0}
onClick={handleDelete}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
handleDelete(event)
}
}}
aria-label={t("messagePart.actions.deleteTitle")}
title={t("messagePart.actions.deleteTitle")}
>
{deleting() ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
</span>
</Show>
<span class="message-reasoning-time">{timestamp()}</span>
</span>
</button>

View File

@@ -5,6 +5,8 @@ import type { MessageRecord } from "../stores/message-v2/types"
import MessagePart from "./message-part"
import { copyToClipboard } from "../lib/clipboard"
import { useI18n } from "../lib/i18n"
import { showAlertDialog } from "../stores/alerts"
import { deleteMessagePart } from "../stores/session-actions"
interface MessageItemProps {
record: MessageRecord
@@ -22,6 +24,7 @@ interface MessageItemProps {
export default function MessageItem(props: MessageItemProps) {
const { t } = useI18n()
const [copied, setCopied] = createSignal(false)
const [deletingParts, setDeletingParts] = createSignal<Set<string>>(new Set())
const isUser = () => props.record.role === "user"
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
@@ -172,6 +175,50 @@ export default function MessageItem(props: MessageItemProps) {
setTimeout(() => setCopied(false), 2000)
}
const deletableTextPartId = () => {
const part = props.parts.find((candidate) => {
if (!candidate || candidate.type !== "text") return false
const id = (candidate as any).id
if (typeof id !== "string" || id.length === 0) return false
return !Boolean((candidate as any).synthetic)
})
return (part as any)?.id as string | undefined
}
const isDeletingPart = (partId?: string) => {
if (!partId) return false
return deletingParts().has(partId)
}
const setPartDeleting = (partId: string, value: boolean) => {
setDeletingParts((prev) => {
const next = new Set(prev)
if (value) {
next.add(partId)
} else {
next.delete(partId)
}
return next
})
}
const handleDeletePart = async (partId?: string) => {
if (!partId) return
if (isDeletingPart(partId)) return
setPartDeleting(partId, true)
try {
await deleteMessagePart(props.instanceId, props.sessionId, props.record.id, partId)
} catch (error) {
showAlertDialog(t("messagePart.actions.deleteFailedMessage"), {
title: t("messagePart.actions.deleteFailedTitle"),
detail: error instanceof Error ? error.message : String(error),
variant: "error",
})
} finally {
setPartDeleting(partId, false)
}
}
if (!isUser() && !hasContent() && !isGenerating()) {
return null
}
@@ -257,19 +304,48 @@ export default function MessageItem(props: MessageItemProps) {
{t("messageItem.actions.copied")}
</Show>
</button>
<Show when={deletableTextPartId()}>
{(partId) => (
<button
class="message-action-button"
onClick={() => void handleDeletePart(partId())}
disabled={isDeletingPart(partId())}
title={t("messagePart.actions.deleteTitle")}
aria-label={t("messagePart.actions.deleteTitle")}
>
{isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
</button>
)}
</Show>
</div>
</Show>
<Show when={!isUser()}>
<button
class="message-action-button"
onClick={handleCopy}
title={t("messageItem.actions.copyTitle")}
aria-label={t("messageItem.actions.copyTitle")}
>
<Show when={copied()} fallback={t("messageItem.actions.copy")}>
{t("messageItem.actions.copied")}
<div class="message-action-group">
<button
class="message-action-button"
onClick={handleCopy}
title={t("messageItem.actions.copyTitle")}
aria-label={t("messageItem.actions.copyTitle")}
>
<Show when={copied()} fallback={t("messageItem.actions.copy")}>
{t("messageItem.actions.copied")}
</Show>
</button>
<Show when={deletableTextPartId()}>
{(partId) => (
<button
class="message-action-button"
onClick={() => void handleDeletePart(partId())}
disabled={isDeletingPart(partId())}
title={t("messagePart.actions.deleteTitle")}
aria-label={t("messagePart.actions.deleteTitle")}
>
{isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
</button>
)}
</Show>
</button>
</div>
</Show>
<time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time>
</div>
@@ -337,6 +413,19 @@ export default function MessageItem(props: MessageItemProps) {
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12l4 4 4-4m-4-8v12" />
</svg>
</button>
<button
type="button"
onClick={() => void handleDeletePart(attachment.id)}
class="attachment-remove"
disabled={isDeletingPart(attachment.id)}
aria-label={t("messagePart.actions.deleteTitle")}
title={t("messagePart.actions.deleteTitle")}
>
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<Show when={isImage}>
<div class="attachment-chip-preview">
<img src={attachment.url} alt={name} />

View File

@@ -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 {
<Match when={partType() === "text"}>
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
<div class={textContainerClass()}>
<Show
when={isAssistantMessage()}
fallback={<span class="text-primary">{plainTextContent()}</span>}
>
<Markdown
part={createTextPartForMarkdown()}
instanceId={props.instanceId}
sessionId={props.sessionId}
isDark={isDark()}
size={isAssistantMessage() ? "tight" : "base"}
onRendered={props.onRendered}
/>
</Show>
<Show
when={isAssistantMessage()}
fallback={<span class="text-primary">{plainTextContent()}</span>}
>
<Markdown
part={createTextPartForMarkdown()}
instanceId={props.instanceId}
sessionId={props.sessionId}
isDark={isDark()}
size={isAssistantMessage() ? "tight" : "base"}
onRendered={props.onRendered}
/>
</Show>
</div>
</div>
</Show>
</Match>

View File

@@ -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}",

View File

@@ -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}",

View File

@@ -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}",

View File

@@ -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}",

View File

@@ -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}",

View File

@@ -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}",

View File

@@ -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<void> {
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,