Show compaction indicator in message stream and timeline

This commit is contained in:
Shantur Rathore
2026-01-06 18:48:00 +00:00
parent 315abf21e6
commit 25bf313338
4 changed files with 128 additions and 16 deletions

View File

@@ -1,4 +1,5 @@
import { For, Match, Show, Switch, createEffect, createMemo, createSignal } from "solid-js" import { For, Match, Show, Switch, createEffect, createMemo, createSignal } from "solid-js"
import { FoldVertical } from "lucide-solid"
import MessageItem from "./message-item" import MessageItem from "./message-item"
import ToolCall from "./tool-call" import ToolCall from "./tool-call"
import type { InstanceMessageStore } from "../stores/message-v2/instance-store" import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
@@ -192,7 +193,15 @@ type ReasoningDisplayItem = {
defaultExpanded: boolean 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 { interface MessageDisplayBlock {
record: MessageRecord record: MessageRecord
@@ -330,6 +339,21 @@ export default function MessageBlock(props: MessageBlockProps) {
return 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") { if (part.type === "step-start") {
flushContent() flushContent()
return return
@@ -477,6 +501,9 @@ export default function MessageBlock(props: MessageBlockProps) {
borderColor={(item as StepDisplayItem).accentColor} borderColor={(item as StepDisplayItem).accentColor}
/> />
</Match> </Match>
<Match when={item.type === "compaction"}>
<CompactionCard part={(item as CompactionDisplayItem).part} messageInfo={(item as CompactionDisplayItem).messageInfo} borderColor={(item as CompactionDisplayItem).accentColor} />
</Match>
<Match when={item.type === "reasoning"}> <Match when={item.type === "reasoning"}>
<ReasoningCard <ReasoningCard
part={(item as ReasoningDisplayItem).part} part={(item as ReasoningDisplayItem).part}
@@ -505,6 +532,29 @@ interface StepCardProps {
borderColor?: string borderColor?: string
} }
function CompactionCard(props: { part: ClientPart; messageInfo?: MessageInfo; borderColor?: string }) {
const isAuto = () => 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 (
<div
class={containerClass()}
style={{ "border-left": `4px solid ${borderColor()}` }}
role="status"
aria-label="Session compaction"
>
<div class="message-compaction-row">
<FoldVertical class="message-compaction-icon w-4 h-4" aria-hidden="true" />
<span class="message-compaction-label">{label()}</span>
</div>
</div>
)
}
function StepCard(props: StepCardProps) { function StepCard(props: StepCardProps) {
const timestamp = () => { const timestamp = () => {
const value = props.messageInfo?.time?.created ?? (props.part as any)?.time?.start ?? Date.now() const value = props.messageInfo?.time?.created ?? (props.part as any)?.time?.start ?? Date.now()

View File

@@ -5,9 +5,9 @@ import type { ClientPart } from "../types/message"
import type { MessageRecord } from "../stores/message-v2/types" import type { MessageRecord } from "../stores/message-v2/types"
import { buildRecordDisplayData } from "../stores/message-v2/record-display-cache" import { buildRecordDisplayData } from "../stores/message-v2/record-display-cache"
import { getToolIcon } from "./tool-call/utils" 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 { export interface TimelineSegment {
id: string id: string
@@ -16,6 +16,7 @@ export interface TimelineSegment {
label: string label: string
tooltip: string tooltip: string
shortLabel?: string shortLabel?: string
variant?: "auto" | "manual"
} }
interface MessageTimelineProps { interface MessageTimelineProps {
@@ -31,6 +32,7 @@ const SEGMENT_LABELS: Record<TimelineSegmentType, string> = {
user: "You", user: "You",
assistant: "Asst", assistant: "Asst",
tool: "Tool", tool: "Tool",
compaction: "Compaction",
} }
const TOOL_FALLBACK_LABEL = "Tool Call" const TOOL_FALLBACK_LABEL = "Tool Call"
@@ -215,6 +217,21 @@ export function buildTimelineSegments(instanceId: string, record: MessageRecord)
continue 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") { if (part.type === "step-start" || part.type === "step-finish") {
continue continue
} }
@@ -347,16 +364,22 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
if (segment.type === "tool") { if (segment.type === "tool") {
return segment.shortLabel ?? getToolIcon("tool") return segment.shortLabel ?? getToolIcon("tool")
} }
if (segment.type === "compaction") {
return <FoldVertical class="message-timeline-icon" aria-hidden="true" />
}
if (segment.type === "user") { if (segment.type === "user") {
return <UserIcon class="message-timeline-icon" aria-hidden="true" /> return <UserIcon class="message-timeline-icon" aria-hidden="true" />
} }
return <BotIcon class="message-timeline-icon" aria-hidden="true" /> return <BotIcon class="message-timeline-icon" aria-hidden="true" />
} }
return ( return (
<button <button
ref={(el) => registerButtonRef(segment.id, el)} ref={(el) => registerButtonRef(segment.id, el)}
type="button" type="button"
class={`message-timeline-segment message-timeline-${segment.type} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`} data-variant={segment.variant}
class={`message-timeline-segment message-timeline-${segment.type} ${segment.type === "compaction" ? `message-timeline-compaction-${segment.variant ?? "manual"}` : ""} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`}
aria-current={isActive() ? "true" : undefined} aria-current={isActive() ? "true" : undefined}
aria-hidden={isHidden() ? "true" : undefined} aria-hidden={isHidden() ? "true" : undefined}
onClick={() => props.onSegmentClick?.(segment)} onClick={() => props.onSegmentClick?.(segment)}

View File

@@ -208,6 +208,35 @@
border-radius: 0; border-radius: 0;
} }
.message-compaction-card {
@apply flex flex-col gap-1 px-3 py-2 text-xs;
background-color: var(--message-assistant-bg);
}
.message-compaction-card--auto {
background-color: var(--session-status-compacting-bg);
color: var(--session-status-compacting-fg);
}
.message-compaction-card--manual {
background-color: var(--message-user-bg);
color: var(--text-primary);
}
.message-compaction-row {
@apply flex items-center gap-2;
justify-content: center;
}
.message-compaction-icon {
@apply inline-flex items-center;
color: inherit;
}
.message-compaction-label {
font-weight: var(--font-weight-medium);
}
.message-step-start { .message-step-start {
background-color: var(--message-assistant-bg); background-color: var(--message-assistant-bg);
border-left: 4px solid var(--message-assistant-border); border-left: 4px solid var(--message-assistant-border);

View File

@@ -146,6 +146,16 @@
background-color: var(--surface-secondary); background-color: var(--surface-secondary);
} }
.message-timeline-compaction-auto {
border-color: var(--session-status-compacting-fg);
background-color: var(--surface-secondary);
}
.message-timeline-compaction-manual {
border-color: var(--message-user-border);
background-color: var(--message-user-bg);
}
.message-timeline-segment-active { .message-timeline-segment-active {
background-color: #0f5b44 !important; background-color: #0f5b44 !important;
border-color: transparent !important; border-color: transparent !important;