feat(ui): add delete-up-to action and range hover overlay

This commit is contained in:
Shantur Rathore
2026-02-26 13:46:48 +00:00
parent f1bd681618
commit 2991de528a
22 changed files with 303 additions and 39 deletions

View File

@@ -22,6 +22,7 @@ interface MessageBlockListProps {
scrollContainer: Accessor<HTMLDivElement | undefined>
loading?: boolean
onRevert?: (messageId: string) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
onFork?: (messageId?: string) => void
onContentRendered?: () => void
deleteHover?: Accessor<DeleteHoverState>
@@ -57,6 +58,7 @@ export default function MessageBlockList(props: MessageBlockListProps) {
deleteHover={props.deleteHover}
onDeleteHoverChange={props.onDeleteHoverChange}
onRevert={props.onRevert}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
onFork={props.onFork}
onContentRendered={props.onContentRendered}
/>

View File

@@ -1,5 +1,5 @@
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, untrack } from "solid-js"
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, Trash2 } from "lucide-solid"
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, ListStart, Trash } from "lucide-solid"
import MessageItem from "./message-item"
import ToolCall from "./tool-call"
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
@@ -16,6 +16,14 @@ import { deleteMessage } from "../stores/session-actions"
import { useI18n } from "../lib/i18n"
import type { DeleteHoverState } from "../types/delete-hover"
function DeleteUpToIcon() {
return (
<span class="relative inline-block w-3.5 h-3.5" aria-hidden="true">
<ListStart class="absolute inset-0 w-3.5 h-3.5" aria-hidden="true" />
</span>
)
}
const TOOL_ICON = "🔧"
const USER_BORDER_COLOR = "var(--message-user-border)"
const ASSISTANT_BORDER_COLOR = "var(--message-assistant-border)"
@@ -195,6 +203,7 @@ interface MessageContentItemProps {
messageIndex: number
lastAssistantIndex: () => number
onRevert?: (messageId: string) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
onFork?: (messageId?: string) => void
onContentRendered?: () => void
showDeleteMessage?: boolean
@@ -288,6 +297,7 @@ function MessageContentItem(props: MessageContentItemProps) {
showDeleteMessage={props.showDeleteMessage}
onDeleteHoverChange={props.onDeleteHoverChange}
onRevert={props.onRevert}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
onFork={props.onFork}
onContentRendered={props.onContentRendered}
/>
@@ -305,11 +315,13 @@ interface ToolCallItemProps {
onContentRendered?: () => void
showDeleteMessage?: boolean
onDeleteHoverChange?: (state: DeleteHoverState) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
}
function ToolCallItem(props: ToolCallItemProps) {
const { t } = useI18n()
const [deletingMessage, setDeletingMessage] = createSignal(false)
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
const record = createMemo(() => props.store().getMessage(props.messageId))
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
@@ -370,6 +382,21 @@ function ToolCallItem(props: ToolCallItemProps) {
}
}
const handleDeleteUpTo = async (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
if (!props.showDeleteMessage) return
if (!props.onDeleteMessagesUpTo) return
if (deletingUpTo()) return
setDeletingUpTo(true)
try {
await props.onDeleteMessagesUpTo(props.messageId)
} finally {
setDeletingUpTo(false)
}
}
return (
<Show when={toolPart()}>
{(resolvedToolPart) => (
@@ -396,6 +423,19 @@ function ToolCallItem(props: ToolCallItemProps) {
</Show>
<Show when={props.showDeleteMessage}>
<button
class="tool-call-header-button"
type="button"
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
onClick={handleDeleteUpTo}
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId })}
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
title={t("messageItem.actions.deleteMessagesUpTo")}
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
>
<DeleteUpToIcon />
</button>
<button
class="tool-call-header-button"
type="button"
@@ -406,7 +446,7 @@ function ToolCallItem(props: ToolCallItemProps) {
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>
</div>
@@ -477,6 +517,7 @@ interface MessageBlockProps {
deleteHover?: () => DeleteHoverState
onDeleteHoverChange?: (state: DeleteHoverState) => void
onRevert?: (messageId: string) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
onFork?: (messageId?: string) => void
onContentRendered?: () => void
}
@@ -489,7 +530,21 @@ export default function MessageBlock(props: MessageBlockProps) {
const isDeleteMessageHovered = () => {
const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState)
return hover.kind === "message" && hover.messageId === props.messageId
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 block = createMemo<MessageDisplayBlock | null>(() => {
@@ -699,6 +754,7 @@ export default function MessageBlock(props: MessageBlockProps) {
showDeleteMessage={index() === 0}
onDeleteHoverChange={props.onDeleteHoverChange}
onRevert={props.onRevert}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
onFork={props.onFork}
onContentRendered={props.onContentRendered}
/>
@@ -716,6 +772,7 @@ export default function MessageBlock(props: MessageBlockProps) {
partId={toolItem.partId}
showDeleteMessage={index() === 0}
onDeleteHoverChange={props.onDeleteHoverChange}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
onContentRendered={props.onContentRendered}
/>
</div>
@@ -733,6 +790,7 @@ export default function MessageBlock(props: MessageBlockProps) {
sessionId={props.sessionId}
messageId={props.messageId}
onDeleteHoverChange={props.onDeleteHoverChange}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
/>
</Match>
<Match when={item.type === "step-finish"}>
@@ -747,6 +805,7 @@ export default function MessageBlock(props: MessageBlockProps) {
sessionId={props.sessionId}
messageId={props.messageId}
onDeleteHoverChange={props.onDeleteHoverChange}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
/>
</Match>
<Match when={item.type === "compaction"}>
@@ -759,6 +818,7 @@ export default function MessageBlock(props: MessageBlockProps) {
messageId={(item as CompactionDisplayItem).messageId}
showDeleteMessage={index() === 0}
onDeleteHoverChange={props.onDeleteHoverChange}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
/>
</Match>
<Match when={item.type === "reasoning"}>
@@ -772,6 +832,7 @@ export default function MessageBlock(props: MessageBlockProps) {
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
showDeleteMessage={index() === 0}
onDeleteHoverChange={props.onDeleteHoverChange}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
/>
</Match>
</Switch>
@@ -795,6 +856,7 @@ interface StepCardProps {
sessionId?: string
messageId?: string
onDeleteHoverChange?: (state: DeleteHoverState) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
}
interface CompactionCardProps {
@@ -806,11 +868,13 @@ interface CompactionCardProps {
messageId: string
showDeleteMessage?: boolean
onDeleteHoverChange?: (state: DeleteHoverState) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
}
function CompactionCard(props: CompactionCardProps) {
const { t } = useI18n()
const [deletingMessage, setDeletingMessage] = createSignal(false)
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
const isAuto = () => Boolean((props.part as any)?.auto)
const label = () => (isAuto() ? t("messageBlock.compaction.autoLabel") : t("messageBlock.compaction.manualLabel"))
const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR)
@@ -839,6 +903,21 @@ function CompactionCard(props: CompactionCardProps) {
}
}
const handleDeleteUpTo = async (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
if (!props.showDeleteMessage) return
if (!props.onDeleteMessagesUpTo) return
if (deletingUpTo()) return
setDeletingUpTo(true)
try {
await props.onDeleteMessagesUpTo(props.messageId)
} finally {
setDeletingUpTo(false)
}
}
return (
<div
class={`delete-hover-scope ${containerClass()} relative`}
@@ -848,6 +927,19 @@ function CompactionCard(props: CompactionCardProps) {
>
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
<Show when={props.showDeleteMessage}>
<button
type="button"
class="tool-call-header-button"
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
onClick={handleDeleteUpTo}
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId })}
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
title={t("messageItem.actions.deleteMessagesUpTo")}
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
>
<DeleteUpToIcon />
</button>
<button
type="button"
class="tool-call-header-button"
@@ -858,7 +950,7 @@ function CompactionCard(props: CompactionCardProps) {
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>
</div>
@@ -874,6 +966,7 @@ function CompactionCard(props: CompactionCardProps) {
function StepCard(props: StepCardProps) {
const { t } = useI18n()
const [deletingMessage, setDeletingMessage] = createSignal(false)
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
const timestamp = () => {
const value = props.messageInfo?.time?.created ?? (props.part as any)?.time?.start ?? Date.now()
const date = new Date(value)
@@ -939,6 +1032,21 @@ function StepCard(props: StepCardProps) {
}
}
const handleDeleteUpTo = async (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
if (!props.messageId) return
if (!props.onDeleteMessagesUpTo) return
if (deletingUpTo()) return
setDeletingUpTo(true)
try {
await props.onDeleteMessagesUpTo(props.messageId)
} finally {
setDeletingUpTo(false)
}
}
const renderUsageChips = (usage: NonNullable<ReturnType<typeof usageStats>>) => {
const entries = [
@@ -971,18 +1079,33 @@ function StepCard(props: StepCardProps) {
return (
<div class={`message-step-card message-step-finish message-step-finish-flush relative`} style={finishStyle()}>
<Show when={props.showDeleteMessage}>
<button
type="button"
class="message-action-button absolute right-2 top-1/2 -translate-y-1/2"
disabled={!canDeleteMessage()}
onClick={handleDeleteMessage}
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")}
>
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
</button>
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
<button
type="button"
class="message-action-button"
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
onClick={handleDeleteUpTo}
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId! })}
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
title={t("messageItem.actions.deleteMessagesUpTo")}
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
>
<DeleteUpToIcon />
</button>
<button
type="button"
class="message-action-button"
disabled={!canDeleteMessage()}
onClick={handleDeleteMessage}
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")}
>
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</div>
</Show>
{renderUsageChips(usage)}
@@ -1025,12 +1148,14 @@ interface ReasoningCardProps {
defaultExpanded?: boolean
showDeleteMessage?: boolean
onDeleteHoverChange?: (state: DeleteHoverState) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
}
function ReasoningCard(props: ReasoningCardProps) {
const { t } = useI18n()
const [expanded, setExpanded] = createSignal(Boolean(props.defaultExpanded))
const [deletingMessage, setDeletingMessage] = createSignal(false)
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
createEffect(() => {
setExpanded(Boolean(props.defaultExpanded))
@@ -1118,6 +1243,21 @@ function ReasoningCard(props: ReasoningCardProps) {
}
}
const handleDeleteUpTo = async (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
if (!props.showDeleteMessage) return
if (!props.onDeleteMessagesUpTo) return
if (deletingUpTo()) return
setDeletingUpTo(true)
try {
await props.onDeleteMessagesUpTo(props.messageId)
} finally {
setDeletingUpTo(false)
}
}
return (
<div class="delete-hover-scope message-reasoning-card">
<div class="message-reasoning-header">
@@ -1165,6 +1305,19 @@ function ReasoningCard(props: ReasoningCardProps) {
</button>
<Show when={props.showDeleteMessage}>
<button
type="button"
class="message-action-button"
onClick={handleDeleteUpTo}
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.messageId })}
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
title={t("messageItem.actions.deleteMessagesUpTo")}
>
<DeleteUpToIcon />
</button>
<button
type="button"
class="message-action-button"
@@ -1175,7 +1328,7 @@ function ReasoningCard(props: ReasoningCardProps) {
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>

View File

@@ -1,5 +1,5 @@
import { For, Show, createSignal } from "solid-js"
import { Copy, Split, Trash2, Undo } from "lucide-solid"
import { Copy, ListStart, Split, Trash, Undo } from "lucide-solid"
import type { MessageInfo, ClientPart, SDKAssistantMessageV2 } from "../types/message"
import { partHasRenderableText } from "../types/message"
import type { MessageRecord } from "../stores/message-v2/types"
@@ -11,6 +11,14 @@ import { deleteMessage } from "../stores/session-actions"
import { isTauriHost } from "../lib/runtime-env"
import type { DeleteHoverState } from "../types/delete-hover"
function DeleteUpToIcon() {
return (
<span class="relative inline-block w-3.5 h-3.5" aria-hidden="true">
<ListStart class="absolute inset-0 w-3.5 h-3.5" aria-hidden="true" />
</span>
)
}
interface MessageItemProps {
record: MessageRecord
messageInfo?: MessageInfo
@@ -19,6 +27,7 @@ interface MessageItemProps {
isQueued?: boolean
parts: ClientPart[]
onRevert?: (messageId: string) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
onFork?: (messageId?: string) => void
showAgentMeta?: boolean
onContentRendered?: () => void
@@ -30,6 +39,7 @@ export default function MessageItem(props: MessageItemProps) {
const { t } = useI18n()
const [copied, setCopied] = createSignal(false)
const [deletingMessage, setDeletingMessage] = createSignal(false)
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
const isUser = () => props.record.role === "user"
const createdTimestamp = () => props.messageInfo?.time?.created ?? props.record.createdAt
@@ -209,6 +219,17 @@ export default function MessageItem(props: MessageItemProps) {
}
}
const handleDeleteUpTo = async () => {
if (!props.onDeleteMessagesUpTo) return
if (deletingUpTo()) return
setDeletingUpTo(true)
try {
await props.onDeleteMessagesUpTo(props.record.id)
} finally {
setDeletingUpTo(false)
}
}
if (!isUser() && !hasContent() && !isGenerating()) {
return null
}
@@ -305,6 +326,18 @@ export default function MessageItem(props: MessageItemProps) {
</Show>
<Show when={props.showDeleteMessage}>
<button
class="message-action-button"
onClick={() => void handleDeleteUpTo()}
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.record.id })}
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
title={t("messageItem.actions.deleteMessagesUpTo")}
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
>
<DeleteUpToIcon />
</button>
<button
class="message-action-button"
onClick={handleDeleteMessage}
@@ -314,7 +347,7 @@ export default function MessageItem(props: MessageItemProps) {
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>
</div>
@@ -331,6 +364,18 @@ export default function MessageItem(props: MessageItemProps) {
</button>
<Show when={props.showDeleteMessage}>
<button
class="message-action-button"
onClick={() => void handleDeleteUpTo()}
disabled={!props.onDeleteMessagesUpTo || deletingUpTo()}
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "deleteUpTo", messageId: props.record.id })}
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
title={t("messageItem.actions.deleteMessagesUpTo")}
aria-label={t("messageItem.actions.deleteMessagesUpTo")}
>
<DeleteUpToIcon />
</button>
<button
class="message-action-button"
onClick={handleDeleteMessage}
@@ -340,7 +385,7 @@ export default function MessageItem(props: MessageItemProps) {
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
>
<Trash2 class="w-3.5 h-3.5" aria-hidden="true" />
<Trash class="w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>
</div>

View File

@@ -10,6 +10,7 @@ interface MessagePreviewProps {
store: () => InstanceMessageStore
deleteHover?: () => DeleteHoverState
onDeleteHoverChange?: (state: DeleteHoverState) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
}
const MessagePreview: Component<MessagePreviewProps> = (props) => {
@@ -29,6 +30,7 @@ const MessagePreview: Component<MessagePreviewProps> = (props) => {
showUsageMetrics={() => false}
deleteHover={props.deleteHover}
onDeleteHoverChange={props.onDeleteHoverChange}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
/>
</div>
)

View File

@@ -24,6 +24,7 @@ export interface MessageSectionProps {
sessionId: string
loading?: boolean
onRevert?: (messageId: string) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
onFork?: (messageId?: string) => void
registerScrollToBottom?: (fn: () => void) => void
showSidebarToggle?: boolean
@@ -900,6 +901,7 @@ export default function MessageSection(props: MessageSectionProps) {
scrollContainer={scrollElement}
loading={props.loading}
onRevert={props.onRevert}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
onFork={props.onFork}
onContentRendered={handleContentRendered}
deleteHover={deleteHover}
@@ -964,6 +966,7 @@ export default function MessageSection(props: MessageSectionProps) {
showToolSegments={showTimelineToolsPreference()}
deleteHover={deleteHover}
onDeleteHoverChange={setDeleteHover}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
/>
</div>
</Show>

View File

@@ -33,6 +33,7 @@ interface MessageTimelineProps {
showToolSegments?: boolean
deleteHover?: () => DeleteHoverState
onDeleteHoverChange?: (state: DeleteHoverState) => void
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
}
const MAX_TOOLTIP_LENGTH = 220
@@ -419,6 +420,16 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
if (hover.kind === "message") {
return hover.messageId === segment.messageId
}
if (hover.kind === "deleteUpTo") {
const ids = store().getSessionMessageIds(props.sessionId)
const targetIndex = ids.indexOf(hover.messageId)
if (targetIndex === -1) return false
const segmentIndex = ids.indexOf(segment.messageId)
if (segmentIndex === -1) return false
return segmentIndex >= targetIndex
}
return false
}
@@ -490,6 +501,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
store={store}
deleteHover={props.deleteHover}
onDeleteHoverChange={props.onDeleteHoverChange}
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
/>
</div>
)

View File

@@ -10,6 +10,7 @@ import { getAttachments, removeAttachment } from "../../stores/attachments"
import { instances } from "../../stores/instances"
import { loadMessages, sendMessage, forkSession, renameSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions"
import { isSessionBusy as getSessionBusyStatus } from "../../stores/session-status"
import { deleteMessage } from "../../stores/session-actions"
import { showAlertDialog } from "../../stores/alerts"
import { getLogger } from "../../lib/logger"
import { requestData } from "../../lib/opencode-api"
@@ -225,6 +226,35 @@ export const SessionView: Component<SessionViewProps> = (props) => {
}
}
async function handleDeleteMessagesUpTo(messageId: string) {
const ids = messageStore().getSessionMessageIds(props.sessionId)
const index = ids.indexOf(messageId)
if (index === -1) return
const restoredText = getUserMessageText(messageId)
const toDelete = ids.slice(index)
try {
for (let idx = toDelete.length - 1; idx >= 0; idx -= 1) {
await deleteMessage(props.instanceId, props.sessionId, toDelete[idx])
}
} catch (error) {
log.error("Failed to delete messages up to", error)
showAlertDialog(t("sessionView.alerts.deleteUpToFailed.message"), {
title: t("sessionView.alerts.deleteUpToFailed.title"),
variant: "error",
})
} finally {
if (restoredText) {
if (promptInputApi) {
promptInputApi.setPromptText(restoredText, { focus: true })
} else {
pendingPromptText = restoredText
}
}
}
}
async function handleFork(messageId?: string) {
if (!messageId) {
log.warn("Fork requires a user message id")
@@ -283,10 +313,11 @@ export const SessionView: Component<SessionViewProps> = (props) => {
<MessageSection
instanceId={props.instanceId}
sessionId={activeSession.id}
loading={messagesLoading()}
onRevert={handleRevert}
onFork={handleFork}
isActive={props.isActive}
loading={messagesLoading()}
onRevert={handleRevert}
onDeleteMessagesUpTo={handleDeleteMessagesUpTo}
onFork={handleFork}
isActive={props.isActive}
registerScrollToBottom={(fn) => {
scrollToBottomHandle = fn
if (props.isActive) {