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
- }
- return
- }
+ const shortLabelContent = () => {
+ if (segment.type === "tool") {
+ return segment.shortLabel ?? getToolIcon("tool")
+ }
+ if (segment.type === "compaction") {
+ return
+ }
+ if (segment.type === "user") {
+ return
+ }
+ return
+ }
+
return (
-