diff --git a/packages/ui/src/components/message-block.tsx b/packages/ui/src/components/message-block.tsx index fc152de6..b42d6be0 100644 --- a/packages/ui/src/components/message-block.tsx +++ b/packages/ui/src/components/message-block.tsx @@ -1,4 +1,5 @@ import { For, Match, Show, Switch, createEffect, createMemo, createSignal } from "solid-js" +import { FoldVertical } from "lucide-solid" import MessageItem from "./message-item" import ToolCall from "./tool-call" import type { InstanceMessageStore } from "../stores/message-v2/instance-store" @@ -192,7 +193,15 @@ type ReasoningDisplayItem = { defaultExpanded: boolean } -type MessageBlockItem = ContentDisplayItem | ToolDisplayItem | StepDisplayItem | ReasoningDisplayItem +type CompactionDisplayItem = { + type: "compaction" + key: string + part: ClientPart + messageInfo?: MessageInfo + accentColor?: string +} + +type MessageBlockItem = ContentDisplayItem | ToolDisplayItem | StepDisplayItem | ReasoningDisplayItem | CompactionDisplayItem interface MessageDisplayBlock { record: MessageRecord @@ -330,6 +339,21 @@ export default function MessageBlock(props: MessageBlockProps) { return } + if (part.type === "compaction") { + flushContent() + const key = `${current.id}:${part.id ?? partIndex}:compaction` + const isAuto = Boolean((part as any)?.auto) + items.push({ + type: "compaction", + key, + part, + messageInfo: info, + accentColor: isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR, + }) + lastAccentColor = isAuto ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR + return + } + if (part.type === "step-start") { flushContent() return @@ -477,6 +501,9 @@ export default function MessageBlock(props: MessageBlockProps) { borderColor={(item as StepDisplayItem).accentColor} /> + + + Boolean((props.part as any)?.auto) + const label = () => (isAuto() ? "Session auto-compacted" : "Session compacted by you") + const borderColor = () => props.borderColor ?? (isAuto() ? "var(--session-status-compacting-fg)" : USER_BORDER_COLOR) + + const containerClass = () => + `message-compaction-card ${isAuto() ? "message-compaction-card--auto" : "message-compaction-card--manual"}` + + return ( +
+
+
+
+ ) +} + function StepCard(props: StepCardProps) { const timestamp = () => { const value = props.messageInfo?.time?.created ?? (props.part as any)?.time?.start ?? Date.now() diff --git a/packages/ui/src/components/message-timeline.tsx b/packages/ui/src/components/message-timeline.tsx index 3f63bda2..df3f28f3 100644 --- a/packages/ui/src/components/message-timeline.tsx +++ b/packages/ui/src/components/message-timeline.tsx @@ -5,9 +5,9 @@ import type { ClientPart } from "../types/message" import type { MessageRecord } from "../stores/message-v2/types" import { buildRecordDisplayData } from "../stores/message-v2/record-display-cache" import { getToolIcon } from "./tool-call/utils" -import { User as UserIcon, Bot as BotIcon } from "lucide-solid" +import { User as UserIcon, Bot as BotIcon, FoldVertical } from "lucide-solid" -export type TimelineSegmentType = "user" | "assistant" | "tool" +export type TimelineSegmentType = "user" | "assistant" | "tool" | "compaction" export interface TimelineSegment { id: string @@ -16,6 +16,7 @@ export interface TimelineSegment { label: string tooltip: string shortLabel?: string + variant?: "auto" | "manual" } interface MessageTimelineProps { @@ -31,6 +32,7 @@ const SEGMENT_LABELS: Record = { user: "You", assistant: "Asst", tool: "Tool", + compaction: "Compaction", } const TOOL_FALLBACK_LABEL = "Tool Call" @@ -215,6 +217,21 @@ export function buildTimelineSegments(instanceId: string, record: MessageRecord) continue } + if (part.type === "compaction") { + flushPending() + const isAuto = Boolean((part as any)?.auto) + result.push({ + id: `${record.id}:${segmentIndex}`, + messageId: record.id, + type: "compaction", + label: SEGMENT_LABELS.compaction, + tooltip: isAuto ? "Auto Compaction" : "User Compaction", + variant: isAuto ? "auto" : "manual", + }) + segmentIndex += 1 + continue + } + if (part.type === "step-start" || part.type === "step-finish") { continue } @@ -343,20 +360,26 @@ const MessageTimeline: Component = (props) => { onCleanup(() => buttonRefs.delete(segment.id)) const isActive = () => props.activeMessageId === segment.messageId const isHidden = () => segment.type === "tool" && !(showTools() || isActive()) - const shortLabelContent = () => { - if (segment.type === "tool") { - return segment.shortLabel ?? getToolIcon("tool") - } - if (segment.type === "user") { - return