feat(ui): add delete action for message parts
This commit is contained in:
@@ -11,6 +11,8 @@ import { messageStoreBus } from "../stores/message-v2/bus"
|
|||||||
import { formatTokenTotal } from "../lib/formatters"
|
import { formatTokenTotal } from "../lib/formatters"
|
||||||
import { sessions, setActiveParentSession, setActiveSession } from "../stores/sessions"
|
import { sessions, setActiveParentSession, setActiveSession } from "../stores/sessions"
|
||||||
import { setActiveInstanceId } from "../stores/instances"
|
import { setActiveInstanceId } from "../stores/instances"
|
||||||
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
|
import { deleteMessagePart } from "../stores/session-actions"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
|
||||||
const TOOL_ICON = "🔧"
|
const TOOL_ICON = "🔧"
|
||||||
@@ -302,6 +304,7 @@ interface ToolCallItemProps {
|
|||||||
|
|
||||||
function ToolCallItem(props: ToolCallItemProps) {
|
function ToolCallItem(props: ToolCallItemProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const [deleting, setDeleting] = 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))
|
||||||
@@ -318,6 +321,14 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
const messageVersion = createMemo(() => record()?.revision ?? 0)
|
const messageVersion = createMemo(() => record()?.revision ?? 0)
|
||||||
const partVersion = createMemo(() => partEntry()?.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 taskSessionId = createMemo(() => {
|
||||||
const state = toolState()
|
const state = toolState()
|
||||||
if (!state) return ""
|
if (!state) return ""
|
||||||
@@ -341,6 +352,26 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
navigateToTaskSession(location)
|
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 (
|
return (
|
||||||
<Show when={toolPart()}>
|
<Show when={toolPart()}>
|
||||||
{(resolvedToolPart) => (
|
{(resolvedToolPart) => (
|
||||||
@@ -351,17 +382,30 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
<span>{t("messageBlock.tool.header")}</span>
|
<span>{t("messageBlock.tool.header")}</span>
|
||||||
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
|
<span class="tool-name">{toolName() || t("messageBlock.tool.unknown")}</span>
|
||||||
</div>
|
</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
|
<button
|
||||||
class="tool-call-header-button"
|
class="tool-call-header-button"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!taskLocation()}
|
disabled={deleteDisabled()}
|
||||||
onClick={handleGoToTaskSession}
|
onClick={handleDeleteToolPart}
|
||||||
title={!taskLocation() ? t("messageBlock.tool.goToSession.unavailableTitle") : t("messageBlock.tool.goToSession.title")}
|
title={t("messageBlock.tool.deletePart.title")}
|
||||||
>
|
>
|
||||||
{t("messageBlock.tool.goToSession.label")}
|
{deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToolCall
|
<ToolCall
|
||||||
@@ -395,6 +439,8 @@ type ReasoningDisplayItem = {
|
|||||||
messageInfo?: MessageInfo
|
messageInfo?: MessageInfo
|
||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
defaultExpanded: boolean
|
defaultExpanded: boolean
|
||||||
|
messageId: string
|
||||||
|
partId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompactionDisplayItem = {
|
type CompactionDisplayItem = {
|
||||||
@@ -403,6 +449,8 @@ type CompactionDisplayItem = {
|
|||||||
part: ClientPart
|
part: ClientPart
|
||||||
messageInfo?: MessageInfo
|
messageInfo?: MessageInfo
|
||||||
accentColor?: string
|
accentColor?: string
|
||||||
|
messageId: string
|
||||||
|
partId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageBlockItem = ContentDisplayItem | ToolDisplayItem | StepDisplayItem | ReasoningDisplayItem | CompactionDisplayItem
|
type MessageBlockItem = ContentDisplayItem | ToolDisplayItem | StepDisplayItem | ReasoningDisplayItem | CompactionDisplayItem
|
||||||
@@ -530,7 +578,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
|
|
||||||
if (part.type === "compaction") {
|
if (part.type === "compaction") {
|
||||||
flushContent()
|
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)
|
const isAuto = Boolean((part as any)?.auto)
|
||||||
items.push({
|
items.push({
|
||||||
type: "compaction",
|
type: "compaction",
|
||||||
@@ -538,6 +587,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
part,
|
part,
|
||||||
messageInfo: info,
|
messageInfo: info,
|
||||||
accentColor: isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR,
|
accentColor: isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR,
|
||||||
|
messageId: current.id,
|
||||||
|
partId,
|
||||||
})
|
})
|
||||||
lastAccentColor = isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR
|
lastAccentColor = isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR
|
||||||
return
|
return
|
||||||
@@ -562,7 +613,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
if (part.type === "reasoning") {
|
if (part.type === "reasoning") {
|
||||||
flushContent()
|
flushContent()
|
||||||
if (props.showThinking() && reasoningHasRenderableContent(part)) {
|
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
|
const showAgentMeta = current.role === "assistant" && !agentMetaAttached
|
||||||
if (showAgentMeta) {
|
if (showAgentMeta) {
|
||||||
agentMetaAttached = true
|
agentMetaAttached = true
|
||||||
@@ -574,6 +626,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
messageInfo: info,
|
messageInfo: info,
|
||||||
showAgentMeta,
|
showAgentMeta,
|
||||||
defaultExpanded: props.thinkingDefaultExpanded(),
|
defaultExpanded: props.thinkingDefaultExpanded(),
|
||||||
|
messageId: current.id,
|
||||||
|
partId,
|
||||||
})
|
})
|
||||||
lastAccentColor = ASSISTANT_BORDER_COLOR
|
lastAccentColor = ASSISTANT_BORDER_COLOR
|
||||||
}
|
}
|
||||||
@@ -647,7 +701,12 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
})()}
|
})()}
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "step-start"}>
|
<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>
|
||||||
<Match when={item.type === "step-finish"}>
|
<Match when={item.type === "step-finish"}>
|
||||||
<StepCard
|
<StepCard
|
||||||
@@ -659,7 +718,15 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "compaction"}>
|
<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>
|
||||||
<Match when={item.type === "reasoning"}>
|
<Match when={item.type === "reasoning"}>
|
||||||
<ReasoningCard
|
<ReasoningCard
|
||||||
@@ -667,6 +734,8 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
messageInfo={(item as ReasoningDisplayItem).messageInfo}
|
messageInfo={(item as ReasoningDisplayItem).messageInfo}
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
|
messageId={(item as ReasoningDisplayItem).messageId}
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
@@ -689,8 +758,19 @@ interface StepCardProps {
|
|||||||
borderColor?: string
|
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 { t } = useI18n()
|
||||||
|
const [deleting, setDeleting] = 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)
|
||||||
@@ -698,13 +778,43 @@ function CompactionCard(props: { part: ClientPart; messageInfo?: MessageInfo; bo
|
|||||||
const containerClass = () =>
|
const containerClass = () =>
|
||||||
`message-compaction-card ${isAuto() ? "message-compaction-card--auto" : "message-compaction-card--manual"}`
|
`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 (
|
return (
|
||||||
<div
|
<div
|
||||||
class={containerClass()}
|
class={`${containerClass()} relative`}
|
||||||
style={{ "border-left": `4px solid ${borderColor()}` }}
|
style={{ "border-left": `4px solid ${borderColor()}` }}
|
||||||
role="status"
|
role="status"
|
||||||
aria-label={t("messageBlock.compaction.ariaLabel")}
|
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">
|
<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" />
|
||||||
<span class="message-compaction-label">{label()}</span>
|
<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 finishStyle = () => (props.borderColor ? { "border-left-color": props.borderColor } : undefined)
|
||||||
|
|
||||||
|
|
||||||
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
|
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
|
||||||
const entries = [
|
const entries = [
|
||||||
{ label: t("messageBlock.usage.input"), value: usage.input, formatter: formatTokenTotal },
|
{ label: t("messageBlock.usage.input"), value: usage.input, formatter: formatTokenTotal },
|
||||||
@@ -824,6 +935,8 @@ interface ReasoningCardProps {
|
|||||||
messageInfo?: MessageInfo
|
messageInfo?: MessageInfo
|
||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
|
messageId: string
|
||||||
|
partId: string
|
||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
defaultExpanded?: boolean
|
defaultExpanded?: boolean
|
||||||
}
|
}
|
||||||
@@ -831,6 +944,7 @@ interface ReasoningCardProps {
|
|||||||
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)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setExpanded(Boolean(props.defaultExpanded))
|
setExpanded(Boolean(props.defaultExpanded))
|
||||||
@@ -894,6 +1008,27 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
|
|
||||||
const toggle = () => setExpanded((prev) => !prev)
|
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 (
|
return (
|
||||||
<div class="message-reasoning-card">
|
<div class="message-reasoning-card">
|
||||||
<button
|
<button
|
||||||
@@ -924,6 +1059,25 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
<span class="message-reasoning-indicator">
|
<span class="message-reasoning-indicator">
|
||||||
{expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")}
|
{expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")}
|
||||||
</span>
|
</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 class="message-reasoning-time">{timestamp()}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import type { MessageRecord } from "../stores/message-v2/types"
|
|||||||
import MessagePart from "./message-part"
|
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 { deleteMessagePart } from "../stores/session-actions"
|
||||||
|
|
||||||
interface MessageItemProps {
|
interface MessageItemProps {
|
||||||
record: MessageRecord
|
record: MessageRecord
|
||||||
@@ -22,6 +24,7 @@ interface MessageItemProps {
|
|||||||
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 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
|
||||||
@@ -172,6 +175,50 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
setTimeout(() => setCopied(false), 2000)
|
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()) {
|
if (!isUser() && !hasContent() && !isGenerating()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -257,19 +304,48 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
{t("messageItem.actions.copied")}
|
{t("messageItem.actions.copied")}
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!isUser()}>
|
<Show when={!isUser()}>
|
||||||
<button
|
<div class="message-action-group">
|
||||||
class="message-action-button"
|
<button
|
||||||
onClick={handleCopy}
|
class="message-action-button"
|
||||||
title={t("messageItem.actions.copyTitle")}
|
onClick={handleCopy}
|
||||||
aria-label={t("messageItem.actions.copyTitle")}
|
title={t("messageItem.actions.copyTitle")}
|
||||||
>
|
aria-label={t("messageItem.actions.copyTitle")}
|
||||||
<Show when={copied()} fallback={t("messageItem.actions.copy")}>
|
>
|
||||||
{t("messageItem.actions.copied")}
|
<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>
|
</Show>
|
||||||
</button>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time>
|
<time class="message-timestamp" dateTime={timestampIso()}>{timestamp()}</time>
|
||||||
</div>
|
</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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12l4 4 4-4m-4-8v12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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}>
|
<Show when={isImage}>
|
||||||
<div class="attachment-chip-preview">
|
<div class="attachment-chip-preview">
|
||||||
<img src={attachment.url} alt={name} />
|
<img src={attachment.url} alt={name} />
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface MessagePartProps {
|
|||||||
sessionId: string
|
sessionId: string
|
||||||
onRendered?: () => void
|
onRendered?: () => void
|
||||||
}
|
}
|
||||||
export default function MessagePart(props: MessagePartProps) {
|
export default function MessagePart(props: MessagePartProps) {
|
||||||
|
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
const { preferences } = useConfig()
|
const { preferences } = useConfig()
|
||||||
@@ -32,6 +32,7 @@ interface MessagePartProps {
|
|||||||
return Boolean((part as any).synthetic) && props.messageType !== "user"
|
return Boolean((part as any).synthetic) && props.messageType !== "user"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const plainTextContent = () => {
|
const plainTextContent = () => {
|
||||||
const part = props.part
|
const part = props.part
|
||||||
|
|
||||||
@@ -103,21 +104,21 @@ interface MessagePartProps {
|
|||||||
<Match when={partType() === "text"}>
|
<Match when={partType() === "text"}>
|
||||||
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
|
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
|
||||||
<div class={textContainerClass()}>
|
<div class={textContainerClass()}>
|
||||||
<Show
|
<Show
|
||||||
when={isAssistantMessage()}
|
when={isAssistantMessage()}
|
||||||
fallback={<span class="text-primary">{plainTextContent()}</span>}
|
fallback={<span class="text-primary">{plainTextContent()}</span>}
|
||||||
>
|
>
|
||||||
<Markdown
|
<Markdown
|
||||||
part={createTextPartForMarkdown()}
|
part={createTextPartForMarkdown()}
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
isDark={isDark()}
|
isDark={isDark()}
|
||||||
size={isAssistantMessage() ? "tight" : "base"}
|
size={isAssistantMessage() ? "tight" : "base"}
|
||||||
onRendered={props.onRendered}
|
onRendered={props.onRendered}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ 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.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.ariaLabel": "Session compaction",
|
||||||
"messageBlock.compaction.autoLabel": "Session auto-compacted",
|
"messageBlock.compaction.autoLabel": "Session auto-compacted",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"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.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.defaultName": "attachment",
|
||||||
"messageItem.attachment.downloadAriaLabel": "Download {name}",
|
"messageItem.attachment.downloadAriaLabel": "Download {name}",
|
||||||
"messageItem.agentMeta.agentLabel": "Agent: {agent}",
|
"messageItem.agentMeta.agentLabel": "Agent: {agent}",
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ 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.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.ariaLabel": "Compactación de sesión",
|
||||||
"messageBlock.compaction.autoLabel": "Sesión compactada automáticamente",
|
"messageBlock.compaction.autoLabel": "Sesión compactada automáticamente",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"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.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.defaultName": "adjunto",
|
||||||
"messageItem.attachment.downloadAriaLabel": "Descargar {name}",
|
"messageItem.attachment.downloadAriaLabel": "Descargar {name}",
|
||||||
"messageItem.agentMeta.agentLabel": "Agente: {agent}",
|
"messageItem.agentMeta.agentLabel": "Agente: {agent}",
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ 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.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.ariaLabel": "Compaction de la session",
|
||||||
"messageBlock.compaction.autoLabel": "Session compactée automatiquement",
|
"messageBlock.compaction.autoLabel": "Session compactée automatiquement",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"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.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.defaultName": "piece-jointe",
|
||||||
"messageItem.attachment.downloadAriaLabel": "Télécharger {name}",
|
"messageItem.attachment.downloadAriaLabel": "Télécharger {name}",
|
||||||
"messageItem.agentMeta.agentLabel": "Agent : {agent}",
|
"messageItem.agentMeta.agentLabel": "Agent : {agent}",
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ 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.deleting": "削除中...",
|
||||||
|
"messageBlock.tool.deletePart.title": "このツール出力を削除",
|
||||||
|
"messageBlock.tool.deletePart.failed.title": "削除に失敗しました",
|
||||||
|
"messageBlock.tool.deletePart.failed.message": "ツール出力の削除に失敗しました",
|
||||||
|
|
||||||
"messageBlock.compaction.ariaLabel": "セッションのコンパクト化",
|
"messageBlock.compaction.ariaLabel": "セッションのコンパクト化",
|
||||||
"messageBlock.compaction.autoLabel": "セッションを自動でコンパクト化しました",
|
"messageBlock.compaction.autoLabel": "セッションを自動でコンパクト化しました",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"messageItem.status.generating": "生成中...",
|
"messageItem.status.generating": "生成中...",
|
||||||
"messageItem.status.sending": "送信中...",
|
"messageItem.status.sending": "送信中...",
|
||||||
"messageItem.status.failedToSend": "メッセージの送信に失敗しました",
|
"messageItem.status.failedToSend": "メッセージの送信に失敗しました",
|
||||||
|
"messagePart.actions.delete": "削除",
|
||||||
|
"messagePart.actions.deleting": "削除中...",
|
||||||
|
"messagePart.actions.deleteTitle": "この項目を削除",
|
||||||
|
"messagePart.actions.deleteFailedTitle": "削除に失敗しました",
|
||||||
|
"messagePart.actions.deleteFailedMessage": "項目の削除に失敗しました",
|
||||||
"messageItem.attachment.defaultName": "添付ファイル",
|
"messageItem.attachment.defaultName": "添付ファイル",
|
||||||
"messageItem.attachment.downloadAriaLabel": "{name} をダウンロード",
|
"messageItem.attachment.downloadAriaLabel": "{name} をダウンロード",
|
||||||
"messageItem.agentMeta.agentLabel": "エージェント: {agent}",
|
"messageItem.agentMeta.agentLabel": "エージェント: {agent}",
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ 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.deleting": "Удаление...",
|
||||||
|
"messageBlock.tool.deletePart.title": "Удалить этот вывод инструмента",
|
||||||
|
"messageBlock.tool.deletePart.failed.title": "Ошибка удаления",
|
||||||
|
"messageBlock.tool.deletePart.failed.message": "Не удалось удалить вывод инструмента",
|
||||||
|
|
||||||
"messageBlock.compaction.ariaLabel": "Компактация сессии",
|
"messageBlock.compaction.ariaLabel": "Компактация сессии",
|
||||||
"messageBlock.compaction.autoLabel": "Сессия автоматически компактирована",
|
"messageBlock.compaction.autoLabel": "Сессия автоматически компактирована",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"messageItem.status.generating": "Генерация…",
|
"messageItem.status.generating": "Генерация…",
|
||||||
"messageItem.status.sending": "Отправка…",
|
"messageItem.status.sending": "Отправка…",
|
||||||
"messageItem.status.failedToSend": "Не удалось отправить сообщение",
|
"messageItem.status.failedToSend": "Не удалось отправить сообщение",
|
||||||
|
"messagePart.actions.delete": "Удалить",
|
||||||
|
"messagePart.actions.deleting": "Удаление...",
|
||||||
|
"messagePart.actions.deleteTitle": "Удалить этот элемент",
|
||||||
|
"messagePart.actions.deleteFailedTitle": "Ошибка удаления",
|
||||||
|
"messagePart.actions.deleteFailedMessage": "Не удалось удалить элемент",
|
||||||
"messageItem.attachment.defaultName": "вложение",
|
"messageItem.attachment.defaultName": "вложение",
|
||||||
"messageItem.attachment.downloadAriaLabel": "Скачать {name}",
|
"messageItem.attachment.downloadAriaLabel": "Скачать {name}",
|
||||||
"messageItem.agentMeta.agentLabel": "Агент: {agent}",
|
"messageItem.agentMeta.agentLabel": "Агент: {agent}",
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ 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.deleting": "正在删除...",
|
||||||
|
"messageBlock.tool.deletePart.title": "删除此工具输出",
|
||||||
|
"messageBlock.tool.deletePart.failed.title": "删除失败",
|
||||||
|
"messageBlock.tool.deletePart.failed.message": "删除工具输出失败",
|
||||||
|
|
||||||
"messageBlock.compaction.ariaLabel": "会话压缩",
|
"messageBlock.compaction.ariaLabel": "会话压缩",
|
||||||
"messageBlock.compaction.autoLabel": "会话已自动压缩",
|
"messageBlock.compaction.autoLabel": "会话已自动压缩",
|
||||||
@@ -73,6 +78,11 @@ export const messagingMessages = {
|
|||||||
"messageItem.status.generating": "正在生成...",
|
"messageItem.status.generating": "正在生成...",
|
||||||
"messageItem.status.sending": "正在发送...",
|
"messageItem.status.sending": "正在发送...",
|
||||||
"messageItem.status.failedToSend": "消息发送失败",
|
"messageItem.status.failedToSend": "消息发送失败",
|
||||||
|
"messagePart.actions.delete": "删除",
|
||||||
|
"messagePart.actions.deleting": "正在删除...",
|
||||||
|
"messagePart.actions.deleteTitle": "删除此项",
|
||||||
|
"messagePart.actions.deleteFailedTitle": "删除失败",
|
||||||
|
"messagePart.actions.deleteFailedMessage": "删除失败",
|
||||||
"messageItem.attachment.defaultName": "附件",
|
"messageItem.attachment.defaultName": "附件",
|
||||||
"messageItem.attachment.downloadAriaLabel": "下载 {name}",
|
"messageItem.attachment.downloadAriaLabel": "下载 {name}",
|
||||||
"messageItem.agentMeta.agentLabel": "智能体:{agent}",
|
"messageItem.agentMeta.agentLabel": "智能体:{agent}",
|
||||||
|
|||||||
@@ -6,6 +6,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 { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { requestData } from "../lib/opencode-api"
|
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 {
|
export {
|
||||||
abortSession,
|
abortSession,
|
||||||
|
deleteMessagePart,
|
||||||
executeCustomCommand,
|
executeCustomCommand,
|
||||||
renameSession,
|
renameSession,
|
||||||
runShellCommand,
|
runShellCommand,
|
||||||
|
|||||||
Reference in New Issue
Block a user