From 4cb16f2d60825b9f945e27be9779f79cddd2feb7 Mon Sep 17 00:00:00 2001 From: Matt Pua Date: Tue, 14 Jul 2026 17:45:40 -0400 Subject: [PATCH] fix: handle compaction boundaries without metadata Generated-By: PostHog Code Task-Id: 7f369b7a-64cf-4bac-8653-4061a42237c6 --- .../components/buildConversationItems.test.ts | 30 ++++++ .../components/buildConversationItems.ts | 7 +- .../CompactBoundaryView.test.ts | 53 +++++++++ .../session-update/CompactBoundaryView.tsx | 102 +++++++++++++----- .../session-update/SessionUpdateView.tsx | 8 +- packages/ui/src/features/sessions/types.ts | 10 ++ 6 files changed, 174 insertions(+), 36 deletions(-) create mode 100644 packages/ui/src/features/sessions/components/session-update/CompactBoundaryView.test.ts diff --git a/packages/ui/src/features/sessions/components/buildConversationItems.test.ts b/packages/ui/src/features/sessions/components/buildConversationItems.test.ts index 50dba8b580..69fb1d2f0b 100644 --- a/packages/ui/src/features/sessions/components/buildConversationItems.test.ts +++ b/packages/ui/src/features/sessions/components/buildConversationItems.test.ts @@ -238,6 +238,36 @@ describe("buildConversationItems", () => { expect(result.isCompacting).toBe(false); }); + it("builds a compact boundary without optional metadata", () => { + const result = buildConversationItems( + [ + userPromptMsg(1, 1, "hi"), + statusMsg(2, "compacting"), + { + type: "acp_message", + ts: 3, + message: { + jsonrpc: "2.0", + method: "_posthog/compact_boundary", + params: { sessionId: "session-1" }, + }, + }, + ], + null, + ); + + const boundary = result.items.find( + (item) => + item.type === "session_update" && + item.update.sessionUpdate === "compact_boundary", + ); + expect(boundary).toMatchObject({ + type: "session_update", + update: { sessionUpdate: "compact_boundary" }, + }); + expect(result.isCompacting).toBe(false); + }); + it("renders a failed compaction as a compacting_failed status row and clears the spinner", () => { // A failed compaction emits no compact_boundary, so the agent sends a // structured `compacting_failed` status: it clears the spinner (the original diff --git a/packages/ui/src/features/sessions/components/buildConversationItems.ts b/packages/ui/src/features/sessions/components/buildConversationItems.ts index d4386b921e..378b0544ea 100644 --- a/packages/ui/src/features/sessions/components/buildConversationItems.ts +++ b/packages/ui/src/features/sessions/components/buildConversationItems.ts @@ -20,6 +20,7 @@ import { } from "@posthog/ui/features/sessions/components/GitActionMessage"; import type { UserShellExecute } from "@posthog/ui/features/sessions/components/session-update/UserShellExecuteView"; import type { + CompactBoundaryMetadata, SessionUpdate, ToolCall, } from "@posthog/ui/features/sessions/types"; @@ -541,11 +542,7 @@ function handleNotification( if (isNotification(msg.method, POSTHOG_NOTIFICATIONS.COMPACT_BOUNDARY)) { ensureImplicitTurn(b, ts); - const params = msg.params as { - trigger: "manual" | "auto"; - preTokens: number; - contextSize?: number; - }; + const params = msg.params as CompactBoundaryMetadata; markCompactingStatusComplete(b); pushItem(b, { sessionUpdate: "compact_boundary", diff --git a/packages/ui/src/features/sessions/components/session-update/CompactBoundaryView.test.ts b/packages/ui/src/features/sessions/components/session-update/CompactBoundaryView.test.ts new file mode 100644 index 0000000000..70fbbbaf6a --- /dev/null +++ b/packages/ui/src/features/sessions/components/session-update/CompactBoundaryView.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { + formatCompactBoundaryLabel, + formatLegacyCompactBoundaryDetails, +} from "./CompactBoundaryView"; + +describe("formatCompactBoundaryLabel", () => { + it.each([ + { + name: "missing metadata", + props: {}, + expected: "Conversation compacted", + }, + { + name: "token count without context size", + props: { trigger: "auto" as const, preTokens: 12_400 }, + expected: "Conversation compacted · auto · ~12K tokens", + }, + { + name: "percentage with context size", + props: { + trigger: "manual" as const, + preTokens: 75_000, + contextSize: 100_000, + }, + expected: "Conversation compacted · manual · 75% of context", + }, + ])("formats $name", ({ props, expected }) => { + expect(formatCompactBoundaryLabel(props)).toBe(expected); + }); +}); + +describe("formatLegacyCompactBoundaryDetails", () => { + it.each([ + { + name: "missing metadata", + props: {}, + expected: null, + }, + { + name: "token count without context size", + props: { preTokens: 12_400 }, + expected: "~12K tokens summarized", + }, + { + name: "percentage and token count", + props: { preTokens: 75_000, contextSize: 100_000 }, + expected: "75% of context · ~75K tokens summarized", + }, + ])("formats $name", ({ props, expected }) => { + expect(formatLegacyCompactBoundaryDetails(props)).toBe(expected); + }); +}); diff --git a/packages/ui/src/features/sessions/components/session-update/CompactBoundaryView.tsx b/packages/ui/src/features/sessions/components/session-update/CompactBoundaryView.tsx index 6629be9134..4481d64cae 100644 --- a/packages/ui/src/features/sessions/components/session-update/CompactBoundaryView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/CompactBoundaryView.tsx @@ -1,24 +1,78 @@ import { Lightning } from "@phosphor-icons/react"; import { ChatMarker, ChatMarkerContent } from "@posthog/quill"; +import type { CompactBoundaryMetadata } from "@posthog/ui/features/sessions/types"; import { Badge, Box, Flex, Text } from "@radix-ui/themes"; import { useChatThreadChrome } from "../chat-thread/chatThreadChrome"; -interface CompactBoundaryViewProps { - trigger: "manual" | "auto"; - preTokens: number; - contextSize?: number; +interface CompactBoundaryDisplayMetadata { + trigger?: "manual" | "auto"; + tokensK?: number; + percent?: number; +} + +function getCompactBoundaryMetadata({ + trigger, + preTokens, + contextSize, +}: CompactBoundaryMetadata): CompactBoundaryDisplayMetadata { + const metadata: CompactBoundaryDisplayMetadata = { + trigger, + }; + + if (preTokens === undefined) { + return metadata; + } + + metadata.tokensK = Math.round(preTokens / 1000); + if (contextSize) { + metadata.percent = Math.round((preTokens / contextSize) * 100); + } + + return metadata; +} + +export function formatCompactBoundaryLabel( + props: CompactBoundaryMetadata, +): string { + const metadata = getCompactBoundaryMetadata(props); + const details: string[] = []; + if (metadata.trigger) { + details.push(metadata.trigger); + } + if (metadata.percent !== undefined) { + details.push(`${metadata.percent}% of context`); + } else if (metadata.tokensK !== undefined) { + details.push(`~${metadata.tokensK}K tokens`); + } + return ["Conversation compacted", ...details].join(" · "); +} + +export function formatLegacyCompactBoundaryDetails( + props: CompactBoundaryMetadata, +): string | null { + const metadata = getCompactBoundaryMetadata(props); + if (metadata.tokensK === undefined) return null; + if (metadata.percent !== undefined) { + return `${metadata.percent}% of context · ~${metadata.tokensK}K tokens summarized`; + } + return `~${metadata.tokensK}K tokens summarized`; } export function CompactBoundaryView({ trigger, preTokens, contextSize, -}: CompactBoundaryViewProps) { - const tokensK = Math.round(preTokens / 1000); - const percent = - contextSize && contextSize > 0 - ? Math.round((preTokens / contextSize) * 100) - : null; +}: CompactBoundaryMetadata) { + const metadata = getCompactBoundaryMetadata({ + trigger, + preTokens, + contextSize, + }); + const legacyDetails = formatLegacyCompactBoundaryDetails({ + trigger, + preTokens, + contextSize, + }); // New thread renders the boundary as a centered separator marker; the legacy thread keeps its // bordered badge row so ConversationView is unchanged when the chat thread is off. const chatChrome = useChatThreadChrome(); @@ -27,9 +81,7 @@ export function CompactBoundaryView({ return ( - {`Conversation compacted · ${trigger} · ${ - percent !== null ? `${percent}% of context` : `~${tokensK}K tokens` - }`} + {formatCompactBoundaryLabel({ trigger, preTokens, contextSize })} ); @@ -40,18 +92,18 @@ export function CompactBoundaryView({ Conversation compacted - - {trigger} - - - {percent !== null - ? `(${percent}% of context · ~${tokensK}K tokens summarized)` - : `(~${tokensK}K tokens summarized)`} - + {metadata.trigger && ( + + {metadata.trigger} + + )} + {legacyDetails && ( + ({legacyDetails}) + )} ); diff --git a/packages/ui/src/features/sessions/components/session-update/SessionUpdateView.tsx b/packages/ui/src/features/sessions/components/session-update/SessionUpdateView.tsx index c4236c5799..b2bf364f1a 100644 --- a/packages/ui/src/features/sessions/components/session-update/SessionUpdateView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/SessionUpdateView.tsx @@ -7,6 +7,7 @@ import { StatusNotificationView } from "@posthog/ui/features/sessions/components import { TaskNotificationView } from "@posthog/ui/features/sessions/components/session-update/TaskNotificationView"; import { ThoughtView } from "@posthog/ui/features/sessions/components/session-update/ThoughtView"; import type { + CompactBoundaryUpdate, SessionUpdate, ToolCall, } from "@posthog/ui/features/sessions/types"; @@ -23,12 +24,7 @@ export type RenderItem = message: string; timestamp?: string; } - | { - sessionUpdate: "compact_boundary"; - trigger: "manual" | "auto"; - preTokens: number; - contextSize?: number; - } + | CompactBoundaryUpdate | { sessionUpdate: "status"; status: string; diff --git a/packages/ui/src/features/sessions/types.ts b/packages/ui/src/features/sessions/types.ts index 353d1abbc3..d6d7f78520 100644 --- a/packages/ui/src/features/sessions/types.ts +++ b/packages/ui/src/features/sessions/types.ts @@ -32,3 +32,13 @@ export type ConfigOptionUpdate = Extract< SessionUpdate, { sessionUpdate: "config_option_update" } >; + +export interface CompactBoundaryMetadata { + trigger?: "manual" | "auto"; + preTokens?: number; + contextSize?: number; +} + +export interface CompactBoundaryUpdate extends CompactBoundaryMetadata { + sessionUpdate: "compact_boundary"; +}