diff --git a/packages/ui/src/features/sessions/components/SessionResourcesBar.stories.tsx b/packages/ui/src/features/sessions/components/SessionResourcesBar.stories.tsx new file mode 100644 index 000000000..be1da4f1e --- /dev/null +++ b/packages/ui/src/features/sessions/components/SessionResourcesBar.stories.tsx @@ -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 = { + title: "Sessions/SessionResourcesBar", + component: SessionResourcesBar, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +/** + * 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) => ( +
+ +
+ ), + ], + 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.", + }, + }, + }, +}; diff --git a/packages/ui/src/features/sessions/components/SessionResourcesBar.tsx b/packages/ui/src/features/sessions/components/SessionResourcesBar.tsx index d2f72aef5..143d872ed 100644 --- a/packages/ui/src/features/sessions/components/SessionResourcesBar.tsx +++ b/packages/ui/src/features/sessions/components/SessionResourcesBar.tsx @@ -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"; /** @@ -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 @@ -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 ( - - + + PostHog resources used - {products.map((product) => { + {visibleProducts.map((product) => { const Icon = PRODUCT_ICON[product.id] ?? SparkleIcon; const docUrl = PRODUCT_DOC_URL[product.id]; return ( @@ -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} > - - {product.label} + + {product.label} ); })} + {hasOverflow && ( + setExpanded((prev) => !prev)} + title={expanded ? "Show fewer" : "Show all resources used"} + > + {expanded ? "Show less" : `+${overflowCount} more`} + + )}