feat(ui): mirror delete hover overlay in timeline

This commit is contained in:
Shantur Rathore
2026-02-25 23:32:32 +00:00
parent 1122070b9c
commit 0f9c99e3bd
8 changed files with 176 additions and 51 deletions

View File

@@ -2,6 +2,7 @@ import { Index, type Accessor } from "solid-js"
import VirtualItem from "./virtual-item"
import MessageBlock from "./message-block"
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
import type { DeleteHoverState } from "../types/delete-hover"
export function getMessageAnchorId(messageId: string) {
return `message-anchor-${messageId}`
@@ -23,6 +24,8 @@ interface MessageBlockListProps {
onRevert?: (messageId: string) => void
onFork?: (messageId?: string) => void
onContentRendered?: () => void
deleteHover?: Accessor<DeleteHoverState>
onDeleteHoverChange?: (state: DeleteHoverState) => void
setBottomSentinel: (element: HTMLDivElement | null) => void
suspendMeasurements?: () => boolean
}
@@ -51,6 +54,8 @@ export default function MessageBlockList(props: MessageBlockListProps) {
showThinking={props.showThinking}
thinkingDefaultExpanded={props.thinkingDefaultExpanded}
showUsageMetrics={props.showUsageMetrics}
deleteHover={props.deleteHover}
onDeleteHoverChange={props.onDeleteHoverChange}
onRevert={props.onRevert}
onFork={props.onFork}
onContentRendered={props.onContentRendered}

View File

@@ -15,6 +15,7 @@ import { showAlertDialog } from "../stores/alerts"
import { deleteMessagePart } from "../stores/session-actions"
import { deleteMessage } from "../stores/session-actions"
import { useI18n } from "../lib/i18n"
import type { DeleteHoverState } from "../types/delete-hover"
const TOOL_ICON = "🔧"
const USER_BORDER_COLOR = "var(--message-user-border)"
@@ -198,7 +199,7 @@ interface MessageContentItemProps {
onFork?: (messageId?: string) => void
onContentRendered?: () => void
showDeleteMessage?: boolean
onDeleteMessageHoverChange?: (hovered: boolean) => void
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
function isSupportedPartType(part: unknown): boolean {
@@ -286,7 +287,7 @@ function MessageContentItem(props: MessageContentItemProps) {
isQueued={isQueued()}
showAgentMeta={showAgentMeta()}
showDeleteMessage={props.showDeleteMessage}
onDeleteMessageHoverChange={props.onDeleteMessageHoverChange}
onDeleteHoverChange={props.onDeleteHoverChange}
onRevert={props.onRevert}
onFork={props.onFork}
onContentRendered={props.onContentRendered}
@@ -304,7 +305,7 @@ interface ToolCallItemProps {
partId: string
onContentRendered?: () => void
showDeleteMessage?: boolean
onDeleteMessageHoverChange?: (hovered: boolean) => void
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
function ToolCallItem(props: ToolCallItemProps) {
@@ -430,8 +431,14 @@ function ToolCallItem(props: ToolCallItemProps) {
type="button"
disabled={deleteDisabled()}
onClick={handleDeleteToolPart}
onMouseEnter={() => setHoverDeletePart(true)}
onMouseLeave={() => setHoverDeletePart(false)}
onMouseEnter={() => {
setHoverDeletePart(true)
props.onDeleteHoverChange?.({ kind: "part", messageId: props.messageId, partId: props.partId, partType: "tool" })
}}
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")}
>
@@ -444,8 +451,8 @@ function ToolCallItem(props: ToolCallItemProps) {
type="button"
disabled={deletingMessage()}
onClick={handleDeleteMessage}
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
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")}
>
@@ -517,6 +524,8 @@ interface MessageBlockProps {
showThinking: () => boolean
thinkingDefaultExpanded: () => boolean
showUsageMetrics: () => boolean
deleteHover?: () => DeleteHoverState
onDeleteHoverChange?: (state: DeleteHoverState) => void
onRevert?: (messageId: string) => void
onFork?: (messageId?: string) => void
onContentRendered?: () => void
@@ -527,10 +536,10 @@ export default function MessageBlock(props: MessageBlockProps) {
const record = createMemo(() => props.store().getMessage(props.messageId))
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
const sessionCache = getSessionRenderCache(props.instanceId, props.sessionId)
const [deleteMessageHovered, setDeleteMessageHovered] = createSignal(false)
const handleDeleteMessageHoverChange = (hovered: boolean) => {
setDeleteMessageHovered(hovered)
const isDeleteMessageHovered = () => {
const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState)
return hover.kind === "message" && hover.messageId === props.messageId
}
const block = createMemo<MessageDisplayBlock | null>(() => {
@@ -723,7 +732,7 @@ export default function MessageBlock(props: MessageBlockProps) {
<div
class="message-stream-block"
data-message-id={resolvedBlock().record.id}
data-delete-message-hover={deleteMessageHovered() ? "true" : undefined}
data-delete-message-hover={isDeleteMessageHovered() ? "true" : undefined}
>
<For each={resolvedBlock().items}>
{(item, index) => (
@@ -738,7 +747,7 @@ export default function MessageBlock(props: MessageBlockProps) {
messageIndex={props.messageIndex}
lastAssistantIndex={props.lastAssistantIndex}
showDeleteMessage={index() === 0}
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
onDeleteHoverChange={props.onDeleteHoverChange}
onRevert={props.onRevert}
onFork={props.onFork}
onContentRendered={props.onContentRendered}
@@ -756,7 +765,7 @@ export default function MessageBlock(props: MessageBlockProps) {
messageId={toolItem.messageId}
partId={toolItem.partId}
showDeleteMessage={index() === 0}
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
onDeleteHoverChange={props.onDeleteHoverChange}
onContentRendered={props.onContentRendered}
/>
</div>
@@ -773,7 +782,7 @@ export default function MessageBlock(props: MessageBlockProps) {
instanceId={props.instanceId}
sessionId={props.sessionId}
messageId={props.messageId}
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
onDeleteHoverChange={props.onDeleteHoverChange}
/>
</Match>
<Match when={item.type === "step-finish"}>
@@ -787,7 +796,7 @@ export default function MessageBlock(props: MessageBlockProps) {
instanceId={props.instanceId}
sessionId={props.sessionId}
messageId={props.messageId}
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
onDeleteHoverChange={props.onDeleteHoverChange}
/>
</Match>
<Match when={item.type === "compaction"}>
@@ -800,7 +809,7 @@ export default function MessageBlock(props: MessageBlockProps) {
messageId={(item as CompactionDisplayItem).messageId}
partId={(item as CompactionDisplayItem).partId}
showDeleteMessage={index() === 0}
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
onDeleteHoverChange={props.onDeleteHoverChange}
/>
</Match>
<Match when={item.type === "reasoning"}>
@@ -814,7 +823,7 @@ export default function MessageBlock(props: MessageBlockProps) {
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
showDeleteMessage={index() === 0}
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
onDeleteHoverChange={props.onDeleteHoverChange}
/>
</Match>
</Switch>
@@ -837,7 +846,7 @@ interface StepCardProps {
instanceId?: string
sessionId?: string
messageId?: string
onDeleteMessageHoverChange?: (hovered: boolean) => void
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
interface CompactionCardProps {
@@ -849,7 +858,7 @@ interface CompactionCardProps {
messageId: string
partId: string
showDeleteMessage?: boolean
onDeleteMessageHoverChange?: (hovered: boolean) => void
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
function CompactionCard(props: CompactionCardProps) {
@@ -920,8 +929,8 @@ function CompactionCard(props: CompactionCardProps) {
class="tool-call-header-button"
disabled={!canDeleteMessage()}
onClick={handleDeleteMessage}
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
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")}
>
@@ -934,8 +943,14 @@ function CompactionCard(props: CompactionCardProps) {
class="tool-call-header-button"
disabled={!canDelete()}
onClick={handleDelete}
onMouseEnter={() => setHoverDeletePart(true)}
onMouseLeave={() => setHoverDeletePart(false)}
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")}
>
@@ -1056,8 +1071,8 @@ function StepCard(props: StepCardProps) {
class="message-action-button absolute right-2 top-1/2 -translate-y-1/2"
disabled={!canDeleteMessage()}
onClick={handleDeleteMessage}
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
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")}
>
@@ -1105,7 +1120,7 @@ interface ReasoningCardProps {
showAgentMeta?: boolean
defaultExpanded?: boolean
showDeleteMessage?: boolean
onDeleteMessageHoverChange?: (hovered: boolean) => void
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
function ReasoningCard(props: ReasoningCardProps) {
@@ -1274,8 +1289,14 @@ function ReasoningCard(props: ReasoningCardProps) {
class="message-action-button"
onClick={handleDelete}
disabled={!canDelete()}
onMouseEnter={() => setHoverDeletePart(true)}
onMouseLeave={() => setHoverDeletePart(false)}
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")}
>
@@ -1289,8 +1310,8 @@ function ReasoningCard(props: ReasoningCardProps) {
class="message-action-button"
onClick={handleDeleteMessage}
disabled={!canDeleteMessage()}
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>

View File

@@ -9,6 +9,7 @@ import { useI18n } from "../lib/i18n"
import { showAlertDialog } from "../stores/alerts"
import { deleteMessage, deleteMessagePart } from "../stores/session-actions"
import { isTauriHost } from "../lib/runtime-env"
import type { DeleteHoverState } from "../types/delete-hover"
interface MessageItemProps {
record: MessageRecord
@@ -22,7 +23,7 @@ interface MessageItemProps {
showAgentMeta?: boolean
onContentRendered?: () => void
showDeleteMessage?: boolean
onDeleteMessageHoverChange?: (hovered: boolean) => void
onDeleteHoverChange?: (state: DeleteHoverState) => void
}
export default function MessageItem(props: MessageItemProps) {
@@ -354,8 +355,8 @@ export default function MessageItem(props: MessageItemProps) {
class="message-action-button"
onClick={handleDeleteMessage}
disabled={deletingMessage()}
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.record.id })}
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")}
>
@@ -381,8 +382,14 @@ export default function MessageItem(props: MessageItemProps) {
class="message-action-button"
onClick={() => void handleDeletePart(partId())}
disabled={isDeletingPart(partId())}
onMouseEnter={() => setHoveredDeletePartId(partId())}
onMouseLeave={() => setHoveredDeletePartId(null)}
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")}
>
@@ -396,8 +403,8 @@ export default function MessageItem(props: MessageItemProps) {
class="message-action-button"
onClick={handleDeleteMessage}
disabled={deletingMessage()}
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.record.id })}
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")}
>
@@ -507,8 +514,16 @@ export default function MessageItem(props: MessageItemProps) {
onClick={() => void handleDeletePart(attachment.id)}
class="attachment-remove"
disabled={isDeletingPart(attachment.id)}
onMouseEnter={() => (attachment.id ? setHoveredDeletePartId(attachment.id) : undefined)}
onMouseLeave={() => setHoveredDeletePartId(null)}
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")}
>

View File

@@ -10,6 +10,7 @@ import { useI18n } from "../lib/i18n"
import { copyToClipboard } from "../lib/clipboard"
import { showToastNotification } from "../lib/notifications"
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
import type { DeleteHoverState } from "../types/delete-hover"
const SCROLL_SCOPE = "session"
const SCROLL_SENTINEL_MARGIN_PX = 48
@@ -145,6 +146,8 @@ export default function MessageSection(props: MessageSectionProps) {
}
}
const [activeMessageId, setActiveMessageId] = createSignal<string | null>(null)
const [deleteHover, setDeleteHover] = createSignal<DeleteHoverState>({ kind: "none" })
const changeToken = createMemo(() => String(sessionRevision()))
const isActive = createMemo(() => props.isActive !== false)
@@ -899,6 +902,8 @@ export default function MessageSection(props: MessageSectionProps) {
onRevert={props.onRevert}
onFork={props.onFork}
onContentRendered={handleContentRendered}
deleteHover={deleteHover}
onDeleteHoverChange={setDeleteHover}
setBottomSentinel={setBottomSentinel}
suspendMeasurements={() => !isActive()}
/>
@@ -957,6 +962,7 @@ export default function MessageSection(props: MessageSectionProps) {
instanceId={props.instanceId}
sessionId={props.sessionId}
showToolSegments={showTimelineToolsPreference()}
deleteHover={deleteHover}
/>
</div>
</Show>

View File

@@ -7,6 +7,7 @@ import { buildRecordDisplayData } from "../stores/message-v2/record-display-cach
import { getToolIcon } from "./tool-call/utils"
import { User as UserIcon, Bot as BotIcon, FoldVertical, ShieldAlert } from "lucide-solid"
import { useI18n } from "../lib/i18n"
import type { DeleteHoverState } from "../types/delete-hover"
export type TimelineSegmentType = "user" | "assistant" | "tool" | "compaction"
@@ -19,6 +20,8 @@ export interface TimelineSegment {
shortLabel?: string
variant?: "auto" | "manual"
toolPartIds?: string[]
partIds?: string[]
partId?: string
}
interface MessageTimelineProps {
@@ -28,6 +31,7 @@ interface MessageTimelineProps {
instanceId: string
sessionId: string
showToolSegments?: boolean
deleteHover?: () => DeleteHoverState
}
const MAX_TOOLTIP_LENGTH = 220
@@ -42,6 +46,7 @@ interface PendingSegment {
toolTypeLabels: string[]
toolIcons: string[]
toolPartIds: string[]
partIds: string[]
hasPrimaryText: boolean
}
@@ -191,6 +196,7 @@ export function buildTimelineSegments(
tooltip,
shortLabel,
toolPartIds: isToolSegment ? pending.toolPartIds : undefined,
partIds: !isToolSegment ? pending.partIds : undefined,
})
segmentIndex += 1
pending = null
@@ -199,7 +205,17 @@ export function buildTimelineSegments(
const ensureSegment = (type: TimelineSegmentType): PendingSegment => {
if (!pending || pending.type !== type) {
flushPending()
pending = { type, texts: [], reasoningTexts: [], toolTitles: [], toolTypeLabels: [], toolIcons: [], toolPartIds: [], hasPrimaryText: type !== "assistant" }
pending = {
type,
texts: [],
reasoningTexts: [],
toolTitles: [],
toolTypeLabels: [],
toolIcons: [],
toolPartIds: [],
partIds: [],
hasPrimaryText: type !== "assistant",
}
}
return pending!
}
@@ -228,6 +244,9 @@ export function buildTimelineSegments(
const target = ensureSegment(defaultContentType)
if (target) {
target.reasoningTexts.push(text)
if (typeof (part as any).id === "string" && (part as any).id.length > 0) {
target.partIds.push((part as any).id)
}
}
continue
}
@@ -235,6 +254,7 @@ export function buildTimelineSegments(
if (part.type === "compaction") {
flushPending()
const isAuto = Boolean((part as any)?.auto)
const partId = typeof (part as any)?.id === "string" ? ((part as any).id as string) : ""
result.push({
id: `${record.id}:${segmentIndex}`,
messageId: record.id,
@@ -242,6 +262,7 @@ export function buildTimelineSegments(
label: segmentLabel("compaction"),
tooltip: isAuto ? t("messageTimeline.tooltip.compaction.auto") : t("messageTimeline.tooltip.compaction.manual"),
variant: isAuto ? "auto" : "manual",
partId,
})
segmentIndex += 1
continue
@@ -257,6 +278,9 @@ export function buildTimelineSegments(
if (target) {
target.texts.push(text)
target.hasPrimaryText = true
if (typeof (part as any).id === "string" && (part as any).id.length > 0) {
target.partIds.push((part as any).id)
}
}
}
@@ -278,6 +302,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
let hoverTimer: number | null = null
let closeTimer: number | null = null
const showTools = () => props.showToolSegments ?? true
const deleteHover = () => props.deleteHover?.() ?? { kind: "none" as const }
const registerButtonRef = (segmentId: string, element: HTMLButtonElement | null) => {
if (element) {
@@ -426,16 +451,34 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
}
return (
<button
ref={(el) => registerButtonRef(segment.id, el)}
type="button"
data-variant={segment.variant}
class={`message-timeline-segment message-timeline-${segment.type} ${hasActivePermission() ? "message-timeline-segment-permission" : ""} ${segment.type === "compaction" ? `message-timeline-compaction-${segment.variant ?? "manual"}` : ""} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`}
<button
ref={(el) => registerButtonRef(segment.id, el)}
type="button"
data-variant={segment.variant}
class={`message-timeline-segment message-timeline-${segment.type} ${hasActivePermission() ? "message-timeline-segment-permission" : ""} ${segment.type === "compaction" ? `message-timeline-compaction-${segment.variant ?? "manual"}` : ""} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`}
aria-current={isActive() ? "true" : undefined}
aria-hidden={isHidden() ? "true" : undefined}
onClick={() => props.onSegmentClick?.(segment)}
onMouseEnter={(event) => handleMouseEnter(segment, event)}
data-delete-hover={(() => {
const hover = deleteHover() as DeleteHoverState
if (hover.kind === "message") {
return hover.messageId === segment.messageId ? "true" : undefined
}
if (hover.kind === "part") {
if (hover.messageId !== segment.messageId) return undefined
if (segment.type === "tool") {
return segment.toolPartIds?.includes(hover.partId) ? "true" : undefined
}
if (segment.type === "compaction") {
return segment.partId === hover.partId ? "true" : undefined
}
return segment.partIds?.includes(hover.partId) ? "true" : undefined
}
return undefined
})()}
aria-current={isActive() ? "true" : undefined}
aria-hidden={isHidden() ? "true" : undefined}
onClick={() => props.onSegmentClick?.(segment)}
onMouseEnter={(event) => handleMouseEnter(segment, event)}
onMouseLeave={handleMouseLeave}
>
<span class="message-timeline-label message-timeline-label-full">{segment.label}</span>

View File

@@ -10,7 +10,7 @@
inset: -2px;
background: var(--status-error-bg);
box-shadow: inset 0 0 0 1px var(--status-error-fg);
border-radius: 10px;
border-radius: 0;
pointer-events: none;
/* Overlay must sit above the message cards (they have opaque backgrounds). */
z-index: 10;
@@ -30,7 +30,7 @@
inset: -2px;
background: var(--status-error-bg);
box-shadow: inset 0 0 0 1px var(--status-error-fg);
border-radius: 10px;
border-radius: 0;
pointer-events: none;
/* Overlay must sit above the part card background. */
z-index: 10;

View File

@@ -87,6 +87,7 @@
border-radius: 0;
border: 1px solid var(--border-base);
background-color: var(--surface-secondary);
position: relative;
display: flex;
align-items: center;
justify-content: center;
@@ -98,6 +99,36 @@
transition: transform 0.15s ease, background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.message-timeline-segment[data-delete-hover="true"]::before {
content: "";
position: absolute;
inset: 0;
background: var(--status-error-bg);
box-shadow: inset 0 0 0 1px var(--status-error-fg);
border-radius: 0;
pointer-events: none;
z-index: 2;
}
/* Ensure delete hover is visible even on the active segment styling. */
.message-timeline-segment[data-delete-hover="true"],
.message-timeline-segment[data-delete-hover="true"]:hover,
.message-timeline-segment[data-delete-hover="true"]:focus-visible,
.message-timeline-segment-active[data-delete-hover="true"],
.message-timeline-segment-active[data-delete-hover="true"]:hover,
.message-timeline-segment-active[data-delete-hover="true"]:focus-visible {
/* Let the ::before overlay provide the highlight (matches stream behavior). */
background-color: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
transform: none !important;
}
.message-timeline-label {
position: relative;
z-index: 3;
}
.message-timeline-segment.message-timeline-segment-hidden {
display: none !important;
}

View File

@@ -0,0 +1,4 @@
export type DeleteHoverState =
| { kind: "none" }
| { kind: "message"; messageId: string }
| { kind: "part"; messageId: string; partId: string; partType?: string }