Skip to content

Commit eeedb4f

Browse files
authored
fix(sessions): shrink resource chips and handle overflow (#2619)
1 parent b231abd commit eeedb4f

2 files changed

Lines changed: 182 additions & 7 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import {
2+
POSTHOG_PRODUCTS,
3+
type PostHogProductId,
4+
} from "@posthog/agent/posthog-products";
5+
import type { AcpMessage } from "@posthog/shared";
6+
import { SessionResourcesBar } from "@posthog/ui/features/sessions/components/SessionResourcesBar";
7+
import type { Meta, StoryObj } from "@storybook/react-vite";
8+
9+
const meta: Meta<typeof SessionResourcesBar> = {
10+
title: "Sessions/SessionResourcesBar",
11+
component: SessionResourcesBar,
12+
parameters: {
13+
layout: "fullscreen",
14+
},
15+
};
16+
17+
export default meta;
18+
type Story = StoryObj<typeof SessionResourcesBar>;
19+
20+
/**
21+
* Build the `_posthog/resources_used` notification the bar accumulates from,
22+
* one product per event — mirroring how the agent reports usage turn by turn.
23+
*/
24+
const resourcesUsedEvents = (ids: PostHogProductId[]): AcpMessage[] =>
25+
ids.map((id, index) => ({
26+
type: "acp_message" as const,
27+
ts: index + 1,
28+
message: {
29+
jsonrpc: "2.0" as const,
30+
method: "_posthog/resources_used",
31+
params: {
32+
sessionId: "session-1",
33+
products: [{ id, label: POSTHOG_PRODUCTS[id] }],
34+
},
35+
},
36+
}));
37+
38+
const ALL_PRODUCT_IDS = Object.keys(POSTHOG_PRODUCTS) as PostHogProductId[];
39+
40+
export const FewResources: Story = {
41+
args: {
42+
events: resourcesUsedEvents([
43+
"feature_flags",
44+
"experiments",
45+
"product_analytics",
46+
]),
47+
},
48+
parameters: {
49+
docs: {
50+
description: {
51+
story:
52+
"The common case: a handful of compact chips on a single row, each linking to the product's docs.",
53+
},
54+
},
55+
},
56+
};
57+
58+
export const AtChipLimit: Story = {
59+
args: {
60+
events: resourcesUsedEvents([
61+
"product_analytics",
62+
"web_analytics",
63+
"feature_flags",
64+
"experiments",
65+
"error_tracking",
66+
"session_replay",
67+
]),
68+
},
69+
parameters: {
70+
docs: {
71+
description: {
72+
story:
73+
"Exactly six products — the maximum shown before collapsing — so no overflow badge appears.",
74+
},
75+
},
76+
},
77+
};
78+
79+
export const OverflowCollapsed: Story = {
80+
args: {
81+
events: resourcesUsedEvents(ALL_PRODUCT_IDS),
82+
},
83+
parameters: {
84+
docs: {
85+
description: {
86+
story:
87+
"Every product used: only the first six chips render, with the rest collapsed behind a clickable “+N more” badge that toggles to “Show less”.",
88+
},
89+
},
90+
},
91+
};
92+
93+
export const WithNonClickableChip: Story = {
94+
args: {
95+
events: resourcesUsedEvents(["llm_analytics", "apm", "logs"]),
96+
},
97+
parameters: {
98+
docs: {
99+
description: {
100+
story:
101+
"APM has no dedicated docs page, so its chip renders without a pointer cursor, hover state, or link.",
102+
},
103+
},
104+
},
105+
};
106+
107+
export const NarrowContainer: Story = {
108+
args: {
109+
events: resourcesUsedEvents([
110+
"product_analytics",
111+
"data_warehouse",
112+
"error_tracking",
113+
]),
114+
},
115+
decorators: [
116+
(Story) => (
117+
<div style={{ maxWidth: 130 }}>
118+
<Story />
119+
</div>
120+
),
121+
],
122+
parameters: {
123+
docs: {
124+
description: {
125+
story:
126+
"In a narrow container, chips wrap and a label wider than the row truncates with an ellipsis instead of overflowing.",
127+
},
128+
},
129+
},
130+
};
131+
132+
export const NoResources: Story = {
133+
args: {
134+
events: [],
135+
},
136+
parameters: {
137+
docs: {
138+
description: {
139+
story: "When no products have been used yet, the bar is hidden.",
140+
},
141+
},
142+
},
143+
};

packages/ui/src/features/sessions/components/SessionResourcesBar.tsx

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import type { AcpMessage } from "@posthog/shared";
2020
import { CHAT_CONTENT_MAX_WIDTH } from "@posthog/ui/features/sessions/constants";
2121
import { openUrlInBrowser } from "@posthog/ui/utils/browser";
2222
import { Badge, Box, Flex, Text } from "@radix-ui/themes";
23-
import { type ComponentType, useMemo } from "react";
23+
import { type ComponentType, useMemo, useState } from "react";
2424
import { accumulateSessionResources } from "./accumulateSessionResources";
2525

2626
/**
@@ -70,6 +70,13 @@ interface SessionResourcesBarProps {
7070
events: AcpMessage[];
7171
}
7272

73+
/**
74+
* How many chips to show before collapsing the rest behind a "+N" badge.
75+
* Keeps the bar to a single tidy row in the common case; the user can expand
76+
* to reveal everything when the agent has touched a lot of products.
77+
*/
78+
const MAX_VISIBLE_CHIPS = 6;
79+
7380
/**
7481
* Persistent bar above the composer listing the PostHog products the agent has
7582
* touched so far this session — via the MCP `exec` tool, or by reading a file
@@ -79,17 +86,28 @@ interface SessionResourcesBarProps {
7986
*/
8087
export function SessionResourcesBar({ events }: SessionResourcesBarProps) {
8188
const products = useMemo(() => accumulateSessionResources(events), [events]);
89+
const [expanded, setExpanded] = useState(false);
8290

8391
if (products.length === 0) return null;
8492

93+
const overflowCount = products.length - MAX_VISIBLE_CHIPS;
94+
const hasOverflow = overflowCount > 0;
95+
const visibleProducts =
96+
hasOverflow && !expanded ? products.slice(0, MAX_VISIBLE_CHIPS) : products;
97+
8598
return (
8699
<Box className="mb-3">
87100
<Box className="mx-auto" style={{ maxWidth: CHAT_CONTENT_MAX_WIDTH }}>
88-
<Flex align="center" gap="2" wrap="wrap" className="px-3 pt-2">
89-
<Text color="gray" className="whitespace-nowrap text-[12px]">
101+
<Flex
102+
align="center"
103+
gap="1"
104+
wrap="wrap"
105+
className="overflow-hidden px-3 pt-2"
106+
>
107+
<Text color="gray" className="whitespace-nowrap text-[11px]">
90108
PostHog resources used
91109
</Text>
92-
{products.map((product) => {
110+
{visibleProducts.map((product) => {
93111
const Icon = PRODUCT_ICON[product.id] ?? SparkleIcon;
94112
const docUrl = PRODUCT_DOC_URL[product.id];
95113
return (
@@ -99,18 +117,32 @@ export function SessionResourcesBar({ events }: SessionResourcesBarProps) {
99117
color="gray"
100118
variant="soft"
101119
className={
102-
docUrl ? "cursor-pointer hover:bg-gray-4" : undefined
120+
docUrl
121+
? "max-w-full cursor-pointer text-[11px] hover:bg-gray-4"
122+
: "max-w-full text-[11px]"
103123
}
104124
onClick={
105125
docUrl ? () => void openUrlInBrowser(docUrl) : undefined
106126
}
107127
title={docUrl ? `Open ${product.label} docs` : undefined}
108128
>
109-
<Icon size={12} />
110-
{product.label}
129+
<Icon size={11} className="shrink-0" />
130+
<span className="truncate">{product.label}</span>
111131
</Badge>
112132
);
113133
})}
134+
{hasOverflow && (
135+
<Badge
136+
size="1"
137+
color="gray"
138+
variant="soft"
139+
className="cursor-pointer text-[11px] hover:bg-gray-4"
140+
onClick={() => setExpanded((prev) => !prev)}
141+
title={expanded ? "Show fewer" : "Show all resources used"}
142+
>
143+
{expanded ? "Show less" : `+${overflowCount} more`}
144+
</Badge>
145+
)}
114146
</Flex>
115147
</Box>
116148
</Box>

0 commit comments

Comments
 (0)