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";
+}