diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index fe58cd8d..a4900c8a 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -11,7 +11,7 @@ import { useI18n } from "../lib/i18n" import { copyToClipboard } from "../lib/clipboard" import { showToastNotification } from "../lib/notifications" import { showAlertDialog } from "../stores/alerts" -import { deleteMessage } from "../stores/session-actions" +import { deleteMessage, deleteMessagePart } from "../stores/session-actions" import type { InstanceMessageStore } from "../stores/message-v2/instance-store" import type { DeleteHoverState } from "../types/delete-hover" import { buildRecordDisplayData } from "../stores/message-v2/record-display-cache" @@ -192,7 +192,17 @@ export default function MessageSection(props: MessageSectionProps) { handleToggleTimelineSelection(segment.id) return } - const newSelected = new Set(selectedTimelineIds()) + const selected = selectedTimelineIds() + const hasAnySelected = group.some((s) => selected.has(s.id)) + if (!hasAnySelected) { + setSelectedTimelineIds((prev) => { + const next = new Set(prev) + for (const s of group) next.add(s.id) + return next + }) + return + } + const newSelected = new Set(selected) for (const s of group) newSelected.delete(s.id) setSelectedTimelineIds(newSelected) } @@ -311,12 +321,39 @@ export default function MessageSection(props: MessageSectionProps) { const [deleteHover, setDeleteHover] = createSignal({ kind: "none" }) const [selectedForDeletion, setSelectedForDeletion] = createSignal>(new Set()) - const isDeleteMode = createMemo(() => selectedForDeletion().size > 0) - const selectedDeleteCount = createMemo(() => selectedForDeletion().size) + const selectedToolParts = createMemo(() => { + const selected = selectedTimelineIds() + if (selected.size === 0) return [] as { messageId: string; partId: string }[] + const segments = timelineSegments() + const segmentById = new Map() + for (const segment of segments) segmentById.set(segment.id, segment) + const toolParts: { messageId: string; partId: string }[] = [] + const seen = new Set() + for (const segId of selected) { + const segment = segmentById.get(segId) + if (!segment || segment.type !== "tool") continue + for (const partId of segment.toolPartIds ?? []) { + if (!partId) continue + const key = `${segment.messageId}:${partId}` + if (seen.has(key)) continue + seen.add(key) + toolParts.push({ messageId: segment.messageId, partId }) + } + } + return toolParts + }) + const deleteMessageIds = createMemo(() => selectedForDeletion()) + const deleteToolParts = createMemo(() => { + const messageIds = deleteMessageIds() + return selectedToolParts().filter((entry) => !messageIds.has(entry.messageId)) + }) + const isDeleteMode = createMemo(() => deleteMessageIds().size > 0 || deleteToolParts().length > 0) + const selectedDeleteCount = createMemo(() => deleteMessageIds().size + deleteToolParts().length) const selectedTokenTotal = createMemo(() => { - const selected = selectedForDeletion() - if (selected.size === 0) return 0 + const selected = deleteMessageIds() + const toolParts = deleteToolParts() + if (selected.size === 0 && toolParts.length === 0) return 0 // Fresh-from-store chars: read parts directly via buildRecordDisplayData + // getPartCharCount so the toolbar stays consistent with the xray overlay // (which also reads live from the store). Falls back to segment totalChars @@ -339,6 +376,27 @@ export default function MessageSection(props: MessageSectionProps) { } total += Math.max(Math.round(chars / 4), 1) } + if (toolParts.length > 0) { + const partFallbackChars = new Map() + for (const segment of timelineSegments()) { + if (segment.type !== "tool") continue + for (const partId of segment.toolPartIds ?? []) { + if (!partId || partFallbackChars.has(partId)) continue + partFallbackChars.set(partId, segment.totalChars) + } + } + for (const { messageId, partId } of toolParts) { + let chars = 0 + const record = s.getMessage(messageId) + const partRecord = record?.parts?.[partId] + if (partRecord?.data) { + chars = getPartCharCount(partRecord.data) + } else { + chars = partFallbackChars.get(partId) ?? 0 + } + total += Math.max(Math.round(chars / 4), 1) + } + } return total }) @@ -377,10 +435,12 @@ export default function MessageSection(props: MessageSectionProps) { return } const segments = timelineSegments() + const segmentById = new Map() + for (const segment of segments) segmentById.set(segment.id, segment) const affectedMessageIds = new Set() for (const segId of timelineIds) { - const segment = segments.find((s) => s.id === segId) - if (segment) affectedMessageIds.add(segment.messageId) + const segment = segmentById.get(segId) + if (segment && segment.type !== "tool") affectedMessageIds.add(segment.messageId) } setSelectedForDeletion(affectedMessageIds) }) @@ -395,8 +455,9 @@ export default function MessageSection(props: MessageSectionProps) { } const deleteSelectedMessages = async () => { - const selected = selectedForDeletion() - if (selected.size === 0) return + const selected = deleteMessageIds() + const toolParts = deleteToolParts() + if (selected.size === 0 && toolParts.length === 0) return const idsInSessionOrder = messageIds() const toDelete: string[] = [] @@ -411,6 +472,9 @@ export default function MessageSection(props: MessageSectionProps) { for (const messageId of toDelete) { await deleteMessage(props.instanceId, props.sessionId, messageId) } + for (const { messageId, partId } of toolParts) { + await deleteMessagePart(props.instanceId, props.sessionId, messageId, partId) + } clearDeleteMode() } catch (error) { showAlertDialog(t("messageSection.bulkDelete.failedMessage"), { diff --git a/packages/ui/src/lib/i18n/messages/en/messaging.ts b/packages/ui/src/lib/i18n/messages/en/messaging.ts index a963c876..b7bd00be 100644 --- a/packages/ui/src/lib/i18n/messages/en/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/en/messaging.ts @@ -83,7 +83,7 @@ export const messagingMessages = { "messageItem.selection.checkboxAriaLabel": "Select message for deletion", - "messageSection.bulkDelete.toolbarAriaLabel": "Selected messages ({count})", + "messageSection.bulkDelete.toolbarAriaLabel": "Selected items ({count})", "messageSection.bulkDelete.deleteSelectedTitle": "Delete selected messages", "messageSection.bulkDelete.selectAllTitle": "Select all messages", "messageSection.bulkDelete.moreOptionsTitle": "More options", diff --git a/packages/ui/src/lib/i18n/messages/es/messaging.ts b/packages/ui/src/lib/i18n/messages/es/messaging.ts index b0af3b5b..6d6f8673 100644 --- a/packages/ui/src/lib/i18n/messages/es/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/es/messaging.ts @@ -85,7 +85,7 @@ export const messagingMessages = { "messageItem.selection.checkboxAriaLabel": "Seleccionar mensaje para eliminar", - "messageSection.bulkDelete.toolbarAriaLabel": "Mensajes seleccionados ({count})", + "messageSection.bulkDelete.toolbarAriaLabel": "Elementos seleccionados ({count})", "messageSection.bulkDelete.deleteSelectedTitle": "Eliminar mensajes seleccionados", "messageSection.bulkDelete.selectAllTitle": "Seleccionar todos los mensajes", "messageSection.bulkDelete.moreOptionsTitle": "Más opciones", diff --git a/packages/ui/src/lib/i18n/messages/fr/messaging.ts b/packages/ui/src/lib/i18n/messages/fr/messaging.ts index e7a1d2cd..3f5bacac 100644 --- a/packages/ui/src/lib/i18n/messages/fr/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/fr/messaging.ts @@ -85,7 +85,7 @@ export const messagingMessages = { "messageItem.selection.checkboxAriaLabel": "Sélectionner le message pour suppression", - "messageSection.bulkDelete.toolbarAriaLabel": "Messages sélectionnés ({count})", + "messageSection.bulkDelete.toolbarAriaLabel": "Éléments sélectionnés ({count})", "messageSection.bulkDelete.deleteSelectedTitle": "Supprimer les messages sélectionnés", "messageSection.bulkDelete.selectAllTitle": "Tout sélectionner", "messageSection.bulkDelete.moreOptionsTitle": "Plus d'options", diff --git a/packages/ui/src/lib/i18n/messages/ja/messaging.ts b/packages/ui/src/lib/i18n/messages/ja/messaging.ts index 2892a77a..cf553c2b 100644 --- a/packages/ui/src/lib/i18n/messages/ja/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ja/messaging.ts @@ -85,7 +85,7 @@ export const messagingMessages = { "messageItem.selection.checkboxAriaLabel": "削除するメッセージを選択", - "messageSection.bulkDelete.toolbarAriaLabel": "選択したメッセージ({count})", + "messageSection.bulkDelete.toolbarAriaLabel": "選択した項目({count})", "messageSection.bulkDelete.deleteSelectedTitle": "選択したメッセージを削除", "messageSection.bulkDelete.selectAllTitle": "すべて選択", "messageSection.bulkDelete.moreOptionsTitle": "その他のオプション", diff --git a/packages/ui/src/lib/i18n/messages/ru/messaging.ts b/packages/ui/src/lib/i18n/messages/ru/messaging.ts index a37ef5da..556b4adb 100644 --- a/packages/ui/src/lib/i18n/messages/ru/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ru/messaging.ts @@ -85,7 +85,7 @@ export const messagingMessages = { "messageItem.selection.checkboxAriaLabel": "Выбрать сообщение для удаления", - "messageSection.bulkDelete.toolbarAriaLabel": "Выбранные сообщения ({count})", + "messageSection.bulkDelete.toolbarAriaLabel": "Выбранные элементы ({count})", "messageSection.bulkDelete.deleteSelectedTitle": "Удалить выбранные сообщения", "messageSection.bulkDelete.selectAllTitle": "Выбрать все сообщения", "messageSection.bulkDelete.moreOptionsTitle": "Больше настроек", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts index eaadffd9..129721a6 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts @@ -85,7 +85,7 @@ export const messagingMessages = { "messageItem.selection.checkboxAriaLabel": "选择要删除的消息", - "messageSection.bulkDelete.toolbarAriaLabel": "已选择的消息({count})", + "messageSection.bulkDelete.toolbarAriaLabel": "已选择的项目({count})", "messageSection.bulkDelete.deleteSelectedTitle": "删除已选择的消息", "messageSection.bulkDelete.selectAllTitle": "全选消息", "messageSection.bulkDelete.moreOptionsTitle": "更多选项",