From bec1af6523b9ecd8dbceaf97cb83e2356f746056 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Wed, 4 Mar 2026 00:41:23 +0000 Subject: [PATCH] fix(ui): keep delete selection consistent across stream and timeline --- packages/ui/src/components/message-block.tsx | 27 ++++++++++++++++++- .../ui/src/components/message-section.tsx | 14 +++++++--- .../ui/src/components/message-timeline.tsx | 16 ++++++++--- .../src/styles/messaging/message-timeline.css | 16 +++++++++++ 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index 070b303c..9b518435 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -318,9 +318,11 @@ interface ToolCallItemProps { partId: string onContentRendered?: () => void showDeleteMessage?: boolean + deleteHover?: () => DeleteHoverState onDeleteHoverChange?: (state: DeleteHoverState) => void onDeleteMessagesUpTo?: (messageId: string) => void | Promise selectedMessageIds?: () => Set + selectedToolPartKeys?: () => Set onToggleSelectedMessage?: (messageId: string, selected: boolean) => void } @@ -331,6 +333,26 @@ function ToolCallItem(props: ToolCallItemProps) { const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId)) + const isSelectedToolPartForDeletion = () => Boolean(props.selectedToolPartKeys?.().has(`${props.messageId}:${props.partId}`)) + + const isDeleteOverlayActive = () => { + if (isSelectedForDeletion()) return true + if (isSelectedToolPartForDeletion()) return true + const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState) + if (hover.kind === "message") { + return hover.messageId === props.messageId + } + if (hover.kind === "deleteUpTo") { + const ids = props.store().getSessionMessageIds(props.sessionId) + const targetIndex = ids.indexOf(hover.messageId) + if (targetIndex === -1) return false + const currentIndex = ids.indexOf(props.messageId) + if (currentIndex === -1) return false + return currentIndex >= targetIndex + } + return false + } + const record = createMemo(() => props.store().getMessage(props.messageId)) const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId)) const partEntry = createMemo(() => record()?.parts?.[props.partId]) @@ -408,7 +430,7 @@ function ToolCallItem(props: ToolCallItemProps) { return ( {(resolvedToolPart) => ( -
+
@@ -543,6 +565,7 @@ interface MessageBlockProps { deleteHover?: () => DeleteHoverState onDeleteHoverChange?: (state: DeleteHoverState) => void selectedMessageIds?: () => Set + selectedToolPartKeys?: () => Set onToggleSelectedMessage?: (messageId: string, selected: boolean) => void onRevert?: (messageId: string) => void onDeleteMessagesUpTo?: (messageId: string) => void | Promise @@ -806,9 +829,11 @@ export default function MessageBlock(props: MessageBlockProps) { messageId={toolItem.messageId} partId={toolItem.partId} showDeleteMessage={index() === 0} + deleteHover={props.deleteHover} onDeleteHoverChange={props.onDeleteHoverChange} onDeleteMessagesUpTo={props.onDeleteMessagesUpTo} selectedMessageIds={props.selectedMessageIds} + selectedToolPartKeys={props.selectedToolPartKeys} onToggleSelectedMessage={props.onToggleSelectedMessage} onContentRendered={props.onContentRendered} /> diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index 3883b4eb..5da2432f 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -294,8 +294,7 @@ export default function MessageSection(props: MessageSectionProps) { } const handleClearTimelineSelection = () => { - setSelectedTimelineIds(new Set()) - setLastSelectionAnchorId(null) + clearDeleteMode() } const applySelectionMode = (mode: "all" | "tools") => { @@ -412,6 +411,14 @@ export default function MessageSection(props: MessageSectionProps) { const allowed = deletableMessageIds() return selectedToolParts().filter((entry) => allowed.has(entry.messageId) && !messageIds.has(entry.messageId)) }) + + const deleteToolPartKeys = createMemo(() => { + const set = new Set() + for (const entry of deleteToolParts()) { + set.add(`${entry.messageId}:${entry.partId}`) + } + return set + }) const isDeleteMode = createMemo(() => deleteMessageIds().size > 0 || deleteToolParts().length > 0) const selectedDeleteCount = createMemo(() => deleteMessageIds().size + deleteToolParts().length) @@ -492,12 +499,12 @@ export default function MessageSection(props: MessageSectionProps) { setDeleteHover({ kind: "none" }) setSelectedTimelineIds(new Set()) setLastSelectionAnchorId(null) + setIsDeleteMenuOpen(false) } createEffect(() => { const timelineIds = selectedTimelineIds() if (timelineIds.size === 0) { - setSelectedForDeletion(new Set()) return } const segments = timelineSegments() @@ -1073,6 +1080,7 @@ export default function MessageSection(props: MessageSectionProps) { deleteHover={deleteHover} onDeleteHoverChange={setDeleteHover} selectedMessageIds={selectedForDeletion} + selectedToolPartKeys={deleteToolPartKeys} onToggleSelectedMessage={setMessageSelectedForDeletion} onRevert={props.onRevert} onDeleteMessagesUpTo={props.onDeleteMessagesUpTo} diff --git a/packages/ui/src/components/message-timeline.tsx b/packages/ui/src/components/message-timeline.tsx index 404decd3..5b48b130 100644 --- a/packages/ui/src/components/message-timeline.tsx +++ b/packages/ui/src/components/message-timeline.tsx @@ -715,6 +715,12 @@ const MessageTimeline: Component = (props) => { return false } + const isDeleteSelected = () => { + const selected = props.selectedMessageIds?.() + if (!selected) return false + return selected.has(segment.messageId) + } + const hasActivePermission = () => { if (segment.type !== "tool") return false const partIds = segment.toolPartIds ?? [] @@ -727,7 +733,9 @@ const MessageTimeline: Component = (props) => { } const isExpanded = () => props.expandedMessageIds?.().has(segment.messageId) ?? false - const isHidden = () => segment.type === "tool" && !(showTools() || isExpanded() || isSelectionActive() || isActive() || hasActivePermission() || isDeleteHovered()) + const isHidden = () => + segment.type === "tool" && + !(showTools() || isExpanded() || isSelectionActive() || isActive() || hasActivePermission() || isDeleteHovered() || isDeleteSelected()) // Group visual indicators: tools belong to the same message as their // assistant. Uses messageId for correctness (not positional adjacency). @@ -762,13 +770,13 @@ const MessageTimeline: Component = (props) => { } return ( -