feat(ui): show in-chat system message when context compaction occurs
Add CompactionItem component that renders a subtle inline notification in the chat stream when the agent compacts its context window. Extends Message type with "system" role and handles compaction_end events in useChat's handleStream. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9c52dc0124
commit
8f4e894370
4 changed files with 95 additions and 2 deletions
|
|
@ -9,13 +9,22 @@ import {
|
|||
type AgentMessageItem,
|
||||
type ExecApprovalRequestPayload,
|
||||
type ApprovalDecision,
|
||||
type CompactionEndEvent,
|
||||
} from "@multica/sdk";
|
||||
|
||||
export type ToolStatus = "running" | "success" | "error" | "interrupted";
|
||||
|
||||
export interface CompactionInfo {
|
||||
removed: number;
|
||||
kept: number;
|
||||
tokensRemoved?: number;
|
||||
tokensKept?: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
role: "user" | "assistant" | "toolResult";
|
||||
role: "user" | "assistant" | "toolResult" | "system";
|
||||
content: ContentBlock[];
|
||||
agentId: string;
|
||||
stopReason?: string;
|
||||
|
|
@ -24,6 +33,8 @@ export interface Message {
|
|||
toolArgs?: Record<string, unknown>;
|
||||
toolStatus?: ToolStatus;
|
||||
isError?: boolean;
|
||||
systemType?: "compaction";
|
||||
compaction?: CompactionInfo;
|
||||
}
|
||||
|
||||
export interface ChatError {
|
||||
|
|
@ -215,6 +226,27 @@ export function useChat() {
|
|||
}
|
||||
case "tool_execution_update":
|
||||
break;
|
||||
case "compaction_end": {
|
||||
const ce = event as CompactionEndEvent;
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: uuidv7(),
|
||||
role: "system",
|
||||
content: [],
|
||||
agentId: payload.agentId,
|
||||
systemType: "compaction",
|
||||
compaction: {
|
||||
removed: ce.removed,
|
||||
kept: ce.kept,
|
||||
tokensRemoved: ce.tokensRemoved,
|
||||
tokensKept: ce.tokensKept,
|
||||
reason: ce.reason,
|
||||
},
|
||||
},
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,17 @@ import type { ContentBlock } from "@multica/sdk"
|
|||
|
||||
export type ToolStatus = "running" | "success" | "error" | "interrupted"
|
||||
|
||||
export interface CompactionInfo {
|
||||
removed: number
|
||||
kept: number
|
||||
tokensRemoved?: number
|
||||
tokensKept?: number
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string
|
||||
role: "user" | "assistant" | "toolResult"
|
||||
role: "user" | "assistant" | "toolResult" | "system"
|
||||
content: ContentBlock[]
|
||||
agentId: string
|
||||
stopReason?: string
|
||||
|
|
@ -13,4 +21,6 @@ export interface Message {
|
|||
toolArgs?: Record<string, unknown>
|
||||
toolStatus?: ToolStatus
|
||||
isError?: boolean
|
||||
systemType?: "compaction"
|
||||
compaction?: CompactionInfo
|
||||
}
|
||||
|
|
|
|||
45
packages/ui/src/components/compaction-item.tsx
Normal file
45
packages/ui/src/components/compaction-item.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"use client"
|
||||
|
||||
import { memo } from "react"
|
||||
import { Scissors } from "lucide-react"
|
||||
import type { Message } from "@multica/store"
|
||||
|
||||
function formatTokens(n: number): string {
|
||||
if (n >= 1000) return `~${(n / 1000).toFixed(1)}k`
|
||||
return `${n}`
|
||||
}
|
||||
|
||||
interface CompactionItemProps {
|
||||
message: Message
|
||||
}
|
||||
|
||||
export const CompactionItem = memo(function CompactionItem({ message }: CompactionItemProps) {
|
||||
const info = message.compaction
|
||||
if (!info) return null
|
||||
|
||||
const label = info.reason === "summary" ? "Context summarized" : "Context compacted"
|
||||
const removed = `${info.removed} messages removed`
|
||||
const tokens = info.tokensRemoved != null
|
||||
? `, ${formatTokens(info.tokensRemoved)} tokens freed`
|
||||
: ""
|
||||
|
||||
return (
|
||||
<div className="py-0.5 px-2.5 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1">
|
||||
{/* Status dot */}
|
||||
<span className="size-1.5 rounded-full shrink-0 bg-muted-foreground/40" />
|
||||
|
||||
{/* Icon */}
|
||||
<Scissors className="size-3.5 shrink-0" />
|
||||
|
||||
{/* Label */}
|
||||
<span className="font-medium shrink-0">{label}</span>
|
||||
|
||||
{/* Stats */}
|
||||
<span className="ml-auto text-xs text-muted-foreground/60 shrink-0">
|
||||
{removed}{tokens}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
@ -5,6 +5,7 @@ import { MemoizedMarkdown } from "@multica/ui/components/markdown";
|
|||
import { StreamingMarkdown } from "@multica/ui/components/markdown/StreamingMarkdown";
|
||||
import { ToolCallItem } from "@multica/ui/components/tool-call-item";
|
||||
import { ThinkingItem } from "@multica/ui/components/thinking-item";
|
||||
import { CompactionItem } from "@multica/ui/components/compaction-item";
|
||||
import { cn, getTextContent } from "@multica/ui/lib/utils";
|
||||
import type { Message } from "@multica/store";
|
||||
import type { ContentBlock, ToolCall, ThinkingContent } from "@multica/sdk";
|
||||
|
|
@ -78,6 +79,11 @@ export const MessageList = memo(function MessageList({ messages, streamingIds }:
|
|||
return (
|
||||
<div className="relative p-6 px-4 sm:px-10 max-w-4xl mx-auto">
|
||||
{messages.map((msg) => {
|
||||
// System messages (e.g. compaction notifications)
|
||||
if (msg.role === "system") {
|
||||
return <CompactionItem key={msg.id} message={msg} />
|
||||
}
|
||||
|
||||
// ToolResult messages → render as tool execution item
|
||||
if (msg.role === "toolResult") {
|
||||
return <ToolCallItem key={msg.id} message={msg} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue