chore(ui): remove delete-part actions and use trash for delete
This commit is contained in:
@@ -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, MessageSquareX, Trash2 } from "lucide-solid"
|
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, 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"
|
||||||
@@ -12,7 +12,6 @@ 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 { showAlertDialog } from "../stores/alerts"
|
||||||
import { deleteMessagePart } from "../stores/session-actions"
|
|
||||||
import { deleteMessage } from "../stores/session-actions"
|
import { deleteMessage } from "../stores/session-actions"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
import type { DeleteHoverState } from "../types/delete-hover"
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
@@ -199,7 +198,6 @@ interface MessageContentItemProps {
|
|||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
showDeleteMessage?: boolean
|
showDeleteMessage?: boolean
|
||||||
deleteHover?: () => DeleteHoverState
|
|
||||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +286,6 @@ function MessageContentItem(props: MessageContentItemProps) {
|
|||||||
isQueued={isQueued()}
|
isQueued={isQueued()}
|
||||||
showAgentMeta={showAgentMeta()}
|
showAgentMeta={showAgentMeta()}
|
||||||
showDeleteMessage={props.showDeleteMessage}
|
showDeleteMessage={props.showDeleteMessage}
|
||||||
deleteHover={props.deleteHover}
|
|
||||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
@@ -307,15 +304,12 @@ interface ToolCallItemProps {
|
|||||||
partId: string
|
partId: string
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
showDeleteMessage?: boolean
|
showDeleteMessage?: boolean
|
||||||
deleteHover?: () => DeleteHoverState
|
|
||||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolCallItem(props: ToolCallItemProps) {
|
function ToolCallItem(props: ToolCallItemProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [deleting, setDeleting] = createSignal(false)
|
|
||||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
const [hoverDeletePart, setHoverDeletePart] = 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))
|
||||||
@@ -332,14 +326,6 @@ 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 ""
|
||||||
@@ -363,26 +349,6 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteMessage = async (event: MouseEvent) => {
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@@ -404,18 +370,10 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDeleteHoveredFromStore = () => {
|
|
||||||
const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState)
|
|
||||||
return hover.kind === "part" && hover.messageId === props.messageId && hover.partId === props.partId
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={toolPart()}>
|
<Show when={toolPart()}>
|
||||||
{(resolvedToolPart) => (
|
{(resolvedToolPart) => (
|
||||||
<div
|
<div class="delete-hover-scope">
|
||||||
class="delete-hover-scope"
|
|
||||||
data-delete-part-hover={(hoverDeletePart() || isDeleteHoveredFromStore()) ? "true" : undefined}
|
|
||||||
>
|
|
||||||
<div class="tool-call-header-label">
|
<div class="tool-call-header-label">
|
||||||
<div class="tool-call-header-meta">
|
<div class="tool-call-header-meta">
|
||||||
<span class="tool-call-icon">{TOOL_ICON}</span>
|
<span class="tool-call-icon">{TOOL_ICON}</span>
|
||||||
@@ -423,7 +381,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"
|
||||||
@@ -437,41 +395,22 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={props.showDeleteMessage}>
|
||||||
<button
|
<button
|
||||||
class="tool-call-header-button"
|
class="tool-call-header-button"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={deleteDisabled()}
|
disabled={deletingMessage()}
|
||||||
onClick={handleDeleteToolPart}
|
onClick={handleDeleteMessage}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||||
setHoverDeletePart(true)
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
props.onDeleteHoverChange?.({ kind: "part", messageId: props.messageId, partId: props.partId, partType: "tool" })
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
}}
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
onMouseLeave={() => {
|
|
||||||
setHoverDeletePart(false)
|
|
||||||
props.onDeleteHoverChange?.({ kind: "none" })
|
|
||||||
}}
|
|
||||||
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")}
|
|
||||||
>
|
>
|
||||||
<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>
|
||||||
<Show when={props.showDeleteMessage}>
|
|
||||||
<button
|
|
||||||
class="tool-call-header-button"
|
|
||||||
type="button"
|
|
||||||
disabled={deletingMessage()}
|
|
||||||
onClick={handleDeleteMessage}
|
|
||||||
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
|
||||||
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
|
||||||
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()}
|
||||||
@@ -758,7 +697,6 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
messageIndex={props.messageIndex}
|
messageIndex={props.messageIndex}
|
||||||
lastAssistantIndex={props.lastAssistantIndex}
|
lastAssistantIndex={props.lastAssistantIndex}
|
||||||
showDeleteMessage={index() === 0}
|
showDeleteMessage={index() === 0}
|
||||||
deleteHover={props.deleteHover}
|
|
||||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
@@ -777,7 +715,6 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
messageId={toolItem.messageId}
|
messageId={toolItem.messageId}
|
||||||
partId={toolItem.partId}
|
partId={toolItem.partId}
|
||||||
showDeleteMessage={index() === 0}
|
showDeleteMessage={index() === 0}
|
||||||
deleteHover={props.deleteHover}
|
|
||||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
@@ -820,9 +757,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
messageId={(item as CompactionDisplayItem).messageId}
|
messageId={(item as CompactionDisplayItem).messageId}
|
||||||
partId={(item as CompactionDisplayItem).partId}
|
|
||||||
showDeleteMessage={index() === 0}
|
showDeleteMessage={index() === 0}
|
||||||
deleteHover={props.deleteHover}
|
|
||||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
@@ -833,11 +768,9 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
messageId={(item as ReasoningDisplayItem).messageId}
|
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}
|
||||||
showDeleteMessage={index() === 0}
|
showDeleteMessage={index() === 0}
|
||||||
deleteHover={props.deleteHover}
|
|
||||||
onDeleteHoverChange={props.onDeleteHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
@@ -871,17 +804,13 @@ interface CompactionCardProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
messageId: string
|
messageId: string
|
||||||
partId: string
|
|
||||||
showDeleteMessage?: boolean
|
showDeleteMessage?: boolean
|
||||||
deleteHover?: () => DeleteHoverState
|
|
||||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function CompactionCard(props: CompactionCardProps) {
|
function CompactionCard(props: CompactionCardProps) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [deleting, setDeleting] = createSignal(false)
|
|
||||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
const [hoverDeletePart, setHoverDeletePart] = 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)
|
||||||
@@ -889,26 +818,6 @@ function CompactionCard(props: CompactionCardProps) {
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
||||||
|
|
||||||
const handleDeleteMessage = async (event: MouseEvent) => {
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
@@ -930,15 +839,9 @@ function CompactionCard(props: CompactionCardProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDeleteHoveredFromStore = () => {
|
|
||||||
const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState)
|
|
||||||
return hover.kind === "part" && hover.messageId === props.messageId && hover.partId === props.partId
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`delete-hover-scope ${containerClass()} relative`}
|
class={`delete-hover-scope ${containerClass()} relative`}
|
||||||
data-delete-part-hover={(hoverDeletePart() || isDeleteHoveredFromStore()) ? "true" : undefined}
|
|
||||||
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")}
|
||||||
@@ -955,28 +858,9 @@ function CompactionCard(props: CompactionCardProps) {
|
|||||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
aria-label={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" />
|
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="tool-call-header-button"
|
|
||||||
disabled={!canDelete()}
|
|
||||||
onClick={handleDelete}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setHoverDeletePart(true)
|
|
||||||
props.onDeleteHoverChange?.({ kind: "part", messageId: props.messageId, partId: props.partId, partType: "compaction" })
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
setHoverDeletePart(false)
|
|
||||||
props.onDeleteHoverChange?.({ kind: "none" })
|
|
||||||
}}
|
|
||||||
title={t("messagePart.actions.deleteTitle")}
|
|
||||||
aria-label={t("messagePart.actions.deleteTitle")}
|
|
||||||
>
|
|
||||||
{deleting() ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message-compaction-row">
|
<div class="message-compaction-row">
|
||||||
@@ -1097,7 +981,7 @@ function StepCard(props: StepCardProps) {
|
|||||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
aria-label={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" />
|
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
@@ -1137,25 +1021,16 @@ interface ReasoningCardProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
messageId: string
|
messageId: string
|
||||||
partId: string
|
|
||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
defaultExpanded?: boolean
|
defaultExpanded?: boolean
|
||||||
showDeleteMessage?: boolean
|
showDeleteMessage?: boolean
|
||||||
deleteHover?: () => DeleteHoverState
|
|
||||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
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 [deletingMessage, setDeletingMessage] = createSignal(false)
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
const [hoverDeletePart, setHoverDeletePart] = createSignal(false)
|
|
||||||
|
|
||||||
const isDeleteHoveredFromStore = () => {
|
|
||||||
const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState)
|
|
||||||
return hover.kind === "part" && hover.messageId === props.messageId && hover.partId === props.partId
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setExpanded(Boolean(props.defaultExpanded))
|
setExpanded(Boolean(props.defaultExpanded))
|
||||||
@@ -1222,27 +1097,6 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
const viewHideLabel = () =>
|
const viewHideLabel = () =>
|
||||||
expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")
|
expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")
|
||||||
|
|
||||||
const hasDeleteTarget = () => Boolean(props.partId)
|
|
||||||
const canDelete = () => hasDeleteTarget() && !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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
||||||
|
|
||||||
const handleDeleteMessage = async (event: MouseEvent) => {
|
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||||
@@ -1265,10 +1119,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class="delete-hover-scope message-reasoning-card">
|
||||||
class="delete-hover-scope message-reasoning-card"
|
|
||||||
data-delete-part-hover={(hoverDeletePart() || isDeleteHoveredFromStore()) ? "true" : undefined}
|
|
||||||
>
|
|
||||||
<div class="message-reasoning-header">
|
<div class="message-reasoning-header">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1313,27 +1164,6 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Show when={hasDeleteTarget()}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="message-action-button"
|
|
||||||
onClick={handleDelete}
|
|
||||||
disabled={!canDelete()}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setHoverDeletePart(true)
|
|
||||||
props.onDeleteHoverChange?.({ kind: "part", messageId: props.messageId, partId: props.partId, partType: "reasoning" })
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
setHoverDeletePart(false)
|
|
||||||
props.onDeleteHoverChange?.({ kind: "none" })
|
|
||||||
}}
|
|
||||||
aria-label={t("messagePart.actions.deleteTitle")}
|
|
||||||
title={t("messagePart.actions.deleteTitle")}
|
|
||||||
>
|
|
||||||
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={props.showDeleteMessage}>
|
<Show when={props.showDeleteMessage}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1345,7 +1175,7 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
title={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" />
|
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { For, Show, createSignal } from "solid-js"
|
import { For, Show, createSignal } from "solid-js"
|
||||||
import { Copy, MessageSquareX, Split, Trash2, Undo } from "lucide-solid"
|
import { Copy, 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 { deleteMessage, deleteMessagePart } from "../stores/session-actions"
|
import { deleteMessage } from "../stores/session-actions"
|
||||||
import { isTauriHost } from "../lib/runtime-env"
|
import { isTauriHost } from "../lib/runtime-env"
|
||||||
import type { DeleteHoverState } from "../types/delete-hover"
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
@@ -23,21 +23,13 @@ interface MessageItemProps {
|
|||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
showDeleteMessage?: boolean
|
showDeleteMessage?: boolean
|
||||||
deleteHover?: () => DeleteHoverState
|
|
||||||
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
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 [deletingMessage, setDeletingMessage] = createSignal(false)
|
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||||
const [hoveredDeletePartId, setHoveredDeletePartId] = createSignal<string | null>(null)
|
|
||||||
|
|
||||||
const isDeleteHoveredFromStore = (partId: string) => {
|
|
||||||
const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState)
|
|
||||||
return hover.kind === "part" && hover.messageId === props.record.id && hover.partId === partId
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -201,50 +193,6 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteMessage = async () => {
|
const handleDeleteMessage = async () => {
|
||||||
if (deletingMessage()) return
|
if (deletingMessage()) return
|
||||||
setDeletingMessage(true)
|
setDeletingMessage(true)
|
||||||
@@ -366,7 +314,7 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
aria-label={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" />
|
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -382,28 +330,6 @@ 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={deletableTextPartId()}>
|
|
||||||
{(partId) => (
|
|
||||||
<button
|
|
||||||
class="message-action-button"
|
|
||||||
onClick={() => void handleDeletePart(partId())}
|
|
||||||
disabled={isDeletingPart(partId())}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setHoveredDeletePartId(partId())
|
|
||||||
props.onDeleteHoverChange?.({ kind: "part", messageId: props.record.id, partId: partId(), partType: "text" })
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
setHoveredDeletePartId(null)
|
|
||||||
props.onDeleteHoverChange?.({ kind: "none" })
|
|
||||||
}}
|
|
||||||
title={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
|
||||||
aria-label={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
|
||||||
>
|
|
||||||
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={props.showDeleteMessage}>
|
<Show when={props.showDeleteMessage}>
|
||||||
<button
|
<button
|
||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
@@ -414,7 +340,7 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
aria-label={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" />
|
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -452,16 +378,8 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
|
|
||||||
<For each={messageParts()}>
|
<For each={messageParts()}>
|
||||||
{(part) => {
|
{(part) => {
|
||||||
const partId = typeof (part as any)?.id === "string" ? ((part as any).id as string) : ""
|
|
||||||
const isHoveredDeleteTarget = () =>
|
|
||||||
Boolean(partId) && (hoveredDeletePartId() === partId || isDeleteHoveredFromStore(partId))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class="message-part-shell">
|
||||||
class="delete-hover-scope message-part-shell"
|
|
||||||
data-part-id={partId}
|
|
||||||
data-delete-part-hover={isHoveredDeleteTarget() ? "true" : undefined}
|
|
||||||
>
|
|
||||||
<MessagePart
|
<MessagePart
|
||||||
part={part}
|
part={part}
|
||||||
messageType={props.record.role}
|
messageType={props.record.role}
|
||||||
@@ -481,12 +399,9 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
{(attachment) => {
|
{(attachment) => {
|
||||||
const name = getAttachmentName(attachment)
|
const name = getAttachmentName(attachment)
|
||||||
const isImage = isImageAttachment(attachment)
|
const isImage = isImageAttachment(attachment)
|
||||||
const isHoveredDeleteTarget = () =>
|
|
||||||
Boolean(attachment.id) && (hoveredDeletePartId() === attachment.id || isDeleteHoveredFromStore(attachment.id))
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`delete-hover-scope attachment-chip ${isImage ? "attachment-chip-image" : ""}`}
|
class={`attachment-chip ${isImage ? "attachment-chip-image" : ""}`}
|
||||||
data-delete-part-hover={isHoveredDeleteTarget() ? "true" : undefined}
|
|
||||||
title={name}
|
title={name}
|
||||||
>
|
>
|
||||||
<Show when={isImage} fallback={
|
<Show when={isImage} fallback={
|
||||||
@@ -516,29 +431,6 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => void handleDeletePart(attachment.id)}
|
|
||||||
class="attachment-remove"
|
|
||||||
disabled={isDeletingPart(attachment.id)}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
if (attachment.id) {
|
|
||||||
setHoveredDeletePartId(attachment.id)
|
|
||||||
props.onDeleteHoverChange?.({ kind: "part", messageId: props.record.id, partId: attachment.id, partType: "file" })
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
setHoveredDeletePartId(null)
|
|
||||||
props.onDeleteHoverChange?.({ kind: "none" })
|
|
||||||
}}
|
|
||||||
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} />
|
||||||
|
|||||||
@@ -419,16 +419,6 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
if (hover.kind === "message") {
|
if (hover.kind === "message") {
|
||||||
return hover.messageId === segment.messageId
|
return hover.messageId === segment.messageId
|
||||||
}
|
}
|
||||||
if (hover.kind === "part") {
|
|
||||||
if (hover.messageId !== segment.messageId) return false
|
|
||||||
if (segment.type === "tool") {
|
|
||||||
return segment.toolPartIds?.includes(hover.partId) ?? false
|
|
||||||
}
|
|
||||||
if (segment.type === "compaction") {
|
|
||||||
return segment.partId === hover.partId
|
|
||||||
}
|
|
||||||
return segment.partIds?.includes(hover.partId) ?? false
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export type DeleteHoverState =
|
export type DeleteHoverState =
|
||||||
| { kind: "none" }
|
| { kind: "none" }
|
||||||
| { kind: "message"; messageId: string }
|
| { kind: "message"; messageId: string }
|
||||||
| { kind: "part"; messageId: string; partId: string; partType?: string }
|
|
||||||
|
|||||||
Reference in New Issue
Block a user