feat(ui): mirror delete hover overlay in timeline
This commit is contained in:
@@ -2,6 +2,7 @@ import { Index, type Accessor } from "solid-js"
|
|||||||
import VirtualItem from "./virtual-item"
|
import VirtualItem from "./virtual-item"
|
||||||
import MessageBlock from "./message-block"
|
import MessageBlock from "./message-block"
|
||||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
export function getMessageAnchorId(messageId: string) {
|
export function getMessageAnchorId(messageId: string) {
|
||||||
return `message-anchor-${messageId}`
|
return `message-anchor-${messageId}`
|
||||||
@@ -23,6 +24,8 @@ interface MessageBlockListProps {
|
|||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
|
deleteHover?: Accessor<DeleteHoverState>
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
setBottomSentinel: (element: HTMLDivElement | null) => void
|
setBottomSentinel: (element: HTMLDivElement | null) => void
|
||||||
suspendMeasurements?: () => boolean
|
suspendMeasurements?: () => boolean
|
||||||
}
|
}
|
||||||
@@ -51,6 +54,8 @@ export default function MessageBlockList(props: MessageBlockListProps) {
|
|||||||
showThinking={props.showThinking}
|
showThinking={props.showThinking}
|
||||||
thinkingDefaultExpanded={props.thinkingDefaultExpanded}
|
thinkingDefaultExpanded={props.thinkingDefaultExpanded}
|
||||||
showUsageMetrics={props.showUsageMetrics}
|
showUsageMetrics={props.showUsageMetrics}
|
||||||
|
deleteHover={props.deleteHover}
|
||||||
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { showAlertDialog } from "../stores/alerts"
|
|||||||
import { deleteMessagePart } from "../stores/session-actions"
|
import { deleteMessagePart } from "../stores/session-actions"
|
||||||
import { deleteMessage } from "../stores/session-actions"
|
import { deleteMessage } from "../stores/session-actions"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
const TOOL_ICON = "🔧"
|
const TOOL_ICON = "🔧"
|
||||||
const USER_BORDER_COLOR = "var(--message-user-border)"
|
const USER_BORDER_COLOR = "var(--message-user-border)"
|
||||||
@@ -198,7 +199,7 @@ interface MessageContentItemProps {
|
|||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
showDeleteMessage?: boolean
|
showDeleteMessage?: boolean
|
||||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSupportedPartType(part: unknown): boolean {
|
function isSupportedPartType(part: unknown): boolean {
|
||||||
@@ -286,7 +287,7 @@ function MessageContentItem(props: MessageContentItemProps) {
|
|||||||
isQueued={isQueued()}
|
isQueued={isQueued()}
|
||||||
showAgentMeta={showAgentMeta()}
|
showAgentMeta={showAgentMeta()}
|
||||||
showDeleteMessage={props.showDeleteMessage}
|
showDeleteMessage={props.showDeleteMessage}
|
||||||
onDeleteMessageHoverChange={props.onDeleteMessageHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
@@ -304,7 +305,7 @@ interface ToolCallItemProps {
|
|||||||
partId: string
|
partId: string
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
showDeleteMessage?: boolean
|
showDeleteMessage?: boolean
|
||||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolCallItem(props: ToolCallItemProps) {
|
function ToolCallItem(props: ToolCallItemProps) {
|
||||||
@@ -430,8 +431,14 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={deleteDisabled()}
|
disabled={deleteDisabled()}
|
||||||
onClick={handleDeleteToolPart}
|
onClick={handleDeleteToolPart}
|
||||||
onMouseEnter={() => setHoverDeletePart(true)}
|
onMouseEnter={() => {
|
||||||
onMouseLeave={() => setHoverDeletePart(false)}
|
setHoverDeletePart(true)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "part", messageId: props.messageId, partId: props.partId, partType: "tool" })
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoverDeletePart(false)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "none" })
|
||||||
|
}}
|
||||||
title={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
title={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
||||||
aria-label={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
aria-label={deleting() ? t("messageBlock.tool.deletePart.deleting") : t("messageBlock.tool.deletePart.label")}
|
||||||
>
|
>
|
||||||
@@ -444,8 +451,8 @@ function ToolCallItem(props: ToolCallItemProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={deletingMessage()}
|
disabled={deletingMessage()}
|
||||||
onClick={handleDeleteMessage}
|
onClick={handleDeleteMessage}
|
||||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
>
|
>
|
||||||
@@ -517,6 +524,8 @@ interface MessageBlockProps {
|
|||||||
showThinking: () => boolean
|
showThinking: () => boolean
|
||||||
thinkingDefaultExpanded: () => boolean
|
thinkingDefaultExpanded: () => boolean
|
||||||
showUsageMetrics: () => boolean
|
showUsageMetrics: () => boolean
|
||||||
|
deleteHover?: () => DeleteHoverState
|
||||||
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
@@ -527,10 +536,10 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
const record = createMemo(() => props.store().getMessage(props.messageId))
|
const record = createMemo(() => props.store().getMessage(props.messageId))
|
||||||
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||||
const sessionCache = getSessionRenderCache(props.instanceId, props.sessionId)
|
const sessionCache = getSessionRenderCache(props.instanceId, props.sessionId)
|
||||||
const [deleteMessageHovered, setDeleteMessageHovered] = createSignal(false)
|
|
||||||
|
|
||||||
const handleDeleteMessageHoverChange = (hovered: boolean) => {
|
const isDeleteMessageHovered = () => {
|
||||||
setDeleteMessageHovered(hovered)
|
const hover = props.deleteHover?.() ?? ({ kind: "none" } as DeleteHoverState)
|
||||||
|
return hover.kind === "message" && hover.messageId === props.messageId
|
||||||
}
|
}
|
||||||
|
|
||||||
const block = createMemo<MessageDisplayBlock | null>(() => {
|
const block = createMemo<MessageDisplayBlock | null>(() => {
|
||||||
@@ -723,7 +732,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
<div
|
<div
|
||||||
class="message-stream-block"
|
class="message-stream-block"
|
||||||
data-message-id={resolvedBlock().record.id}
|
data-message-id={resolvedBlock().record.id}
|
||||||
data-delete-message-hover={deleteMessageHovered() ? "true" : undefined}
|
data-delete-message-hover={isDeleteMessageHovered() ? "true" : undefined}
|
||||||
>
|
>
|
||||||
<For each={resolvedBlock().items}>
|
<For each={resolvedBlock().items}>
|
||||||
{(item, index) => (
|
{(item, index) => (
|
||||||
@@ -738,7 +747,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
messageIndex={props.messageIndex}
|
messageIndex={props.messageIndex}
|
||||||
lastAssistantIndex={props.lastAssistantIndex}
|
lastAssistantIndex={props.lastAssistantIndex}
|
||||||
showDeleteMessage={index() === 0}
|
showDeleteMessage={index() === 0}
|
||||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
@@ -756,7 +765,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
messageId={toolItem.messageId}
|
messageId={toolItem.messageId}
|
||||||
partId={toolItem.partId}
|
partId={toolItem.partId}
|
||||||
showDeleteMessage={index() === 0}
|
showDeleteMessage={index() === 0}
|
||||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
onContentRendered={props.onContentRendered}
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -773,7 +782,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
messageId={props.messageId}
|
messageId={props.messageId}
|
||||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "step-finish"}>
|
<Match when={item.type === "step-finish"}>
|
||||||
@@ -787,7 +796,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
messageId={props.messageId}
|
messageId={props.messageId}
|
||||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "compaction"}>
|
<Match when={item.type === "compaction"}>
|
||||||
@@ -800,7 +809,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
messageId={(item as CompactionDisplayItem).messageId}
|
messageId={(item as CompactionDisplayItem).messageId}
|
||||||
partId={(item as CompactionDisplayItem).partId}
|
partId={(item as CompactionDisplayItem).partId}
|
||||||
showDeleteMessage={index() === 0}
|
showDeleteMessage={index() === 0}
|
||||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "reasoning"}>
|
<Match when={item.type === "reasoning"}>
|
||||||
@@ -814,7 +823,7 @@ export default function MessageBlock(props: MessageBlockProps) {
|
|||||||
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
|
showAgentMeta={(item as ReasoningDisplayItem).showAgentMeta}
|
||||||
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
|
defaultExpanded={(item as ReasoningDisplayItem).defaultExpanded}
|
||||||
showDeleteMessage={index() === 0}
|
showDeleteMessage={index() === 0}
|
||||||
onDeleteMessageHoverChange={handleDeleteMessageHoverChange}
|
onDeleteHoverChange={props.onDeleteHoverChange}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -837,7 +846,7 @@ interface StepCardProps {
|
|||||||
instanceId?: string
|
instanceId?: string
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
messageId?: string
|
messageId?: string
|
||||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CompactionCardProps {
|
interface CompactionCardProps {
|
||||||
@@ -849,7 +858,7 @@ interface CompactionCardProps {
|
|||||||
messageId: string
|
messageId: string
|
||||||
partId: string
|
partId: string
|
||||||
showDeleteMessage?: boolean
|
showDeleteMessage?: boolean
|
||||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function CompactionCard(props: CompactionCardProps) {
|
function CompactionCard(props: CompactionCardProps) {
|
||||||
@@ -920,8 +929,8 @@ function CompactionCard(props: CompactionCardProps) {
|
|||||||
class="tool-call-header-button"
|
class="tool-call-header-button"
|
||||||
disabled={!canDeleteMessage()}
|
disabled={!canDeleteMessage()}
|
||||||
onClick={handleDeleteMessage}
|
onClick={handleDeleteMessage}
|
||||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
>
|
>
|
||||||
@@ -934,8 +943,14 @@ function CompactionCard(props: CompactionCardProps) {
|
|||||||
class="tool-call-header-button"
|
class="tool-call-header-button"
|
||||||
disabled={!canDelete()}
|
disabled={!canDelete()}
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
onMouseEnter={() => setHoverDeletePart(true)}
|
onMouseEnter={() => {
|
||||||
onMouseLeave={() => setHoverDeletePart(false)}
|
setHoverDeletePart(true)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "part", messageId: props.messageId, partId: props.partId, partType: "compaction" })
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoverDeletePart(false)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "none" })
|
||||||
|
}}
|
||||||
title={t("messagePart.actions.deleteTitle")}
|
title={t("messagePart.actions.deleteTitle")}
|
||||||
aria-label={t("messagePart.actions.deleteTitle")}
|
aria-label={t("messagePart.actions.deleteTitle")}
|
||||||
>
|
>
|
||||||
@@ -1056,8 +1071,8 @@ function StepCard(props: StepCardProps) {
|
|||||||
class="message-action-button absolute right-2 top-1/2 -translate-y-1/2"
|
class="message-action-button absolute right-2 top-1/2 -translate-y-1/2"
|
||||||
disabled={!canDeleteMessage()}
|
disabled={!canDeleteMessage()}
|
||||||
onClick={handleDeleteMessage}
|
onClick={handleDeleteMessage}
|
||||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId! })}
|
||||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
>
|
>
|
||||||
@@ -1105,7 +1120,7 @@ interface ReasoningCardProps {
|
|||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
defaultExpanded?: boolean
|
defaultExpanded?: boolean
|
||||||
showDeleteMessage?: boolean
|
showDeleteMessage?: boolean
|
||||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReasoningCard(props: ReasoningCardProps) {
|
function ReasoningCard(props: ReasoningCardProps) {
|
||||||
@@ -1274,8 +1289,14 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={!canDelete()}
|
disabled={!canDelete()}
|
||||||
onMouseEnter={() => setHoverDeletePart(true)}
|
onMouseEnter={() => {
|
||||||
onMouseLeave={() => setHoverDeletePart(false)}
|
setHoverDeletePart(true)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "part", messageId: props.messageId, partId: props.partId, partType: "reasoning" })
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoverDeletePart(false)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "none" })
|
||||||
|
}}
|
||||||
aria-label={t("messagePart.actions.deleteTitle")}
|
aria-label={t("messagePart.actions.deleteTitle")}
|
||||||
title={t("messagePart.actions.deleteTitle")}
|
title={t("messagePart.actions.deleteTitle")}
|
||||||
>
|
>
|
||||||
@@ -1289,8 +1310,8 @@ function ReasoningCard(props: ReasoningCardProps) {
|
|||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
onClick={handleDeleteMessage}
|
onClick={handleDeleteMessage}
|
||||||
disabled={!canDeleteMessage()}
|
disabled={!canDeleteMessage()}
|
||||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.messageId })}
|
||||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useI18n } from "../lib/i18n"
|
|||||||
import { showAlertDialog } from "../stores/alerts"
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
import { deleteMessage, deleteMessagePart } from "../stores/session-actions"
|
import { deleteMessage, deleteMessagePart } from "../stores/session-actions"
|
||||||
import { isTauriHost } from "../lib/runtime-env"
|
import { isTauriHost } from "../lib/runtime-env"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
interface MessageItemProps {
|
interface MessageItemProps {
|
||||||
record: MessageRecord
|
record: MessageRecord
|
||||||
@@ -22,7 +23,7 @@ interface MessageItemProps {
|
|||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
showDeleteMessage?: boolean
|
showDeleteMessage?: boolean
|
||||||
onDeleteMessageHoverChange?: (hovered: boolean) => void
|
onDeleteHoverChange?: (state: DeleteHoverState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MessageItem(props: MessageItemProps) {
|
export default function MessageItem(props: MessageItemProps) {
|
||||||
@@ -354,8 +355,8 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
onClick={handleDeleteMessage}
|
onClick={handleDeleteMessage}
|
||||||
disabled={deletingMessage()}
|
disabled={deletingMessage()}
|
||||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.record.id })}
|
||||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
>
|
>
|
||||||
@@ -381,8 +382,14 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
onClick={() => void handleDeletePart(partId())}
|
onClick={() => void handleDeletePart(partId())}
|
||||||
disabled={isDeletingPart(partId())}
|
disabled={isDeletingPart(partId())}
|
||||||
onMouseEnter={() => setHoveredDeletePartId(partId())}
|
onMouseEnter={() => {
|
||||||
onMouseLeave={() => setHoveredDeletePartId(null)}
|
setHoveredDeletePartId(partId())
|
||||||
|
props.onDeleteHoverChange?.({ kind: "part", messageId: props.record.id, partId: partId(), partType: "text" })
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoveredDeletePartId(null)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "none" })
|
||||||
|
}}
|
||||||
title={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
title={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
||||||
aria-label={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
aria-label={isDeletingPart(partId()) ? t("messagePart.actions.deleting") : t("messagePart.actions.delete")}
|
||||||
>
|
>
|
||||||
@@ -396,8 +403,8 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
class="message-action-button"
|
class="message-action-button"
|
||||||
onClick={handleDeleteMessage}
|
onClick={handleDeleteMessage}
|
||||||
disabled={deletingMessage()}
|
disabled={deletingMessage()}
|
||||||
onMouseEnter={() => props.onDeleteMessageHoverChange?.(true)}
|
onMouseEnter={() => props.onDeleteHoverChange?.({ kind: "message", messageId: props.record.id })}
|
||||||
onMouseLeave={() => props.onDeleteMessageHoverChange?.(false)}
|
onMouseLeave={() => props.onDeleteHoverChange?.({ kind: "none" })}
|
||||||
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
title={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
aria-label={deletingMessage() ? t("messageItem.actions.deletingMessage") : t("messageItem.actions.deleteMessage")}
|
||||||
>
|
>
|
||||||
@@ -507,8 +514,16 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
onClick={() => void handleDeletePart(attachment.id)}
|
onClick={() => void handleDeletePart(attachment.id)}
|
||||||
class="attachment-remove"
|
class="attachment-remove"
|
||||||
disabled={isDeletingPart(attachment.id)}
|
disabled={isDeletingPart(attachment.id)}
|
||||||
onMouseEnter={() => (attachment.id ? setHoveredDeletePartId(attachment.id) : undefined)}
|
onMouseEnter={() => {
|
||||||
onMouseLeave={() => setHoveredDeletePartId(null)}
|
if (attachment.id) {
|
||||||
|
setHoveredDeletePartId(attachment.id)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "part", messageId: props.record.id, partId: attachment.id, partType: "file" })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoveredDeletePartId(null)
|
||||||
|
props.onDeleteHoverChange?.({ kind: "none" })
|
||||||
|
}}
|
||||||
aria-label={t("messagePart.actions.deleteTitle")}
|
aria-label={t("messagePart.actions.deleteTitle")}
|
||||||
title={t("messagePart.actions.deleteTitle")}
|
title={t("messagePart.actions.deleteTitle")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useI18n } from "../lib/i18n"
|
|||||||
import { copyToClipboard } from "../lib/clipboard"
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
import { showToastNotification } from "../lib/notifications"
|
import { showToastNotification } from "../lib/notifications"
|
||||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
const SCROLL_SCOPE = "session"
|
const SCROLL_SCOPE = "session"
|
||||||
const SCROLL_SENTINEL_MARGIN_PX = 48
|
const SCROLL_SENTINEL_MARGIN_PX = 48
|
||||||
@@ -145,6 +146,8 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [activeMessageId, setActiveMessageId] = createSignal<string | null>(null)
|
const [activeMessageId, setActiveMessageId] = createSignal<string | null>(null)
|
||||||
|
|
||||||
|
const [deleteHover, setDeleteHover] = createSignal<DeleteHoverState>({ kind: "none" })
|
||||||
|
|
||||||
const changeToken = createMemo(() => String(sessionRevision()))
|
const changeToken = createMemo(() => String(sessionRevision()))
|
||||||
const isActive = createMemo(() => props.isActive !== false)
|
const isActive = createMemo(() => props.isActive !== false)
|
||||||
@@ -899,6 +902,8 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
onContentRendered={handleContentRendered}
|
onContentRendered={handleContentRendered}
|
||||||
|
deleteHover={deleteHover}
|
||||||
|
onDeleteHoverChange={setDeleteHover}
|
||||||
setBottomSentinel={setBottomSentinel}
|
setBottomSentinel={setBottomSentinel}
|
||||||
suspendMeasurements={() => !isActive()}
|
suspendMeasurements={() => !isActive()}
|
||||||
/>
|
/>
|
||||||
@@ -957,6 +962,7 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
showToolSegments={showTimelineToolsPreference()}
|
showToolSegments={showTimelineToolsPreference()}
|
||||||
|
deleteHover={deleteHover}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { buildRecordDisplayData } from "../stores/message-v2/record-display-cach
|
|||||||
import { getToolIcon } from "./tool-call/utils"
|
import { getToolIcon } from "./tool-call/utils"
|
||||||
import { User as UserIcon, Bot as BotIcon, FoldVertical, ShieldAlert } from "lucide-solid"
|
import { User as UserIcon, Bot as BotIcon, FoldVertical, ShieldAlert } from "lucide-solid"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import type { DeleteHoverState } from "../types/delete-hover"
|
||||||
|
|
||||||
export type TimelineSegmentType = "user" | "assistant" | "tool" | "compaction"
|
export type TimelineSegmentType = "user" | "assistant" | "tool" | "compaction"
|
||||||
|
|
||||||
@@ -19,6 +20,8 @@ export interface TimelineSegment {
|
|||||||
shortLabel?: string
|
shortLabel?: string
|
||||||
variant?: "auto" | "manual"
|
variant?: "auto" | "manual"
|
||||||
toolPartIds?: string[]
|
toolPartIds?: string[]
|
||||||
|
partIds?: string[]
|
||||||
|
partId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageTimelineProps {
|
interface MessageTimelineProps {
|
||||||
@@ -28,6 +31,7 @@ interface MessageTimelineProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
showToolSegments?: boolean
|
showToolSegments?: boolean
|
||||||
|
deleteHover?: () => DeleteHoverState
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TOOLTIP_LENGTH = 220
|
const MAX_TOOLTIP_LENGTH = 220
|
||||||
@@ -42,6 +46,7 @@ interface PendingSegment {
|
|||||||
toolTypeLabels: string[]
|
toolTypeLabels: string[]
|
||||||
toolIcons: string[]
|
toolIcons: string[]
|
||||||
toolPartIds: string[]
|
toolPartIds: string[]
|
||||||
|
partIds: string[]
|
||||||
hasPrimaryText: boolean
|
hasPrimaryText: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +196,7 @@ export function buildTimelineSegments(
|
|||||||
tooltip,
|
tooltip,
|
||||||
shortLabel,
|
shortLabel,
|
||||||
toolPartIds: isToolSegment ? pending.toolPartIds : undefined,
|
toolPartIds: isToolSegment ? pending.toolPartIds : undefined,
|
||||||
|
partIds: !isToolSegment ? pending.partIds : undefined,
|
||||||
})
|
})
|
||||||
segmentIndex += 1
|
segmentIndex += 1
|
||||||
pending = null
|
pending = null
|
||||||
@@ -199,7 +205,17 @@ export function buildTimelineSegments(
|
|||||||
const ensureSegment = (type: TimelineSegmentType): PendingSegment => {
|
const ensureSegment = (type: TimelineSegmentType): PendingSegment => {
|
||||||
if (!pending || pending.type !== type) {
|
if (!pending || pending.type !== type) {
|
||||||
flushPending()
|
flushPending()
|
||||||
pending = { type, texts: [], reasoningTexts: [], toolTitles: [], toolTypeLabels: [], toolIcons: [], toolPartIds: [], hasPrimaryText: type !== "assistant" }
|
pending = {
|
||||||
|
type,
|
||||||
|
texts: [],
|
||||||
|
reasoningTexts: [],
|
||||||
|
toolTitles: [],
|
||||||
|
toolTypeLabels: [],
|
||||||
|
toolIcons: [],
|
||||||
|
toolPartIds: [],
|
||||||
|
partIds: [],
|
||||||
|
hasPrimaryText: type !== "assistant",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return pending!
|
return pending!
|
||||||
}
|
}
|
||||||
@@ -228,6 +244,9 @@ export function buildTimelineSegments(
|
|||||||
const target = ensureSegment(defaultContentType)
|
const target = ensureSegment(defaultContentType)
|
||||||
if (target) {
|
if (target) {
|
||||||
target.reasoningTexts.push(text)
|
target.reasoningTexts.push(text)
|
||||||
|
if (typeof (part as any).id === "string" && (part as any).id.length > 0) {
|
||||||
|
target.partIds.push((part as any).id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -235,6 +254,7 @@ export function buildTimelineSegments(
|
|||||||
if (part.type === "compaction") {
|
if (part.type === "compaction") {
|
||||||
flushPending()
|
flushPending()
|
||||||
const isAuto = Boolean((part as any)?.auto)
|
const isAuto = Boolean((part as any)?.auto)
|
||||||
|
const partId = typeof (part as any)?.id === "string" ? ((part as any).id as string) : ""
|
||||||
result.push({
|
result.push({
|
||||||
id: `${record.id}:${segmentIndex}`,
|
id: `${record.id}:${segmentIndex}`,
|
||||||
messageId: record.id,
|
messageId: record.id,
|
||||||
@@ -242,6 +262,7 @@ export function buildTimelineSegments(
|
|||||||
label: segmentLabel("compaction"),
|
label: segmentLabel("compaction"),
|
||||||
tooltip: isAuto ? t("messageTimeline.tooltip.compaction.auto") : t("messageTimeline.tooltip.compaction.manual"),
|
tooltip: isAuto ? t("messageTimeline.tooltip.compaction.auto") : t("messageTimeline.tooltip.compaction.manual"),
|
||||||
variant: isAuto ? "auto" : "manual",
|
variant: isAuto ? "auto" : "manual",
|
||||||
|
partId,
|
||||||
})
|
})
|
||||||
segmentIndex += 1
|
segmentIndex += 1
|
||||||
continue
|
continue
|
||||||
@@ -257,6 +278,9 @@ export function buildTimelineSegments(
|
|||||||
if (target) {
|
if (target) {
|
||||||
target.texts.push(text)
|
target.texts.push(text)
|
||||||
target.hasPrimaryText = true
|
target.hasPrimaryText = true
|
||||||
|
if (typeof (part as any).id === "string" && (part as any).id.length > 0) {
|
||||||
|
target.partIds.push((part as any).id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +302,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
let hoverTimer: number | null = null
|
let hoverTimer: number | null = null
|
||||||
let closeTimer: number | null = null
|
let closeTimer: number | null = null
|
||||||
const showTools = () => props.showToolSegments ?? true
|
const showTools = () => props.showToolSegments ?? true
|
||||||
|
const deleteHover = () => props.deleteHover?.() ?? { kind: "none" as const }
|
||||||
|
|
||||||
const registerButtonRef = (segmentId: string, element: HTMLButtonElement | null) => {
|
const registerButtonRef = (segmentId: string, element: HTMLButtonElement | null) => {
|
||||||
if (element) {
|
if (element) {
|
||||||
@@ -426,16 +451,34 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={(el) => registerButtonRef(segment.id, el)}
|
ref={(el) => registerButtonRef(segment.id, el)}
|
||||||
type="button"
|
type="button"
|
||||||
data-variant={segment.variant}
|
data-variant={segment.variant}
|
||||||
class={`message-timeline-segment message-timeline-${segment.type} ${hasActivePermission() ? "message-timeline-segment-permission" : ""} ${segment.type === "compaction" ? `message-timeline-compaction-${segment.variant ?? "manual"}` : ""} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`}
|
class={`message-timeline-segment message-timeline-${segment.type} ${hasActivePermission() ? "message-timeline-segment-permission" : ""} ${segment.type === "compaction" ? `message-timeline-compaction-${segment.variant ?? "manual"}` : ""} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`}
|
||||||
|
|
||||||
aria-current={isActive() ? "true" : undefined}
|
data-delete-hover={(() => {
|
||||||
aria-hidden={isHidden() ? "true" : undefined}
|
const hover = deleteHover() as DeleteHoverState
|
||||||
onClick={() => props.onSegmentClick?.(segment)}
|
if (hover.kind === "message") {
|
||||||
onMouseEnter={(event) => handleMouseEnter(segment, event)}
|
return hover.messageId === segment.messageId ? "true" : undefined
|
||||||
|
}
|
||||||
|
if (hover.kind === "part") {
|
||||||
|
if (hover.messageId !== segment.messageId) return undefined
|
||||||
|
if (segment.type === "tool") {
|
||||||
|
return segment.toolPartIds?.includes(hover.partId) ? "true" : undefined
|
||||||
|
}
|
||||||
|
if (segment.type === "compaction") {
|
||||||
|
return segment.partId === hover.partId ? "true" : undefined
|
||||||
|
}
|
||||||
|
return segment.partIds?.includes(hover.partId) ? "true" : undefined
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})()}
|
||||||
|
|
||||||
|
aria-current={isActive() ? "true" : undefined}
|
||||||
|
aria-hidden={isHidden() ? "true" : undefined}
|
||||||
|
onClick={() => props.onSegmentClick?.(segment)}
|
||||||
|
onMouseEnter={(event) => handleMouseEnter(segment, event)}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
<span class="message-timeline-label message-timeline-label-full">{segment.label}</span>
|
<span class="message-timeline-label message-timeline-label-full">{segment.label}</span>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
inset: -2px;
|
inset: -2px;
|
||||||
background: var(--status-error-bg);
|
background: var(--status-error-bg);
|
||||||
box-shadow: inset 0 0 0 1px var(--status-error-fg);
|
box-shadow: inset 0 0 0 1px var(--status-error-fg);
|
||||||
border-radius: 10px;
|
border-radius: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
/* Overlay must sit above the message cards (they have opaque backgrounds). */
|
/* Overlay must sit above the message cards (they have opaque backgrounds). */
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
inset: -2px;
|
inset: -2px;
|
||||||
background: var(--status-error-bg);
|
background: var(--status-error-bg);
|
||||||
box-shadow: inset 0 0 0 1px var(--status-error-fg);
|
box-shadow: inset 0 0 0 1px var(--status-error-fg);
|
||||||
border-radius: 10px;
|
border-radius: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
/* Overlay must sit above the part card background. */
|
/* Overlay must sit above the part card background. */
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border: 1px solid var(--border-base);
|
border: 1px solid var(--border-base);
|
||||||
background-color: var(--surface-secondary);
|
background-color: var(--surface-secondary);
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -98,6 +99,36 @@
|
|||||||
transition: transform 0.15s ease, background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.15s ease, background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-timeline-segment[data-delete-hover="true"]::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--status-error-bg);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--status-error-fg);
|
||||||
|
border-radius: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure delete hover is visible even on the active segment styling. */
|
||||||
|
.message-timeline-segment[data-delete-hover="true"],
|
||||||
|
.message-timeline-segment[data-delete-hover="true"]:hover,
|
||||||
|
.message-timeline-segment[data-delete-hover="true"]:focus-visible,
|
||||||
|
.message-timeline-segment-active[data-delete-hover="true"],
|
||||||
|
.message-timeline-segment-active[data-delete-hover="true"]:hover,
|
||||||
|
.message-timeline-segment-active[data-delete-hover="true"]:focus-visible {
|
||||||
|
/* Let the ::before overlay provide the highlight (matches stream behavior). */
|
||||||
|
background-color: transparent !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-timeline-label {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
.message-timeline-segment.message-timeline-segment-hidden {
|
.message-timeline-segment.message-timeline-segment-hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
4
packages/ui/src/types/delete-hover.ts
Normal file
4
packages/ui/src/types/delete-hover.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type DeleteHoverState =
|
||||||
|
| { kind: "none" }
|
||||||
|
| { kind: "message"; messageId: string }
|
||||||
|
| { kind: "part"; messageId: string; partId: string; partType?: string }
|
||||||
Reference in New Issue
Block a user