diff --git a/packages/hooks/src/use-chat.ts b/packages/hooks/src/use-chat.ts index 5711cde5..208f06b2 100644 --- a/packages/hooks/src/use-chat.ts +++ b/packages/hooks/src/use-chat.ts @@ -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; 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; + } } }, []); diff --git a/packages/store/src/types.ts b/packages/store/src/types.ts index 40654954..d0d48340 100644 --- a/packages/store/src/types.ts +++ b/packages/store/src/types.ts @@ -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 toolStatus?: ToolStatus isError?: boolean + systemType?: "compaction" + compaction?: CompactionInfo } diff --git a/packages/ui/src/components/compaction-item.tsx b/packages/ui/src/components/compaction-item.tsx new file mode 100644 index 00000000..1425f94c --- /dev/null +++ b/packages/ui/src/components/compaction-item.tsx @@ -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 ( +
+
+ {/* Status dot */} + + + {/* Icon */} + + + {/* Label */} + {label} + + {/* Stats */} + + {removed}{tokens} + +
+
+ ) +}) diff --git a/packages/ui/src/components/message-list.tsx b/packages/ui/src/components/message-list.tsx index 3a754bc3..b2ea2fa2 100644 --- a/packages/ui/src/components/message-list.tsx +++ b/packages/ui/src/components/message-list.tsx @@ -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 (
{messages.map((msg) => { + // System messages (e.g. compaction notifications) + if (msg.role === "system") { + return + } + // ToolResult messages → render as tool execution item if (msg.role === "toolResult") { return