diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index ac8497f73..cad75773c 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -298,6 +298,7 @@ interface MessageContentItemProps { showDeleteMessage?: boolean onDeleteHoverChange?: (state: DeleteHoverState) => void selectedMessageIds?: () => Set + selectedToolPartKeys?: () => Set onToggleSelectedMessage?: (messageId: string, selected: boolean) => void } @@ -396,6 +397,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} @@ -756,11 +758,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 } @@ -1111,6 +1108,7 @@ export default function MessageBlock(props: MessageBlockProps) { onDeleteHoverChange={props.onDeleteHoverChange} onDeleteMessagesUpTo={props.onDeleteMessagesUpTo} selectedMessageIds={props.selectedMessageIds} + selectedToolPartKeys={props.selectedToolPartKeys} onToggleSelectedMessage={props.onToggleSelectedMessage} onContentRendered={props.onContentRendered} forceExpanded={activeSearchMatch()?.partId === (item() as ReasoningDisplayItem).partId} @@ -1302,7 +1300,7 @@ function StepCard(props: StepCardProps) { } const info = props.messageInfo const part = props.part as any - + // step-finish parts have tokens embedded; also check messageInfo const partTokens = part?.tokens const infoTokens = info && info.role === "assistant" ? info.tokens : undefined @@ -1310,7 +1308,7 @@ function StepCard(props: StepCardProps) { if (!tokens) { return null } - + return { input: tokens.input ?? 0, output: tokens.output ?? 0, @@ -1507,6 +1505,7 @@ interface ReasoningCardProps { onDeleteHoverChange?: (state: DeleteHoverState) => void onDeleteMessagesUpTo?: (messageId: string) => void | Promise selectedMessageIds?: () => Set + selectedToolPartKeys?: () => Set onToggleSelectedMessage?: (messageId: string, selected: boolean) => void onContentRendered?: () => void forceExpanded?: boolean @@ -1585,7 +1584,16 @@ function ReasoningCard(props: ReasoningCardProps) { const [deletingMessage, setDeletingMessage] = createSignal(false) const [deletingUpTo, setDeletingUpTo] = createSignal(false) const [scrollTopSnapshot, setScrollTopSnapshot] = createSignal(0) - 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 + } createEffect(() => { setExpanded(Boolean(props.defaultExpanded)) @@ -1766,6 +1774,7 @@ function ReasoningCard(props: ReasoningCardProps) { return (
diff --git a/packages/ui/src/components/message-item.tsx b/packages/ui/src/components/message-item.tsx index d0f60386c..36d738f61 100644 --- a/packages/ui/src/components/message-item.tsx +++ b/packages/ui/src/components/message-item.tsx @@ -33,6 +33,7 @@ interface MessageItemProps { parts: ClientPart[] onRevert?: (messageId: string) => void selectedMessageIds?: () => Set + selectedToolPartKeys?: () => Set onToggleSelectedMessage?: (messageId: string, selected: boolean) => void onDeleteMessagesUpTo?: (messageId: string) => void | Promise onFork?: (messageId?: string) => void @@ -99,7 +100,16 @@ export default function MessageItem(props: MessageItemProps) { }) }) - const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.record.id)) + const isSelectedForDeletion = () => { + if (props.selectedMessageIds?.().has(props.record.id)) return true + const toolKeys = props.selectedToolPartKeys?.() + if (!toolKeys || toolKeys.size === 0) return false + const prefix = `${props.record.id}:` + for (const key of toolKeys) { + if (key.startsWith(prefix)) return true + } + return false + } let topRowEl: HTMLDivElement | undefined let actionsEl: HTMLDivElement | undefined @@ -468,7 +478,10 @@ export default function MessageItem(props: MessageItemProps) { data-message-role={isUser() ? "user" : "assistant"} data-message-status={props.record.status} > -
+
(topRowEl = el)}>
(speakerPrimaryEl = el)}> diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index 8efe06106..c55a68938 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -614,6 +614,17 @@ export default function MessageSection(props: MessageSectionProps) { const allowed = deletableMessageIds() + 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[] = [] for (let idx = idsInSessionOrder.length - 1; idx >= 0; idx -= 1) { @@ -623,6 +634,37 @@ export default function MessageSection(props: MessageSectionProps) { } } + const stepFinishPartsToDelete: { messageId: string; partId: string }[] = [] + const reasoningPartsToDelete: { 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[] = [] + const reasoningPartIds: 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 (part.type === "reasoning") { + reasoningPartIds.push(partId) + } + } + if (selectedToolPartIds.size === 0) continue + for (const partId of stepFinishPartIds) { + stepFinishPartsToDelete.push({ messageId, partId }) + } + for (const partId of reasoningPartIds) { + reasoningPartsToDelete.push({ messageId, partId }) + } + } + } + try { for (const messageId of toDelete) { await deleteMessage(props.instanceId, props.sessionId, messageId) @@ -631,6 +673,14 @@ 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) + } + for (const { messageId, partId } of reasoningPartsToDelete) { + if (!allowed.has(messageId)) continue + await deleteMessagePart(props.instanceId, props.sessionId, messageId, partId) + } clearDeleteMode() } catch (error) { showAlertDialog(t("messageSection.bulkDelete.failedMessage"), { diff --git a/packages/ui/src/styles/messaging/delete-overlays.css b/packages/ui/src/styles/messaging/delete-overlays.css index 344c8a1ed..c38b485b9 100644 --- a/packages/ui/src/styles/messaging/delete-overlays.css +++ b/packages/ui/src/styles/messaging/delete-overlays.css @@ -26,10 +26,16 @@ .delete-hover-scope[data-delete-part-hover="true"]::before { content: ""; position: absolute; - inset: -2px; + inset: -4px; background: var(--status-error-bg); border-radius: 0; pointer-events: none; /* Overlay must sit above the part card background. */ z-index: 10; } + + + +.message-reasoning-card[data-delete-part-hover="true"]::before { + inset: 0; +}