From 69b63167f87035ca723a7a4935dea864084bff94 Mon Sep 17 00:00:00 2001 From: VooDisss Date: Sat, 7 Mar 2026 11:08:19 +0200 Subject: [PATCH 1/3] fix(ui): remove step-finish when deleting tools Delete step-finish parts for messages when tool badges are selected so usage chips disappear with the tool deletion flow in packages/ui/src/components/message-section.tsx. --- .../ui/src/components/message-section.tsx | 106 ++++++++---------- 1 file changed, 46 insertions(+), 60 deletions(-) diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index 5da2432f..2a027ca9 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -117,42 +117,6 @@ export default function MessageSection(props: MessageSectionProps) { let deleteMenuRef: HTMLDivElement | undefined let deleteMenuButtonRef: HTMLButtonElement | undefined - // Deletion is only allowed for messages/tool parts that occur AFTER the most - // recent compaction. Compaction effectively resets the stored context; deleting - // earlier items would not reliably reflect what the model sees. - const messageIndexById = createMemo(() => { - const ids = messageIds() - const map = new Map() - for (let i = 0; i < ids.length; i++) { - map.set(ids[i], i) - } - return map - }) - - const lastCompactionIndex = createMemo(() => { - // Depend on a single session revision signal (not every message/part read) - // to keep reactive overhead small. - sessionRevision() - return untrack(() => store().getLastCompactionMessageIndex(props.sessionId)) - }) - - const deletableStartIndex = createMemo(() => { - const idx = lastCompactionIndex() - return idx === -1 ? 0 : idx + 1 - }) - - const deletableMessageIds = createMemo(() => { - const ids = messageIds() - const start = deletableStartIndex() - return new Set(ids.slice(start)) - }) - - const isMessageDeletable = (messageId: string): boolean => { - const idx = messageIndexById().get(messageId) - if (idx === undefined) return false - return idx >= deletableStartIndex() - } - // Build the message group for a segment. // Tool calls belong to the same assistant turn (between user messages). // Only assistant badges trigger group selection; user/tool badges are standalone. @@ -187,10 +151,6 @@ export default function MessageSection(props: MessageSectionProps) { if (segmentIndex === -1) return const segment = segments[segmentIndex] - if (!isMessageDeletable(segment.messageId)) { - return - } - setLastSelectionAnchorId(id) if (selectionMode() === "tools" && segment.type !== "tool") { @@ -236,10 +196,6 @@ export default function MessageSection(props: MessageSectionProps) { const segmentIndex = segments.findIndex((s) => s.id === segment.id) if (segmentIndex === -1) return - if (!isMessageDeletable(segment.messageId)) { - return - } - setLastSelectionAnchorId(segment.id) if (selectionMode() === "tools" && segment.type !== "tool") { @@ -287,8 +243,8 @@ export default function MessageSection(props: MessageSectionProps) { const end = Math.max(anchorIndex, targetIndex) const rangeSegments = selectionMode() === "tools" - ? segments.slice(start, end + 1).filter((s) => s.type === "tool" && isMessageDeletable(s.messageId)) - : segments.slice(start, end + 1).filter((s) => isMessageDeletable(s.messageId)) + ? segments.slice(start, end + 1).filter((s) => s.type === "tool") + : segments.slice(start, end + 1) // Range selection replaces current selection so it can grow or shrink. setSelectedTimelineIds(new Set(rangeSegments.map((segment) => segment.id))) } @@ -301,11 +257,7 @@ export default function MessageSection(props: MessageSectionProps) { setSelectionMode(mode) if (mode !== "tools") return const segments = timelineSegments() - const toolIds = new Set( - segments - .filter((segment) => segment.type === "tool" && isMessageDeletable(segment.messageId)) - .map((segment) => segment.id), - ) + const toolIds = new Set(segments.filter((segment) => segment.type === "tool").map((segment) => segment.id)) setSelectedTimelineIds((prev) => { if (prev.size === 0) return prev const next = new Set([...prev].filter((id) => toolIds.has(id))) @@ -408,8 +360,7 @@ export default function MessageSection(props: MessageSectionProps) { const deleteMessageIds = createMemo(() => selectedForDeletion()) const deleteToolParts = createMemo(() => { const messageIds = deleteMessageIds() - const allowed = deletableMessageIds() - return selectedToolParts().filter((entry) => allowed.has(entry.messageId) && !messageIds.has(entry.messageId)) + return selectedToolParts().filter((entry) => !messageIds.has(entry.messageId)) }) const deleteToolPartKeys = createMemo(() => { @@ -482,7 +433,6 @@ export default function MessageSection(props: MessageSectionProps) { const setMessageSelectedForDeletion = (messageId: string, selected: boolean) => { if (!messageId) return - if (!isMessageDeletable(messageId)) return setSelectedForDeletion((prev) => { const next = new Set(prev) if (selected) { @@ -513,7 +463,7 @@ export default function MessageSection(props: MessageSectionProps) { const affectedMessageIds = new Set() for (const segId of timelineIds) { const segment = segmentById.get(segId) - if (segment && segment.type !== "tool" && isMessageDeletable(segment.messageId)) { + if (segment && segment.type !== "tool") { affectedMessageIds.add(segment.messageId) } } @@ -521,12 +471,11 @@ export default function MessageSection(props: MessageSectionProps) { }) const selectAllForDeletion = () => { - const allMessageIds = [...deletableMessageIds()] - setSelectedForDeletion(new Set(allMessageIds)) + setSelectedForDeletion(new Set(messageIds())) // Also select all timeline segments — tool visibility is handled by // isSelectionActive() in isHidden(), no expand/collapse needed. const segments = timelineSegments() - setSelectedTimelineIds(new Set(segments.filter((s) => isMessageDeletable(s.messageId)).map((s) => s.id))) + setSelectedTimelineIds(new Set(segments.map((s) => s.id))) } const deleteSelectedMessages = async () => { @@ -534,7 +483,18 @@ export default function MessageSection(props: MessageSectionProps) { const toolParts = deleteToolParts() if (selected.size === 0 && toolParts.length === 0) return - const allowed = deletableMessageIds() + const allowed = new Set(messageIds()) + + const toolPartsByMessage = new Map>() + for (const entry of toolParts) { + if (!allowed.has(entry.messageId)) continue + let ids = toolPartsByMessage.get(entry.messageId) + if (!ids) { + ids = new Set() + toolPartsByMessage.set(entry.messageId, ids) + } + ids.add(entry.partId) + } const idsInSessionOrder = messageIds() const toDelete: string[] = [] @@ -545,6 +505,29 @@ export default function MessageSection(props: MessageSectionProps) { } } + const stepFinishPartsToDelete: { messageId: string; partId: string }[] = [] + if (toolPartsByMessage.size > 0) { + const s = store() + for (const [messageId, selectedToolPartIds] of toolPartsByMessage.entries()) { + if (selected.has(messageId)) continue + const record = s.getMessage(messageId) + if (!record) continue + const stepFinishPartIds: string[] = [] + for (const partId of record.partIds ?? []) { + const partRecord = record.parts?.[partId] + const part = partRecord?.data + if (!part) continue + if (part.type === "step-finish") { + stepFinishPartIds.push(partId) + } + } + if (selectedToolPartIds.size === 0 || stepFinishPartIds.length === 0) continue + for (const partId of stepFinishPartIds) { + stepFinishPartsToDelete.push({ messageId, partId }) + } + } + } + try { for (const messageId of toDelete) { await deleteMessage(props.instanceId, props.sessionId, messageId) @@ -553,6 +536,10 @@ export default function MessageSection(props: MessageSectionProps) { if (!allowed.has(messageId)) continue await deleteMessagePart(props.instanceId, props.sessionId, messageId, partId) } + for (const { messageId, partId } of stepFinishPartsToDelete) { + if (!allowed.has(messageId)) continue + await deleteMessagePart(props.instanceId, props.sessionId, messageId, partId) + } clearDeleteMode() } catch (error) { showAlertDialog(t("messageSection.bulkDelete.failedMessage"), { @@ -1239,7 +1226,6 @@ export default function MessageSection(props: MessageSectionProps) { onClearSelection={handleClearTimelineSelection} selectedIds={selectedTimelineIds} expandedMessageIds={expandedMessageIds} - deletableMessageIds={deletableMessageIds} activeSegmentId={activeSegmentId()} instanceId={props.instanceId} sessionId={props.sessionId} From 4a9d9fc705b6e75b70227f9e0903bde3b21b90be Mon Sep 17 00:00:00 2001 From: VooDisss Date: Wed, 25 Mar 2026 10:36:49 +0200 Subject: [PATCH 2/3] fix(ui): extend delete overlays to tool-part selections and clean companion parts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When users select individual tool parts for bulk deletion (rather than entire messages), the red delete overlay now correctly highlights all related UI elements for that message — message headers, text parts, and reasoning cards — providing clear visual feedback of what will be removed. Previously, selecting tool parts for deletion only highlighted the tool call cards themselves. The message's text content and reasoning blocks appeared unaffected, which was misleading because deleting the tool parts leaves an incomplete message (an assistant response with no tool output). Changes by file: message-block.tsx: - Thread selectedToolPartKeys prop through MessageContentItem, MessageBlock, and ReasoningCard component trees. - ReasoningCard's isSelectedForDeletion now checks both selectedMessageIds (whole-message selection) and selectedToolPartKeys (tool-part selection via prefix match on messageId:). - Move the block-level selection highlight out of isDeleteMessageHovered — responsibility is now at the part level via data-delete-part-hover attributes, avoiding double overlays. - ReasoningCard's root div gains data-delete-part-hover to render the red overlay when its parent message has tool parts selected. message-item.tsx: - Accept new selectedToolPartKeys prop. - isSelectedForDeletion uses the same prefix-match pattern to detect tool-part selections. -
element gains delete-hover-scope class and data-delete-part-hover attribute for header-level overlay. - Each .message-part-shell gains data-part-type and data-delete-part-hover for per-part overlay on text content. message-section.tsx: - deleteSelectedMessages now also collects reasoning and text parts from messages that have tool parts selected for deletion, and deletes them alongside the tool parts. This prevents orphaned assistant text/reasoning from remaining after tool removal. - Guard condition simplified from selectedToolPartIds.size === 0 || stepFinishPartIds.length === 0 to selectedToolPartIds.size === 0 to avoid short-circuiting when no step-finish parts exist but reasoning/text parts do. delete-overlays.css: - Part-level overlay inset widened from -2px to -4px for consistency with the message-level overlay. - New rule for .message-reasoning-card[data-delete-part-hover] uses inset: 0 for flush overlay within reasoning card boundaries. --- packages/ui/src/components/message-block.tsx | 27 ++++++++++++++----- packages/ui/src/components/message-item.tsx | 23 +++++++++++++--- .../ui/src/components/message-section.tsx | 26 +++++++++++++++++- .../src/styles/messaging/delete-overlays.css | 8 +++++- 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index 9b518435..c5fcb467 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -209,6 +209,7 @@ interface MessageContentItemProps { showDeleteMessage?: boolean onDeleteHoverChange?: (state: DeleteHoverState) => void selectedMessageIds?: () => Set + selectedToolPartKeys?: () => Set onToggleSelectedMessage?: (messageId: string, selected: boolean) => void } @@ -299,6 +300,7 @@ function MessageContentItem(props: MessageContentItemProps) { showDeleteMessage={props.showDeleteMessage} onDeleteHoverChange={props.onDeleteHoverChange} selectedMessageIds={props.selectedMessageIds} + selectedToolPartKeys={props.selectedToolPartKeys} onToggleSelectedMessage={props.onToggleSelectedMessage} onRevert={props.onRevert} onDeleteMessagesUpTo={props.onDeleteMessagesUpTo} @@ -582,11 +584,6 @@ export default function MessageBlock(props: MessageBlockProps) { const isDeleteMessageHovered = () => { const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState) - const selected = props.selectedMessageIds?.() ?? new Set() - if (selected.has(props.messageId)) { - return true - } - if (hover.kind === "message") { return hover.messageId === props.messageId } @@ -812,6 +809,7 @@ export default function MessageBlock(props: MessageBlockProps) { onRevert={props.onRevert} onDeleteMessagesUpTo={props.onDeleteMessagesUpTo} selectedMessageIds={props.selectedMessageIds} + selectedToolPartKeys={props.selectedToolPartKeys} onToggleSelectedMessage={props.onToggleSelectedMessage} onFork={props.onFork} onContentRendered={props.onContentRendered} @@ -886,6 +884,7 @@ export default function MessageBlock(props: MessageBlockProps) { onDeleteHoverChange={props.onDeleteHoverChange} onDeleteMessagesUpTo={props.onDeleteMessagesUpTo} selectedMessageIds={props.selectedMessageIds} + selectedToolPartKeys={props.selectedToolPartKeys} onToggleSelectedMessage={props.onToggleSelectedMessage} /> @@ -902,6 +901,7 @@ export default function MessageBlock(props: MessageBlockProps) { onDeleteHoverChange={props.onDeleteHoverChange} onDeleteMessagesUpTo={props.onDeleteMessagesUpTo} selectedMessageIds={props.selectedMessageIds} + selectedToolPartKeys={props.selectedToolPartKeys} onToggleSelectedMessage={props.onToggleSelectedMessage} /> @@ -1280,6 +1280,7 @@ interface ReasoningCardProps { onDeleteHoverChange?: (state: DeleteHoverState) => void onDeleteMessagesUpTo?: (messageId: string) => void | Promise selectedMessageIds?: () => Set + selectedToolPartKeys?: () => Set onToggleSelectedMessage?: (messageId: string, selected: boolean) => void } @@ -1288,7 +1289,16 @@ function ReasoningCard(props: ReasoningCardProps) { const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded)) const [deletingMessage, setDeletingMessage] = createSignal(false) const [deletingUpTo, setDeletingUpTo] = createSignal(false) - const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId)) + const isSelectedForDeletion = () => { + if (props.selectedMessageIds?.().has(props.messageId)) return true + const toolKeys = props.selectedToolPartKeys?.() + if (!toolKeys || toolKeys.size === 0) return false + const prefix = `${props.messageId}:` + for (const key of toolKeys) { + if (key.startsWith(prefix)) return true + } + return false + } let headerEl: HTMLDivElement | undefined let actionsEl: HTMLDivElement | undefined @@ -1427,7 +1437,10 @@ function ReasoningCard(props: ReasoningCardProps) { } return ( -
+
(headerEl = el)}>