fix(ui): sync delete-hover overlays across preview and stream

This commit is contained in:
Shantur Rathore
2026-02-26 10:10:46 +00:00
parent 688b127c6d
commit b91dbb1a60
5 changed files with 52 additions and 5 deletions

View File

@@ -199,6 +199,7 @@ interface MessageContentItemProps {
onFork?: (messageId?: string) => void
onContentRendered?: () => void
showDeleteMessage?: boolean
deleteHover?: () => DeleteHoverState
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
@@ -287,6 +288,7 @@ function MessageContentItem(props: MessageContentItemProps) {
isQueued={isQueued()}
showAgentMeta={showAgentMeta()}
showDeleteMessage={props.showDeleteMessage}
deleteHover={props.deleteHover}
onDeleteHoverChange={props.onDeleteHoverChange}
onRevert={props.onRevert}
onFork={props.onFork}
@@ -305,6 +307,7 @@ interface ToolCallItemProps {
partId: string
onContentRendered?: () => void
showDeleteMessage?: boolean
deleteHover?: () => DeleteHoverState
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
@@ -401,10 +404,18 @@ 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 (
<Show when={toolPart()}>
{(resolvedToolPart) => (
<div class="delete-hover-scope" data-delete-part-hover={hoverDeletePart() ? "true" : undefined}>
<div
class="delete-hover-scope"
data-delete-part-hover={(hoverDeletePart() || isDeleteHoveredFromStore()) ? "true" : undefined}
>
<div class="tool-call-header-label">
<div class="tool-call-header-meta">
<span class="tool-call-icon">{TOOL_ICON}</span>
@@ -747,6 +758,7 @@ export default function MessageBlock(props: MessageBlockProps) {
messageIndex={props.messageIndex}
lastAssistantIndex={props.lastAssistantIndex}
showDeleteMessage={index() === 0}
deleteHover={props.deleteHover}
onDeleteHoverChange={props.onDeleteHoverChange}
onRevert={props.onRevert}
onFork={props.onFork}
@@ -765,6 +777,7 @@ export default function MessageBlock(props: MessageBlockProps) {
messageId={toolItem.messageId}
partId={toolItem.partId}
showDeleteMessage={index() === 0}
deleteHover={props.deleteHover}
onDeleteHoverChange={props.onDeleteHoverChange}
onContentRendered={props.onContentRendered}
/>
@@ -809,6 +822,7 @@ export default function MessageBlock(props: MessageBlockProps) {
messageId={(item as CompactionDisplayItem).messageId}
partId={(item as CompactionDisplayItem).partId}
showDeleteMessage={index() === 0}
deleteHover={props.deleteHover}
onDeleteHoverChange={props.onDeleteHoverChange}
/>
</Match>
@@ -823,6 +837,7 @@ export default function MessageBlock(props: MessageBlockProps) {
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
showDeleteMessage={index() === 0}
deleteHover={props.deleteHover}
onDeleteHoverChange={props.onDeleteHoverChange}
/>
</Match>
@@ -858,6 +873,7 @@ interface CompactionCardProps {
messageId: string
partId: string
showDeleteMessage?: boolean
deleteHover?: () => DeleteHoverState
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
@@ -914,10 +930,15 @@ 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 (
<div
class={`delete-hover-scope ${containerClass()} relative`}
data-delete-part-hover={hoverDeletePart() ? "true" : undefined}
data-delete-part-hover={(hoverDeletePart() || isDeleteHoveredFromStore()) ? "true" : undefined}
style={{ "border-left": `4px solid ${borderColor()}` }}
role="status"
aria-label={t("messageBlock.compaction.ariaLabel")}
@@ -1120,6 +1141,7 @@ interface ReasoningCardProps {
showAgentMeta?: boolean
defaultExpanded?: boolean
showDeleteMessage?: boolean
deleteHover?: () => DeleteHoverState
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
@@ -1130,6 +1152,11 @@ function ReasoningCard(props: ReasoningCardProps) {
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(() => {
setExpanded(Boolean(props.defaultExpanded))
})
@@ -1238,7 +1265,10 @@ function ReasoningCard(props: ReasoningCardProps) {
}
return (
<div class="delete-hover-scope message-reasoning-card" data-delete-part-hover={hoverDeletePart() ? "true" : undefined}>
<div
class="delete-hover-scope message-reasoning-card"
data-delete-part-hover={(hoverDeletePart() || isDeleteHoveredFromStore()) ? "true" : undefined}
>
<div class="message-reasoning-header">
<button
type="button"

View File

@@ -23,6 +23,7 @@ interface MessageItemProps {
showAgentMeta?: boolean
onContentRendered?: () => void
showDeleteMessage?: boolean
deleteHover?: () => DeleteHoverState
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
@@ -33,6 +34,11 @@ export default function MessageItem(props: MessageItemProps) {
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 createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
@@ -447,7 +453,8 @@ export default function MessageItem(props: MessageItemProps) {
<For each={messageParts()}>
{(part) => {
const partId = typeof (part as any)?.id === "string" ? ((part as any).id as string) : ""
const isHoveredDeleteTarget = () => Boolean(partId) && hoveredDeletePartId() === partId
const isHoveredDeleteTarget = () =>
Boolean(partId) && (hoveredDeletePartId() === partId || isDeleteHoveredFromStore(partId))
return (
<div
@@ -474,7 +481,8 @@ export default function MessageItem(props: MessageItemProps) {
{(attachment) => {
const name = getAttachmentName(attachment)
const isImage = isImageAttachment(attachment)
const isHoveredDeleteTarget = () => hoveredDeletePartId() === attachment.id
const isHoveredDeleteTarget = () =>
Boolean(attachment.id) && (hoveredDeletePartId() === attachment.id || isDeleteHoveredFromStore(attachment.id))
return (
<div
class={`delete-hover-scope attachment-chip ${isImage ? "attachment-chip-image" : ""}`}

View File

@@ -1,12 +1,15 @@
import type { Component } from "solid-js"
import MessageBlock from "./message-block"
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
import type { DeleteHoverState } from "../types/delete-hover"
interface MessagePreviewProps {
instanceId: string
sessionId: string
messageId: string
store: () => InstanceMessageStore
deleteHover?: () => DeleteHoverState
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
const MessagePreview: Component<MessagePreviewProps> = (props) => {
@@ -24,6 +27,8 @@ const MessagePreview: Component<MessagePreviewProps> = (props) => {
showThinking={() => false}
thinkingDefaultExpanded={() => false}
showUsageMetrics={() => false}
deleteHover={props.deleteHover}
onDeleteHoverChange={props.onDeleteHoverChange}
/>
</div>
)

View File

@@ -963,6 +963,7 @@ export default function MessageSection(props: MessageSectionProps) {
sessionId={props.sessionId}
showToolSegments={showTimelineToolsPreference()}
deleteHover={deleteHover}
onDeleteHoverChange={setDeleteHover}
/>
</div>
</Show>

View File

@@ -32,6 +32,7 @@ interface MessageTimelineProps {
sessionId: string
showToolSegments?: boolean
deleteHover?: () => DeleteHoverState
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
const MAX_TOOLTIP_LENGTH = 220
@@ -497,6 +498,8 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
instanceId={props.instanceId}
sessionId={props.sessionId}
store={store}
deleteHover={props.deleteHover}
onDeleteHoverChange={props.onDeleteHoverChange}
/>
</div>
)