Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
POSTHOG_PRODUCTS,
type PostHogProductId,
} from "@posthog/agent/posthog-products";
import type { AcpMessage } from "@posthog/shared";
import { SessionResourcesBar } from "@posthog/ui/features/sessions/components/SessionResourcesBar";
import type { Meta, StoryObj } from "@storybook/react-vite";

const meta: Meta<typeof SessionResourcesBar> = {
title: "Sessions/SessionResourcesBar",
component: SessionResourcesBar,
parameters: {
layout: "fullscreen",
},
};

export default meta;
type Story = StoryObj<typeof SessionResourcesBar>;

/**
* Build the `_posthog/resources_used` notification the bar accumulates from,
* one product per event — mirroring how the agent reports usage turn by turn.
*/
const resourcesUsedEvents = (ids: PostHogProductId[]): AcpMessage[] =>
ids.map((id, index) => ({
type: "acp_message" as const,
ts: index + 1,
message: {
jsonrpc: "2.0" as const,
method: "_posthog/resources_used",
params: {
sessionId: "session-1",
products: [{ id, label: POSTHOG_PRODUCTS[id] }],
},
},
}));

const ALL_PRODUCT_IDS = Object.keys(POSTHOG_PRODUCTS) as PostHogProductId[];

export const FewResources: Story = {
args: {
events: resourcesUsedEvents([
"feature_flags",
"experiments",
"product_analytics",
]),
},
parameters: {
docs: {
description: {
story:
"The common case: a handful of compact chips on a single row, each linking to the product's docs.",
},
},
},
};

export const AtChipLimit: Story = {
args: {
events: resourcesUsedEvents([
"product_analytics",
"web_analytics",
"feature_flags",
"experiments",
"error_tracking",
"session_replay",
]),
},
parameters: {
docs: {
description: {
story:
"Exactly six products — the maximum shown before collapsing — so no overflow badge appears.",
},
},
},
};

export const OverflowCollapsed: Story = {
args: {
events: resourcesUsedEvents(ALL_PRODUCT_IDS),
},
parameters: {
docs: {
description: {
story:
"Every product used: only the first six chips render, with the rest collapsed behind a clickable “+N more” badge that toggles to “Show less”.",
},
},
},
};

export const WithNonClickableChip: Story = {
args: {
events: resourcesUsedEvents(["llm_analytics", "apm", "logs"]),
},
parameters: {
docs: {
description: {
story:
"APM has no dedicated docs page, so its chip renders without a pointer cursor, hover state, or link.",
},
},
},
};

export const NarrowContainer: Story = {
args: {
events: resourcesUsedEvents([
"product_analytics",
"data_warehouse",
"error_tracking",
]),
},
decorators: [
(Story) => (
<div style={{ maxWidth: 130 }}>
<Story />
</div>
),
],
parameters: {
docs: {
description: {
story:
"In a narrow container, chips wrap and a label wider than the row truncates with an ellipsis instead of overflowing.",
},
},
},
};

export const NoResources: Story = {
args: {
events: [],
},
parameters: {
docs: {
description: {
story: "When no products have been used yet, the bar is hidden.",
},
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { AcpMessage } from "@posthog/shared";
import { CHAT_CONTENT_MAX_WIDTH } from "@posthog/ui/features/sessions/constants";
import { openUrlInBrowser } from "@posthog/ui/utils/browser";
import { Badge, Box, Flex, Text } from "@radix-ui/themes";
import { type ComponentType, useMemo } from "react";
import { type ComponentType, useMemo, useState } from "react";
import { accumulateSessionResources } from "./accumulateSessionResources";

/**
Expand Down Expand Up @@ -70,6 +70,13 @@ interface SessionResourcesBarProps {
events: AcpMessage[];
}

/**
* How many chips to show before collapsing the rest behind a "+N" badge.
* Keeps the bar to a single tidy row in the common case; the user can expand
* to reveal everything when the agent has touched a lot of products.
*/
const MAX_VISIBLE_CHIPS = 6;

/**
* Persistent bar above the composer listing the PostHog products the agent has
* touched so far this session — via the MCP `exec` tool, or by reading a file
Expand All @@ -79,17 +86,28 @@ interface SessionResourcesBarProps {
*/
export function SessionResourcesBar({ events }: SessionResourcesBarProps) {
const products = useMemo(() => accumulateSessionResources(events), [events]);
const [expanded, setExpanded] = useState(false);

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

const overflowCount = products.length - MAX_VISIBLE_CHIPS;
const hasOverflow = overflowCount > 0;
const visibleProducts =
hasOverflow && !expanded ? products.slice(0, MAX_VISIBLE_CHIPS) : products;

return (
<Box className="mb-3">
<Box className="mx-auto" style={{ maxWidth: CHAT_CONTENT_MAX_WIDTH }}>
<Flex align="center" gap="2" wrap="wrap" className="px-3 pt-2">
<Text color="gray" className="whitespace-nowrap text-[12px]">
<Flex
align="center"
gap="1"
wrap="wrap"
className="overflow-hidden px-3 pt-2"
>
<Text color="gray" className="whitespace-nowrap text-[11px]">
PostHog resources used
</Text>
{products.map((product) => {
{visibleProducts.map((product) => {
const Icon = PRODUCT_ICON[product.id] ?? SparkleIcon;
const docUrl = PRODUCT_DOC_URL[product.id];
return (
Expand All @@ -99,18 +117,32 @@ export function SessionResourcesBar({ events }: SessionResourcesBarProps) {
color="gray"
variant="soft"
className={
docUrl ? "cursor-pointer hover:bg-gray-4" : undefined
docUrl
? "max-w-full cursor-pointer text-[11px] hover:bg-gray-4"
: "max-w-full text-[11px]"
}
onClick={
docUrl ? () => void openUrlInBrowser(docUrl) : undefined
}
title={docUrl ? `Open ${product.label} docs` : undefined}
>
<Icon size={12} />
{product.label}
<Icon size={11} className="shrink-0" />
<span className="truncate">{product.label}</span>
</Badge>
);
})}
{hasOverflow && (
<Badge
size="1"
color="gray"
variant="soft"
className="cursor-pointer text-[11px] hover:bg-gray-4"
onClick={() => setExpanded((prev) => !prev)}
title={expanded ? "Show fewer" : "Show all resources used"}
>
{expanded ? "Show less" : `+${overflowCount} more`}
</Badge>
)}
</Flex>
</Box>
</Box>
Expand Down
Loading