Show compaction indicator in message stream and timeline
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user