From fb11b5d6a9e8cc8a870e3146314c9f59319483c1 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Wed, 10 Jun 2026 00:05:21 +0100 Subject: [PATCH 1/3] feat(code): inbox onboarding takeover + Agents callout --- .github/workflows/react-doctor.yml | 4 +- .../features/agents/components/AgentsView.tsx | 4 +- .../features/inbox/components/InboxView.tsx | 32 +- .../onboarding/InboxOnboardingCallout.tsx | 49 +++ .../onboarding/InboxOnboardingPane.tsx | 348 +++++++++++++++++ .../onboarding/InboxOnboardingWelcome.tsx | 357 ++++++++++++++++++ .../onboarding/useInboxOnboardingState.ts | 127 +++++++ 7 files changed, 917 insertions(+), 4 deletions(-) create mode 100644 packages/ui/src/features/inbox/components/onboarding/InboxOnboardingCallout.tsx create mode 100644 packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx create mode 100644 packages/ui/src/features/inbox/components/onboarding/InboxOnboardingWelcome.tsx create mode 100644 packages/ui/src/features/inbox/components/onboarding/useInboxOnboardingState.ts diff --git a/.github/workflows/react-doctor.yml b/.github/workflows/react-doctor.yml index 03583d4b7..c1dab2c46 100644 --- a/.github/workflows/react-doctor.yml +++ b/.github/workflows/react-doctor.yml @@ -43,6 +43,8 @@ jobs: NO_COLOR: "1" REACT_DOCTOR_BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} + # Bypass `min-release-age` from .npmrc for this CI-only pinned utility. + NPM_CONFIG_MIN_RELEASE_AGE: "0" run: | # -e omitted: exit codes are captured and inspected explicitly. set -uo pipefail @@ -59,7 +61,7 @@ jobs: fi REPORT="${RUNNER_TEMP}/react-doctor-report.json" status=0 - npx --yes react-doctor@0.4.2 . --blocking error --changed-files-from "$CHANGED" --json --json-compact --no-telemetry > "$REPORT" || status=$? + npx --yes react-doctor@0.5.1 . --blocking error --changed-files-from "$CHANGED" --json --json-compact --no-telemetry > "$REPORT" || status=$? echo "exit-code=$status" >> "$GITHUB_OUTPUT" echo "report=$REPORT" >> "$GITHUB_OUTPUT" if [ "$status" -ne 0 ]; then diff --git a/packages/ui/src/features/agents/components/AgentsView.tsx b/packages/ui/src/features/agents/components/AgentsView.tsx index 83deedf8c..a64908fd7 100644 --- a/packages/ui/src/features/agents/components/AgentsView.tsx +++ b/packages/ui/src/features/agents/components/AgentsView.tsx @@ -1,5 +1,6 @@ import { RobotIcon } from "@phosphor-icons/react"; import { ConfigureAgentsSection } from "@posthog/ui/features/inbox/components/ConfigureAgentsSection"; +import { InboxOnboardingCallout } from "@posthog/ui/features/inbox/components/onboarding/InboxOnboardingCallout"; import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent"; import { Flex, Text } from "@radix-ui/themes"; import { useMemo } from "react"; @@ -27,7 +28,7 @@ export function AgentsView() { Agents @@ -38,6 +39,7 @@ export function AgentsView() { +
diff --git a/packages/ui/src/features/inbox/components/InboxView.tsx b/packages/ui/src/features/inbox/components/InboxView.tsx index 37a6b7af3..3e89f9bb8 100644 --- a/packages/ui/src/features/inbox/components/InboxView.tsx +++ b/packages/ui/src/features/inbox/components/InboxView.tsx @@ -1,6 +1,15 @@ import { EnvelopeSimpleIcon } from "@phosphor-icons/react"; import { isInboxDetailPath } from "@posthog/core/inbox/reportMembership"; import { InboxPageHeader } from "@posthog/ui/features/inbox/components/InboxPageHeader"; +import { + InboxOnboardingHeader, + InboxOnboardingPane, +} from "@posthog/ui/features/inbox/components/onboarding/InboxOnboardingPane"; +import { InboxOnboardingWelcome } from "@posthog/ui/features/inbox/components/onboarding/InboxOnboardingWelcome"; +import { + useInboxOnboardingSessionStore, + useInboxOnboardingState, +} from "@posthog/ui/features/inbox/components/onboarding/useInboxOnboardingState"; import { useInboxAllReports } from "@posthog/ui/features/inbox/hooks/useInboxAllReports"; import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent"; import { Flex, Text } from "@radix-ui/themes"; @@ -33,12 +42,31 @@ export function InboxView() { const { counts } = useInboxAllReports(); const pathname = useRouterState({ select: (s) => s.location.pathname }); const isDetailView = isInboxDetailPath(pathname); + const onboarding = useInboxOnboardingState(); + const welcomeAcknowledged = useInboxOnboardingSessionStore( + (s) => s.welcomeAcknowledged, + ); + const showOnboarding = + !isDetailView && !onboarding.isLoading && !onboarding.isComplete; + const showWelcome = showOnboarding && !welcomeAcknowledged; return ( - {!isDetailView && } + {showOnboarding ? ( + + ) : ( + !isDetailView && + )}
- + {showOnboarding ? ( + showWelcome ? ( + + ) : ( + + ) + ) : ( + + )}
); diff --git a/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingCallout.tsx b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingCallout.tsx new file mode 100644 index 000000000..c114e55f9 --- /dev/null +++ b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingCallout.tsx @@ -0,0 +1,49 @@ +import { ArrowRightIcon } from "@phosphor-icons/react"; +import { + inboxOnboardingProgress, + useInboxOnboardingState, +} from "@posthog/ui/features/inbox/components/onboarding/useInboxOnboardingState"; +import { Flex, Text } from "@radix-ui/themes"; +import { Link } from "@tanstack/react-router"; + +/** + * Slim sticky strip shown above the Agents view's config when the inbox + * is still being onboarded. Points back to the Inbox takeover. + */ +export function InboxOnboardingCallout() { + const state = useInboxOnboardingState(); + if (state.isLoading || state.isComplete) return null; + const progress = inboxOnboardingProgress(state); + + return ( + + + + Finish setting up your inbox + + + {progress.doneCount} of {progress.totalCount} done + + + + Continue + + + + + ); +} diff --git a/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx new file mode 100644 index 000000000..71cc9dcb3 --- /dev/null +++ b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx @@ -0,0 +1,348 @@ +import { CheckIcon, SlackLogoIcon, WarningIcon } from "@phosphor-icons/react"; +import { Button } from "@posthog/quill"; +import { formatRelativeTimeLong } from "@posthog/shared"; +import { + builderHog, + explorerHog as detectiveHog, + happyHog, +} from "@posthog/ui/assets/hedgehogs"; +import mailHog from "@posthog/ui/assets/images/mail-hog.png"; +import { + type InboxOnboardingStep, + inboxOnboardingProgress, + useInboxOnboardingSessionStore, + useInboxOnboardingState, +} from "@posthog/ui/features/inbox/components/onboarding/useInboxOnboardingState"; +import { + type Integration, + useIntegrationSelectors, +} from "@posthog/ui/features/integrations/store"; +import { useSlackConnect } from "@posthog/ui/features/integrations/useSlackConnect"; +import { GitHubIntegrationSection } from "@posthog/ui/features/settings/components/sections/GitHubIntegrationSection"; +import { SignalDefaultChannelSettings } from "@posthog/ui/features/settings/components/sections/SignalDefaultChannelSettings"; +import { SignalSourcesSettings } from "@posthog/ui/features/settings/components/sections/SignalSourcesSettings"; +import { useIntegrations } from "@posthog/ui/hooks/useIntegrations"; +import { OnboardingHogTip } from "@posthog/ui/primitives/OnboardingHogTip"; +import { Flex, Spinner, Text } from "@radix-ui/themes"; + +const STEP_META: Record< + InboxOnboardingStep, + { title: string; subtitle: string } +> = { + slack: { + title: "Connect Slack", + subtitle: + "Slack is the fastest way to use your agents – kick off tasks by mentioning @PostHog, ask questions in any channel, and have inbox events land where your team already works.", + }, + github: { + title: "Connect GitHub", + subtitle: + "Research needs source to chase. Connect the GitHub org and pick the repo your agents should open pull requests against by default.", + }, + sources: { + title: "Pick signal sources", + subtitle: + "What should your agents watch? Error tracking, session replays, support tickets, GitHub issues – anything you turn on becomes input for the inbox.", + }, + notifications: { + title: "Pick a notification channel", + subtitle: + "Where should inbox events land in Slack? Pick a default channel; you can change this any time.", + }, +}; + +const STEP_LABEL: Record = { + slack: "Slack", + github: "GitHub", + sources: "Sources", + notifications: "Notifications", +}; + +const STEP_HOG: Record = { + slack: { + src: happyHog, + tip: "Slack's where I'm most useful – mention me anywhere and I'll get to work.", + }, + github: { + src: builderHog, + tip: "Show me where the code lives and I'll start opening pull requests.", + }, + sources: { + src: detectiveHog, + tip: "Tell me what to investigate, I'll dig through the rest.", + }, + notifications: { + src: mailHog, + tip: "Pick a channel and I'll start dropping reports there.", + }, +}; + +/** + * Full-screen takeover shown in place of the inbox tabs until setup is done. + * Strictly linear: each step gates the next, with Slack offering a session + * skip for genuine non-Slack users. + */ +export function InboxOnboardingPane() { + const state = useInboxOnboardingState(); + const skipSlack = useInboxOnboardingSessionStore((s) => s.skipSlack); + const { slackIntegrations } = useIntegrationSelectors(); + const slackIntegrationId = slackIntegrations[0]?.id; + + if (state.isLoading || state.currentStep === null) return null; + + const currentStep = state.currentStep; + const meta = STEP_META[currentStep]; + const hog = STEP_HOG[currentStep]; + const progress = inboxOnboardingProgress(state); + const stepNumber = progress.doneCount + 1; + + return ( +
+ + + + + + Step {stepNumber} of {progress.totalCount} · Self-driving setup + + + {meta.title} + + + {meta.subtitle} + + + +
+ {currentStep === "slack" && } + {currentStep === "github" && ( + + )} + {currentStep === "sources" && ( + + )} + {currentStep === "notifications" && ( + + )} +
+ + + + {currentStep === "slack" && ( + + )} +
+
+ ); +} + +function Stepper({ + currentStep, + state, +}: { + currentStep: InboxOnboardingStep; + state: ReturnType; +}) { + const stepDone: Record = { + slack: state.slack.done, + github: state.github.done, + sources: state.sources.done, + notifications: state.notifications.done, + }; + const visibleSteps: InboxOnboardingStep[] = ["slack", "github", "sources"]; + if (state.notifications.applicable) visibleSteps.push("notifications"); + + return ( + + {visibleSteps.map((step, idx) => { + const isCurrent = step === currentStep; + const isDone = stepDone[step]; + return ( + + {idx > 0 && ( + + )} + + + + {STEP_LABEL[step]} + + + + ); + })} + + ); +} + +function StepBadge({ + index, + isCurrent, + isDone, +}: { + index: number; + isCurrent: boolean; + isDone: boolean; +}) { + const base = + "flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[11px] font-semibold"; + if (isDone) { + return ( + + + + ); + } + if (isCurrent) { + return ( + + {index} + + ); + } + return ( + + {index} + + ); +} + +/** + * Onboarding-shaped Slack widget: just the connect handshake and the + * connected state. Notification channel choice belongs to the dedicated + * notifications step, so we deliberately don't pull in + * `SlackInboxNotificationsSettings` here. + */ +function SlackStepBody() { + const { isLoading } = useIntegrations(); + const { slackIntegrations, hasSlackIntegration } = useIntegrationSelectors(); + const { connect, isConnecting, isTimedOut, hasError, error } = + useSlackConnect(); + + if (isLoading) { + return ( + + + Loading… + + ); + } + + if (hasSlackIntegration) { + return ; + } + + return ( + + + {isTimedOut && ( + + + Didn't hear back from Slack. Try again. + + )} + {hasError && error && ( + + + {error.message} + + )} + + ); +} + +function SlackConnectedRow({ integration }: { integration: Integration }) { + const rawDisplayName = integration.display_name; + const workspaceName = + (typeof rawDisplayName === "string" && rawDisplayName.trim()) || + "Slack workspace"; + const createdAt = + typeof integration.created_at === "string" ? integration.created_at : null; + + return ( + + + + + + + Connected to {workspaceName} + + {createdAt && ( + + Linked {formatRelativeTimeLong(createdAt)} + + )} + + + ); +} + +/** + * Header rendered above the takeover pane so the Inbox view chrome still + * reads as "this is the inbox" even while it's gated. Matches the regular + * `InboxPageHeader` / Agents header shape so the surface stays unified. + */ +export function InboxOnboardingHeader() { + return ( + + + Inbox + + + A few connections, then your agents start shipping pull requests, + reports, and live runs here. + + + ); +} diff --git a/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingWelcome.tsx b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingWelcome.tsx new file mode 100644 index 000000000..36c9c78c8 --- /dev/null +++ b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingWelcome.tsx @@ -0,0 +1,357 @@ +import { ArrowRightIcon } from "@phosphor-icons/react"; +import { Button } from "@posthog/quill"; +import { explorerHog } from "@posthog/ui/assets/hedgehogs"; +import { ConventionalCommitScopeTag } from "@posthog/ui/features/inbox/components/ConventionalCommitScopeTag"; +import { useInboxOnboardingSessionStore } from "@posthog/ui/features/inbox/components/onboarding/useInboxOnboardingState"; +import { PriorityMonogram } from "@posthog/ui/features/inbox/components/PriorityMonogram"; +import { ForYouBadge } from "@posthog/ui/features/inbox/components/utils/ForYouBadge"; +import { InboxBadge } from "@posthog/ui/features/inbox/components/utils/InboxBadge"; +import { OnboardingHogTip } from "@posthog/ui/primitives/OnboardingHogTip"; +import { Flex, Text } from "@radix-ui/themes"; +import { motion } from "framer-motion"; + +/** + * Welcome scene shown once per session before the setup stepper. Sells + * self-driving as the product: agents ship pull requests, deliver them + * to your Slack, and respond when you ask them directly. + */ +export function InboxOnboardingWelcome() { + const acknowledgeWelcome = useInboxOnboardingSessionStore( + (s) => s.acknowledgeWelcome, + ); + + return ( +
+ + + + + } + delay={0.05} + /> + } + delay={0.1} + /> + } + delay={0.15} + /> + + + + + + + About two minutes · Slack, GitHub, sources, notifications + + + +
+ ); +} + +function Hero() { + return ( + + + + Welcome to PostHog Code + + + Self-driving for your product. + + + Your agents read your product data and ship pull requests against your + code. They drop the PRs into Slack so you don't have to context-switch + – and you can talk to them like a teammate. + + + + ); +} + +function Beat({ + index, + label, + description, + preview, + delay, +}: { + index: number; + label: string; + description: string; + preview: React.ReactNode; + delay: number; +}) { + return ( + + + + + 0{index} + + + + {label} + + + {description} + + + +
{preview}
+
+
+ ); +} + +// ── Preview clones ─────────────────────────────────────────────────────── +// +// Visual stand-ins for the real surfaces (PullRequestCard, Slack chrome). +// The PR card reuses production primitives directly so it reads pixel- +// close to what users will see post-setup. The Slack previews mirror +// Slack's chrome closely enough that the demo doesn't read as stylised. + +function PullRequestCardPreview() { + return ( +
+ + + + + + + Stop sending duplicate $pageview events on SPA history push + + + + 5.4% of $pageview events were duplicates — inflated funnels and + ~$1.2k/month over-billing across 2,317 customers. + + + + posthog/posthog + + + +12 + −3 + + Actionable + + + + + + + +
+ ); +} + +// ── Slack previews ─────────────────────────────────────────────────────── + +function SlackPrNotificationPreview() { + return ( + + + + I just shipped a draft PR for review: + + + + + ); +} + +function SlackMentionPreview() { + return ( + + + + + can you look into the dashboard + latency complaints? Something regressed in the last week. + + + + + On it. Pulling the last 7 days of session replays and error tracking + for the dashboard route – I'll tag you when I have something. + + + + + ); +} + +function SlackChrome({ + channel, + children, +}: { + channel: string; + children: React.ReactNode; +}) { + return ( +
+ + # + {channel} + +
{children}
+
+ ); +} + +function SlackMessage({ + authorName, + avatarSrc, + authorBadge, + timestamp, + children, +}: { + authorName: string; + avatarSrc: "alice" | "posthog"; + authorBadge?: string; + timestamp: string; + children: React.ReactNode; +}) { + return ( + + + + + + {authorName} + + {authorBadge && ( + + {authorBadge} + + )} + + {timestamp} + + +
{children}
+
+
+ ); +} + +function SlackAvatar({ variant }: { variant: "alice" | "posthog" }) { + if (variant === "posthog") { + return ( + + 🦔 + + ); + } + return ( + + A + + ); +} + +function SlackMention({ name }: { name: string }) { + return ( + + @{name} + + ); +} + +function SlackAttachment() { + return ( +
+
+ + + posthog/posthog#12345 + + + Stop sending duplicate $pageview events on SPA history push + + + + +12 + −3 + + · + + Review on GitHub + + + +
+ ); +} diff --git a/packages/ui/src/features/inbox/components/onboarding/useInboxOnboardingState.ts b/packages/ui/src/features/inbox/components/onboarding/useInboxOnboardingState.ts new file mode 100644 index 000000000..a946c201e --- /dev/null +++ b/packages/ui/src/features/inbox/components/onboarding/useInboxOnboardingState.ts @@ -0,0 +1,127 @@ +import { useSignalSourceToggles } from "@posthog/ui/features/inbox/hooks/useSignalSourceToggles"; +import { useSignalTeamConfig } from "@posthog/ui/features/inbox/hooks/useSignalTeamConfig"; +import { useIntegrationSelectors } from "@posthog/ui/features/integrations/store"; +import { useRepositoryIntegration } from "@posthog/ui/hooks/useIntegrations"; +import { create } from "zustand"; + +export type InboxOnboardingStep = + | "slack" + | "github" + | "sources" + | "notifications"; + +const STEP_ORDER: InboxOnboardingStep[] = [ + "slack", + "github", + "sources", + "notifications", +]; + +interface OnboardingSessionStore { + /** + * Slack skip is session-scoped: if the user finishes onboarding without + * Slack we just never re-show the takeover, which naturally means no nag. + * If they abandon mid-flow the skip evaporates on next open. + */ + slackSkipped: boolean; + /** + * The welcome scene appears once per session before the stepper starts. + * Acknowledging it via "Set it up" drops the user straight into the + * first incomplete step on subsequent renders. + */ + welcomeAcknowledged: boolean; + skipSlack: () => void; + acknowledgeWelcome: () => void; + reset: () => void; +} + +export const useInboxOnboardingSessionStore = create( + (set) => ({ + slackSkipped: false, + welcomeAcknowledged: false, + skipSlack: () => set({ slackSkipped: true }), + acknowledgeWelcome: () => set({ welcomeAcknowledged: true }), + reset: () => set({ slackSkipped: false, welcomeAcknowledged: false }), + }), +); + +export interface InboxOnboardingState { + slack: { done: boolean; skipped: boolean }; + github: { done: boolean }; + sources: { done: boolean }; + notifications: { done: boolean; applicable: boolean }; + currentStep: InboxOnboardingStep | null; + isComplete: boolean; + isLoading: boolean; +} + +export function useInboxOnboardingState(): InboxOnboardingState { + const { hasSlackIntegration } = useIntegrationSelectors(); + // `useRepositoryIntegration` is the same signal the Agents view uses to + // surface "Connected and active (N repos)" — gating on this keeps the + // onboarding consistent with what the user sees over there. + const { hasGithubIntegration, repositories } = useRepositoryIntegration(); + const { data: teamConfig, isLoading: teamConfigLoading } = + useSignalTeamConfig(); + const { displayValues, isLoading: sourcesLoading } = useSignalSourceToggles(); + const slackSkipped = useInboxOnboardingSessionStore((s) => s.slackSkipped); + + const slackDone = hasSlackIntegration || slackSkipped; + const githubDone = hasGithubIntegration && repositories.length > 0; + const sourcesDone = Object.values(displayValues).some(Boolean); + const notificationsApplicable = hasSlackIntegration && !slackSkipped; + const notificationsDone = + !notificationsApplicable || + !!teamConfig?.default_slack_notification_channel; + + const isLoading = teamConfigLoading || sourcesLoading; + const isComplete = + slackDone && githubDone && sourcesDone && notificationsDone; + + let currentStep: InboxOnboardingStep | null = null; + if (!isComplete) { + const stepDone: Record = { + slack: slackDone, + github: githubDone, + sources: sourcesDone, + notifications: notificationsDone, + }; + currentStep = + STEP_ORDER.find( + (step) => !stepDone[step] && stepApplies(step, slackSkipped), + ) ?? null; + } + + return { + slack: { done: slackDone, skipped: slackSkipped }, + github: { done: githubDone }, + sources: { done: sourcesDone }, + notifications: { + done: notificationsDone, + applicable: notificationsApplicable, + }, + currentStep, + isComplete, + isLoading, + }; +} + +function stepApplies( + step: InboxOnboardingStep, + slackSkipped: boolean, +): boolean { + if (step === "notifications") return !slackSkipped; + return true; +} + +export function inboxOnboardingProgress(state: InboxOnboardingState): { + doneCount: number; + totalCount: number; +} { + const steps = [state.slack.done, state.github.done, state.sources.done]; + if (state.notifications.applicable) steps.push(state.notifications.done); + return { + doneCount: steps.filter(Boolean).length, + totalCount: steps.length, + }; +} From 5ed440ccdfbbcb3169be6e12af1b22a7fea28e22 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Thu, 11 Jun 2026 15:04:10 +0100 Subject: [PATCH 2/3] feat(code): navigable inbox onboarding steps + Slack-mocked welcome Rework the inbox onboarding into a sleeker, navigable stepper: Welcome -> Connect GitHub -> Connect Slack -> Activate agents, with an explicit cursor so the user can move backward as well as forward. - Welcome is now the first step (content extracted to InboxWelcomeContent); the pane owns Back/Continue, gated on each step being satisfied. - Activate step bundles signal sources + the default Slack channel; the primary CTA becomes "Activate agents". A session latch decouples visibility from isComplete so finishing the last step doesn't yank the pane mid-flow. - Welcome previews: extract a presentational PullRequestCardView reused by the real PullRequestCard, plus a high-fidelity FakeSlack component set (real report-notification block layout, PostHog Slack-app avatar, app-scaled text, Slack-style button hover/active, meep sound effects, Silicon Valley characters). - Fix broken Radix decimal gap tokens (gap="2.5" etc.) by switching to Tailwind gap utilities. --- .../src/assets/services/posthog-slack-app.png | Bin 0 -> 5672 bytes .../components/ConfigureAgentsSection.tsx | 4 +- .../features/inbox/components/InboxView.tsx | 35 +- .../inbox/components/PullRequestCard.tsx | 152 +++----- .../inbox/components/PullRequestCardView.tsx | 152 ++++++++ .../inbox/components/onboarding/FakeSlack.tsx | 242 ++++++++++++ .../onboarding/InboxOnboardingPane.tsx | 356 ++++++++++-------- .../onboarding/InboxOnboardingWelcome.tsx | 349 +++-------------- .../onboarding/useInboxOnboardingState.ts | 151 ++++---- .../sections/GitHubIntegrationSection.tsx | 2 +- 10 files changed, 815 insertions(+), 628 deletions(-) create mode 100644 packages/ui/src/assets/services/posthog-slack-app.png create mode 100644 packages/ui/src/features/inbox/components/PullRequestCardView.tsx create mode 100644 packages/ui/src/features/inbox/components/onboarding/FakeSlack.tsx diff --git a/packages/ui/src/assets/services/posthog-slack-app.png b/packages/ui/src/assets/services/posthog-slack-app.png new file mode 100644 index 0000000000000000000000000000000000000000..3adbbc5d2879b349416af6b868bbf87dbb1bb1ff GIT binary patch literal 5672 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 literal 0 HcmV?d00001 diff --git a/packages/ui/src/features/inbox/components/ConfigureAgentsSection.tsx b/packages/ui/src/features/inbox/components/ConfigureAgentsSection.tsx index 4f1f12f40..537891602 100644 --- a/packages/ui/src/features/inbox/components/ConfigureAgentsSection.tsx +++ b/packages/ui/src/features/inbox/components/ConfigureAgentsSection.tsx @@ -91,7 +91,7 @@ export function ConfigureAgentsSection() { userAutonomyConfig?.autostart_priority ?? NEVER_AUTOSTART_VALUE; return ( - + {showSetupTask ? : null} diff --git a/packages/ui/src/features/inbox/components/InboxView.tsx b/packages/ui/src/features/inbox/components/InboxView.tsx index 3e89f9bb8..5c58c0c9b 100644 --- a/packages/ui/src/features/inbox/components/InboxView.tsx +++ b/packages/ui/src/features/inbox/components/InboxView.tsx @@ -5,7 +5,6 @@ import { InboxOnboardingHeader, InboxOnboardingPane, } from "@posthog/ui/features/inbox/components/onboarding/InboxOnboardingPane"; -import { InboxOnboardingWelcome } from "@posthog/ui/features/inbox/components/onboarding/InboxOnboardingWelcome"; import { useInboxOnboardingSessionStore, useInboxOnboardingState, @@ -14,7 +13,7 @@ import { useInboxAllReports } from "@posthog/ui/features/inbox/hooks/useInboxAll import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent"; import { Flex, Text } from "@radix-ui/themes"; import { Outlet, useRouterState } from "@tanstack/react-router"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; /** * Inbox shell. Owns the in-page header (title + RFC subtitle + tab bar) and @@ -43,12 +42,24 @@ export function InboxView() { const pathname = useRouterState({ select: (s) => s.location.pathname }); const isDetailView = isInboxDetailPath(pathname); const onboarding = useInboxOnboardingState(); - const welcomeAcknowledged = useInboxOnboardingSessionStore( - (s) => s.welcomeAcknowledged, - ); + const active = useInboxOnboardingSessionStore((s) => s.active); + const finished = useInboxOnboardingSessionStore((s) => s.finished); + const setActive = useInboxOnboardingSessionStore((s) => s.setActive); + + // Latch onboarding visibility once per session from `isComplete`. After that + // the user only leaves by finishing the Activate step, so completing the last + // step doesn't unmount the pane mid-flow. + useEffect(() => { + if (!onboarding.isLoading && active === null) { + setActive(!onboarding.isComplete); + } + }, [onboarding.isLoading, onboarding.isComplete, active, setActive]); + const showOnboarding = - !isDetailView && !onboarding.isLoading && !onboarding.isComplete; - const showWelcome = showOnboarding && !welcomeAcknowledged; + !isDetailView && + !onboarding.isLoading && + !finished && + (active === true || !onboarding.isComplete); return ( @@ -58,15 +69,7 @@ export function InboxView() { !isDetailView && )}
- {showOnboarding ? ( - showWelcome ? ( - - ) : ( - - ) - ) : ( - - )} + {showOnboarding ? : }
); diff --git a/packages/ui/src/features/inbox/components/PullRequestCard.tsx b/packages/ui/src/features/inbox/components/PullRequestCard.tsx index e0efc16b9..f1c8e1b90 100644 --- a/packages/ui/src/features/inbox/components/PullRequestCard.tsx +++ b/packages/ui/src/features/inbox/components/PullRequestCard.tsx @@ -1,4 +1,3 @@ -import { ThumbsDownIcon } from "@phosphor-icons/react"; import { extractRepoSelectionRepository } from "@posthog/core/inbox/artefacts"; import { deriveHeadline, @@ -6,18 +5,12 @@ import { parseConventionalCommitTitle, parsePrUrl, } from "@posthog/core/inbox/reportPresentation"; -import { Button, cn } from "@posthog/quill"; import type { SignalReport } from "@posthog/shared/types"; -import { ConventionalCommitScopeTag } from "@posthog/ui/features/inbox/components/ConventionalCommitScopeTag"; -import { InboxCardSourceMeta } from "@posthog/ui/features/inbox/components/InboxCardSourceMeta"; -import { InboxCardTitle } from "@posthog/ui/features/inbox/components/InboxCardTitle"; import { PrDiffStats } from "@posthog/ui/features/inbox/components/PrDiffStats"; -import { PriorityMonogram } from "@posthog/ui/features/inbox/components/PriorityMonogram"; +import { PullRequestCardView } from "@posthog/ui/features/inbox/components/PullRequestCardView"; import { SuggestedReviewerAvatarStack } from "@posthog/ui/features/inbox/components/SuggestedReviewerAvatarStack"; import { useInboxReportDetailPrefetch } from "@posthog/ui/features/inbox/hooks/useInboxReportDetailPrefetch"; import { useInboxReportArtefacts } from "@posthog/ui/features/inbox/hooks/useInboxReports"; -import { Button as UiButton } from "@posthog/ui/primitives/Button"; -import { Flex, Text } from "@radix-ui/themes"; import { Link, useNavigate } from "@tanstack/react-router"; import type { MouseEvent } from "react"; @@ -64,105 +57,50 @@ export function PullRequestCard({ ); return ( -
+ ) : null + } + reviewersSlot={ + + } + onReview={() => { + prefetch(); + navigate(detailRoute); + }} + onDismiss={onDismiss} + dismissDisabledReason={dismissDisabledReason} + isDismissPending={isDismissPending} + renderSummary={(summary, className) => ( + { + onRowClick?.(event); + if (event.metaKey || event.ctrlKey || event.shiftKey) { + event.preventDefault(); + return; + } + prefetch(); + }} + className={className} + > + {summary} + )} - {...pointerHandlers} - > - { - onRowClick?.(event); - if (event.metaKey || event.ctrlKey || event.shiftKey) { - event.preventDefault(); - return; - } - prefetch(); - }} - className="flex min-w-0 flex-1 items-start gap-3 text-left text-inherit no-underline focus-visible:outline-none" - > - - - - - {conventionalTitle && ( - - )} - {cardTitle} - - - {(() => { - const headline = deriveHeadline(report.summary); - return headline ? ( - - {headline} - - ) : null; - })()} - - - - - - - - {report.implementation_pr_url && ( - - )} - - - - { - event.stopPropagation(); - onDismiss(); - }} - > - - - - - -
+ /> ); } diff --git a/packages/ui/src/features/inbox/components/PullRequestCardView.tsx b/packages/ui/src/features/inbox/components/PullRequestCardView.tsx new file mode 100644 index 000000000..81432e9d6 --- /dev/null +++ b/packages/ui/src/features/inbox/components/PullRequestCardView.tsx @@ -0,0 +1,152 @@ +import { ThumbsDownIcon } from "@phosphor-icons/react"; +import { Button, cn } from "@posthog/quill"; +import type { SignalReportPriority } from "@posthog/shared/types"; +import { ConventionalCommitScopeTag } from "@posthog/ui/features/inbox/components/ConventionalCommitScopeTag"; +import { InboxCardSourceMeta } from "@posthog/ui/features/inbox/components/InboxCardSourceMeta"; +import { InboxCardTitle } from "@posthog/ui/features/inbox/components/InboxCardTitle"; +import { PriorityMonogram } from "@posthog/ui/features/inbox/components/PriorityMonogram"; +import { Button as UiButton } from "@posthog/ui/primitives/Button"; +import { Flex, Text } from "@radix-ui/themes"; +import type { MouseEvent, ReactNode } from "react"; + +/** Layout class for the clickable left region; shared so the real card's `` matches. */ +export const PULL_REQUEST_CARD_ROW_CLASS = + "flex min-w-0 flex-1 items-start gap-3 text-left text-inherit no-underline focus-visible:outline-none"; + +interface PullRequestCardViewProps { + priority: SignalReportPriority | null | undefined; + conventionalTitle: { type: string; scope: string | null } | null; + title: string; + headline?: string | null; + repoSlug?: string | null; + sourceProducts?: string[] | null; + isSelected?: boolean; + /** Diff adornment (`` in the real card, a static `` in previews). */ + diffSlot?: ReactNode; + /** Suggested-reviewer avatars; omitted in previews. */ + reviewersSlot?: ReactNode; + reviewLabel?: string; + onReview?: (event: MouseEvent) => void; + /** Omit to hide the dismiss affordance entirely (e.g. previews). */ + onDismiss?: () => void; + dismissDisabledReason?: string | null; + isDismissPending?: boolean; + pointerHandlers?: { onPointerDown?: () => void }; + /** + * Wraps the summary region. The real card passes a TanStack `` so the row + * navigates and preloads; previews leave it undefined to render a static `
`. + */ + renderSummary?: (summary: ReactNode, className: string) => ReactNode; +} + +/** + * Pure, presentational pull-request card. Holds no router or query dependencies so it + * can render identically in the live inbox list and in mocked onboarding previews. + * `PullRequestCard` is the data-resolving wrapper around this view. + */ +export function PullRequestCardView({ + priority, + conventionalTitle, + title, + headline = null, + repoSlug = null, + sourceProducts = null, + isSelected = false, + diffSlot, + reviewersSlot, + reviewLabel = "Review", + onReview, + onDismiss, + dismissDisabledReason = null, + isDismissPending = false, + pointerHandlers, + renderSummary, +}: PullRequestCardViewProps) { + const summary = ( + <> + + + + {conventionalTitle && ( + + )} + {title} + + + {headline ? ( + + {headline} + + ) : null} + + + + + ); + + return ( +
+ {renderSummary ? ( + renderSummary(summary, PULL_REQUEST_CARD_ROW_CLASS) + ) : ( +
{summary}
+ )} + + + + {diffSlot} + {reviewersSlot} + + + {onDismiss && ( + { + event.stopPropagation(); + onDismiss(); + }} + > + + + )} + + + +
+ ); +} diff --git a/packages/ui/src/features/inbox/components/onboarding/FakeSlack.tsx b/packages/ui/src/features/inbox/components/onboarding/FakeSlack.tsx new file mode 100644 index 000000000..4e76dfab5 --- /dev/null +++ b/packages/ui/src/features/inbox/components/onboarding/FakeSlack.tsx @@ -0,0 +1,242 @@ +import { cn } from "@posthog/quill"; +import slackAppLogo from "@posthog/ui/assets/services/posthog-slack-app.png"; +import { playCompletionSound } from "@posthog/ui/utils/sounds"; +import { Flex, Text } from "@radix-ui/themes"; +import type { ReactNode } from "react"; +import { useState } from "react"; + +/** + * High-fidelity, non-interactive Slack stand-ins used in the inbox onboarding + * welcome scene. These mimic Slack's chrome — channel header, message gutter, + * square avatars, bold name + timestamp, mention pills, and Block Kit-style + * message bodies — closely enough that the demo reads as the real thing without + * pulling in any live Slack data. Theme tokens are used so it sits naturally in + * the app's light or dark surface rather than a hard white Slack panel. + */ + +function SlackSurface({ + channel, + children, +}: { + channel: string; + children: ReactNode; +}) { + return ( +
+
+ + # + + + {channel} + +
+
{children}
+
+ ); +} + +function SlackAvatar({ variant }: { variant: "richard" | "posthog" }) { + if (variant === "posthog") { + return ( + + ); + } + return ( + + R + + ); +} + +function SlackMessageRow({ + author, + avatar, + badge, + timestamp, + children, +}: { + author: string; + avatar: "richard" | "posthog"; + badge?: string; + timestamp: string; + children: ReactNode; +}) { + return ( + + + + + + {author} + + {badge && ( + + {badge} + + )} + + {timestamp} + + +
+ {children} +
+
+
+ ); +} + +export function SlackMention({ name }: { name: string }) { + return ( + + @{name} + + ); +} + +function SlackButton({ + children, + primary = false, + onClick, +}: { + children: ReactNode; + primary?: boolean; + onClick?: () => void; +}) { + return ( + + ); +} + +/** + * Beat 2 preview: the Block Kit report notification PostHog posts to the + * dedicated `#posthog-inbox` channel. Mirrors the real backend block layout in + * `slack_inbox_notifications.py`: header → meta line → summary → context line → + * action buttons. + */ +export function SlackReportNotificationPreview() { + return ( + + + + {/* header block */} + + Stop sending duplicate $pageview events on SPA history push + + {/* section block */} + + + ❗ P1 · Error tracking · PostHog/hogflix + + + 5.4% of $pageview events were duplicates — inflated funnels and + ~$1.2k/month over-billing across 2,317 customers. + + + {/* context block */} + + 3 signals  ·  👤 Suggested reviewers:{" "} + + + {/* actions block */} + + playCompletionSound("meep")}> + Review PR + + playCompletionSound("meep-smol")}> + Open in PostHog Code + + + + + + ); +} + +// First three sentences are always shown; the rest sit behind "Show more". +const ANSWER_VISIBLE = + "found it — the dashboard p75 load time jumped from 0.8s to 3.2s last Tuesday, right when we shipped PostHog/hogflix#4821 (“cache homepage rows per-user”). Session replays of the slow loads show the rows skeleton hanging 3–4s on first paint for ~22% of views, and Error tracking has a matching spike in HomeRowsTimeout over the same window. The cause is the new per-user cache key — it dropped the shared-row cache hit rate from 91% to 12%, so nearly every visit recomputes the homepage from scratch."; + +const ANSWER_HIDDEN = + "I traced it to getHomeRowsCacheKey() in src/server/cache.ts, where #4821 appends the viewer's user id to every key. The fix keeps the shared key for the non-personalized rows and only scopes the user id to the “Because you watched” row, which restores the hit rate without mixing up anyone's recommendations. I've drafted and tested it against the last week of traffic — in the replay of those sessions p75 drops back to ~0.85s."; + +const ANSWER_QUESTION = "Do you want me to ship this fix as a pull request?"; + +/** + * Beat 3 preview: a teammate asks PostHog a one-off in a normal channel, and + * PostHog answers — grounded in analytics, error tracking, replay, and the + * codebase — then offers to ship the fix. The answer reveals real depth behind + * a "Show more" toggle and ends on the value punch: a PR on request. + */ +export function SlackAskPostHogPreview() { + const [expanded, setExpanded] = useState(false); + + return ( + + + can you look into the dashboard latency + complaints? Something regressed in the last week. + + + + {ANSWER_VISIBLE} + + {expanded && ( + <> + {" "} + {ANSWER_HIDDEN} + + {ANSWER_QUESTION} + + + )} + {!expanded && ( + + )} + + + ); +} diff --git a/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx index 71cc9dcb3..75c04b135 100644 --- a/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx +++ b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx @@ -1,15 +1,16 @@ -import { CheckIcon, SlackLogoIcon, WarningIcon } from "@phosphor-icons/react"; -import { Button } from "@posthog/quill"; -import { formatRelativeTimeLong } from "@posthog/shared"; import { - builderHog, - explorerHog as detectiveHog, - happyHog, -} from "@posthog/ui/assets/hedgehogs"; -import mailHog from "@posthog/ui/assets/images/mail-hog.png"; + ArrowLeftIcon, + ArrowRightIcon, + CheckIcon, + SlackLogoIcon, + WarningIcon, +} from "@phosphor-icons/react"; +import { Button, cn } from "@posthog/quill"; +import { formatRelativeTimeLong } from "@posthog/shared"; +import { InboxWelcomeContent } from "@posthog/ui/features/inbox/components/onboarding/InboxOnboardingWelcome"; import { type InboxOnboardingStep, - inboxOnboardingProgress, + type InboxOnboardingStepInfo, useInboxOnboardingSessionStore, useInboxOnboardingState, } from "@posthog/ui/features/inbox/components/onboarding/useInboxOnboardingState"; @@ -17,191 +18,227 @@ import { type Integration, useIntegrationSelectors, } from "@posthog/ui/features/integrations/store"; +import { useIntegrations } from "@posthog/ui/features/integrations/useIntegrations"; import { useSlackConnect } from "@posthog/ui/features/integrations/useSlackConnect"; -import { GitHubIntegrationSection } from "@posthog/ui/features/settings/components/sections/GitHubIntegrationSection"; -import { SignalDefaultChannelSettings } from "@posthog/ui/features/settings/components/sections/SignalDefaultChannelSettings"; -import { SignalSourcesSettings } from "@posthog/ui/features/settings/components/sections/SignalSourcesSettings"; -import { useIntegrations } from "@posthog/ui/hooks/useIntegrations"; -import { OnboardingHogTip } from "@posthog/ui/primitives/OnboardingHogTip"; +import { GitHubIntegrationSection } from "@posthog/ui/features/settings/sections/GitHubIntegrationSection"; +import { SignalDefaultChannelSettings } from "@posthog/ui/features/settings/sections/SignalDefaultChannelSettings"; +import { SignalSourcesSettings } from "@posthog/ui/features/settings/sections/SignalSourcesSettings"; import { Flex, Spinner, Text } from "@radix-ui/themes"; +const STEP_LABEL: Record = { + welcome: "Welcome", + github: "GitHub", + slack: "Slack", + activate: "Activate", +}; + const STEP_META: Record< - InboxOnboardingStep, + Exclude, { title: string; subtitle: string } > = { - slack: { - title: "Connect Slack", - subtitle: - "Slack is the fastest way to use your agents – kick off tasks by mentioning @PostHog, ask questions in any channel, and have inbox events land where your team already works.", - }, github: { title: "Connect GitHub", subtitle: - "Research needs source to chase. Connect the GitHub org and pick the repo your agents should open pull requests against by default.", + "Point your agents at the code they'll open pull requests against. Connect your org and pick the repo to target by default.", }, - sources: { - title: "Pick signal sources", + slack: { + title: "Connect Slack", subtitle: - "What should your agents watch? Error tracking, session replays, support tickets, GitHub issues – anything you turn on becomes input for the inbox.", + "Slack is where your agents deliver reports and take requests. Connect your workspace so everything lands where your team already works.", }, - notifications: { - title: "Pick a notification channel", + activate: { + title: "Activate agents", subtitle: - "Where should inbox events land in Slack? Pick a default channel; you can change this any time.", - }, -}; - -const STEP_LABEL: Record = { - slack: "Slack", - github: "GitHub", - sources: "Sources", - notifications: "Notifications", -}; - -const STEP_HOG: Record = { - slack: { - src: happyHog, - tip: "Slack's where I'm most useful – mention me anywhere and I'll get to work.", - }, - github: { - src: builderHog, - tip: "Show me where the code lives and I'll start opening pull requests.", - }, - sources: { - src: detectiveHog, - tip: "Tell me what to investigate, I'll dig through the rest.", - }, - notifications: { - src: mailHog, - tip: "Pick a channel and I'll start dropping reports there.", + "Choose what your agents watch and where reports land. Flip these on and self-driving starts working.", }, }; /** - * Full-screen takeover shown in place of the inbox tabs until setup is done. - * Strictly linear: each step gates the next, with Slack offering a session - * skip for genuine non-Slack users. + * Full-screen onboarding takeover shown in place of the inbox tabs until setup + * is done. A linear-but-navigable stepper: Welcome → GitHub → Slack → Activate. + * The cursor lives in the session store so the user can move backward as well + * as forward; Continue is gated on the current step being satisfied. */ export function InboxOnboardingPane() { const state = useInboxOnboardingState(); + const goNext = useInboxOnboardingSessionStore((s) => s.goNext); + const goBack = useInboxOnboardingSessionStore((s) => s.goBack); + const goToStep = useInboxOnboardingSessionStore((s) => s.goToStep); const skipSlack = useInboxOnboardingSessionStore((s) => s.skipSlack); - const { slackIntegrations } = useIntegrationSelectors(); - const slackIntegrationId = slackIntegrations[0]?.id; + const finish = useInboxOnboardingSessionStore((s) => s.finish); + const { slackIntegrations, hasSlackIntegration } = useIntegrationSelectors(); + const slackIntegrationId = slackIntegrations[0]?.id ?? null; - if (state.isLoading || state.currentStep === null) return null; + if (state.isLoading) return null; - const currentStep = state.currentStep; - const meta = STEP_META[currentStep]; - const hog = STEP_HOG[currentStep]; - const progress = inboxOnboardingProgress(state); - const stepNumber = progress.doneCount + 1; + const { currentStep, currentIndex, currentStepDone, isLastStep, steps } = + state; + const doneByStep = Object.fromEntries( + steps.map((s) => [s.step, s.done]), + ) as Record; + const isWelcome = currentStep === "welcome"; + const showSkipSlack = currentStep === "slack" && !hasSlackIntegration; - return ( -
- - - - - - Step {stepNumber} of {progress.totalCount} · Self-driving setup - - - {meta.title} - - - {meta.subtitle} - - - -
- {currentStep === "slack" && } - {currentStep === "github" && ( - - )} - {currentStep === "sources" && ( - - )} - {currentStep === "notifications" && ( - - )} -
+ const handleSkipSlack = () => { + skipSlack(); + goNext(); + }; + const handleContinue = () => { + if (isLastStep) finish(); + else goNext(); + }; - + + - {currentStep === "slack" && ( - + {isWelcome ? ( + + ) : ( + + + + {STEP_META[currentStep].title} + + + {STEP_META[currentStep].subtitle} + + + +
+ {currentStep === "github" && ( + + )} + {currentStep === "slack" && } + {currentStep === "activate" && ( + + )} +
+
)} + + +
+ {currentIndex > 0 && ( + + )} +
+ + {showSkipSlack && ( + + )} + + +
); } function Stepper({ - currentStep, - state, + steps, + currentIndex, + currentStepDone, + onSelect, }: { - currentStep: InboxOnboardingStep; - state: ReturnType; + steps: InboxOnboardingStepInfo[]; + currentIndex: number; + currentStepDone: boolean; + onSelect: (index: number) => void; }) { - const stepDone: Record = { - slack: state.slack.done, - github: state.github.done, - sources: state.sources.done, - notifications: state.notifications.done, - }; - const visibleSteps: InboxOnboardingStep[] = ["slack", "github", "sources"]; - if (state.notifications.applicable) visibleSteps.push("notifications"); - return ( - {visibleSteps.map((step, idx) => { - const isCurrent = step === currentStep; - const isDone = stepDone[step]; + {steps.map((info, idx) => { + const isCurrent = idx === currentIndex; + // Back to anything already visited; forward only one step, once the + // current step is satisfied. + const reachable = + idx <= currentIndex || (idx === currentIndex + 1 && currentStepDone); return ( - + {idx > 0 && ( )} - + ); })} @@ -220,23 +257,26 @@ function StepBadge({ }) { const base = "flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[11px] font-semibold"; - if (isDone) { + if (isCurrent) { return ( - - + + {index} ); } - if (isCurrent) { + if (isDone) { return ( - - {index} + + ); } return ( {index} @@ -245,10 +285,33 @@ function StepBadge({ } /** - * Onboarding-shaped Slack widget: just the connect handshake and the - * connected state. Notification channel choice belongs to the dedicated - * notifications step, so we deliberately don't pull in - * `SlackInboxNotificationsSettings` here. + * Activate step: pick the signal sources the agents watch and, when Slack is + * connected, the default channel reports post to. Toggling these is what makes + * the step "done" and lights up the Activate button. + */ +function ActivateStepBody({ + slackIntegrationId, + slackChannelApplicable, +}: { + slackIntegrationId: number | null; + slackChannelApplicable: boolean; +}) { + return ( + + + {slackChannelApplicable && ( +
+ +
+ )} +
+ ); +} + +/** + * Onboarding-shaped Slack widget: just the connect handshake and the connected + * state. The "I don't use Slack" escape lives in the pane footer, and the + * notification channel choice belongs to the Activate step. */ function SlackStepBody() { const { isLoading } = useIntegrations(); @@ -310,7 +373,7 @@ function SlackConnectedRow({ integration }: { integration: Integration }) { - + Connected to {workspaceName} @@ -333,8 +396,7 @@ export function InboxOnboardingHeader() { return ( Inbox diff --git a/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingWelcome.tsx b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingWelcome.tsx index 36c9c78c8..1d0203361 100644 --- a/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingWelcome.tsx +++ b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingWelcome.tsx @@ -1,75 +1,48 @@ -import { ArrowRightIcon } from "@phosphor-icons/react"; -import { Button } from "@posthog/quill"; -import { explorerHog } from "@posthog/ui/assets/hedgehogs"; -import { ConventionalCommitScopeTag } from "@posthog/ui/features/inbox/components/ConventionalCommitScopeTag"; -import { useInboxOnboardingSessionStore } from "@posthog/ui/features/inbox/components/onboarding/useInboxOnboardingState"; -import { PriorityMonogram } from "@posthog/ui/features/inbox/components/PriorityMonogram"; -import { ForYouBadge } from "@posthog/ui/features/inbox/components/utils/ForYouBadge"; -import { InboxBadge } from "@posthog/ui/features/inbox/components/utils/InboxBadge"; -import { OnboardingHogTip } from "@posthog/ui/primitives/OnboardingHogTip"; +import { + SlackAskPostHogPreview, + SlackMention, + SlackReportNotificationPreview, +} from "@posthog/ui/features/inbox/components/onboarding/FakeSlack"; +import { PrDiffIndicator } from "@posthog/ui/features/inbox/components/PrDiffIndicator"; +import { PullRequestCardView } from "@posthog/ui/features/inbox/components/PullRequestCardView"; import { Flex, Text } from "@radix-ui/themes"; import { motion } from "framer-motion"; /** - * Welcome scene shown once per session before the setup stepper. Sells - * self-driving as the product: agents ship pull requests, deliver them - * to your Slack, and respond when you ask them directly. + * The welcome scene — now the first onboarding step. Sells self-driving as the + * product (agents ship pull requests, deliver them to Slack, respond when + * asked). The surrounding pane owns the stepper chrome and Back/Continue, so + * this renders content only. */ -export function InboxOnboardingWelcome() { - const acknowledgeWelcome = useInboxOnboardingSessionStore( - (s) => s.acknowledgeWelcome, - ); - +export function InboxWelcomeContent() { return ( -
- - - - - } - delay={0.05} - /> - } - delay={0.1} - /> - } - delay={0.15} - /> - - - - - - - About two minutes · Slack, GitHub, sources, notifications - - + + + + + } + delay={0.05} + /> + } + delay={0.1} + /> + } + delay={0.15} + /> -
+
); } @@ -80,17 +53,15 @@ function Hero() { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.35 }} > - - - Welcome to PostHog Code - + - Self-driving for your product. + Welcome to self-driving for your product - - Your agents read your product data and ship pull requests against your - code. They drop the PRs into Slack so you don't have to context-switch - – and you can talk to them like a teammate. + + PostHog responder agents monitor your users' experience and your + systems for issues. They hand you the fix as a pull request, dropped + into Slack so you never context-switch. And you can talk to{" "} + like a teammate. @@ -121,7 +92,7 @@ function Beat({ 0{index} - + {label} @@ -136,222 +107,20 @@ function Beat({ ); } -// ── Preview clones ─────────────────────────────────────────────────────── -// -// Visual stand-ins for the real surfaces (PullRequestCard, Slack chrome). -// The PR card reuses production primitives directly so it reads pixel- -// close to what users will see post-setup. The Slack previews mirror -// Slack's chrome closely enough that the demo doesn't read as stylised. - +/** + * Beat 1 preview: the exact production `PullRequestCardView`, fed mocked data + * and no handlers, so it reads pixel-identical to the live pull requests list. + */ function PullRequestCardPreview() { return ( -
- - - - - - - Stop sending duplicate $pageview events on SPA history push - - - - 5.4% of $pageview events were duplicates — inflated funnels and - ~$1.2k/month over-billing across 2,317 customers. - - - - posthog/posthog - - - +12 - −3 - - Actionable - - - - - - - -
- ); -} - -// ── Slack previews ─────────────────────────────────────────────────────── - -function SlackPrNotificationPreview() { - return ( - - - - I just shipped a draft PR for review: - - - - - ); -} - -function SlackMentionPreview() { - return ( - - - - - can you look into the dashboard - latency complaints? Something regressed in the last week. - - - - - On it. Pulling the last 7 days of session replays and error tracking - for the dashboard route – I'll tag you when I have something. - - - - - ); -} - -function SlackChrome({ - channel, - children, -}: { - channel: string; - children: React.ReactNode; -}) { - return ( -
- - # - {channel} - -
{children}
-
- ); -} - -function SlackMessage({ - authorName, - avatarSrc, - authorBadge, - timestamp, - children, -}: { - authorName: string; - avatarSrc: "alice" | "posthog"; - authorBadge?: string; - timestamp: string; - children: React.ReactNode; -}) { - return ( - - - - - - {authorName} - - {authorBadge && ( - - {authorBadge} - - )} - - {timestamp} - - -
{children}
-
-
- ); -} - -function SlackAvatar({ variant }: { variant: "alice" | "posthog" }) { - if (variant === "posthog") { - return ( - - 🦔 - - ); - } - return ( - - A - - ); -} - -function SlackMention({ name }: { name: string }) { - return ( - - @{name} - - ); -} - -function SlackAttachment() { - return ( -
-
- - - posthog/posthog#12345 - - - Stop sending duplicate $pageview events on SPA history push - - - - +12 - −3 - - · - - Review on GitHub - - - -
+ } + /> ); } diff --git a/packages/ui/src/features/inbox/components/onboarding/useInboxOnboardingState.ts b/packages/ui/src/features/inbox/components/onboarding/useInboxOnboardingState.ts index a946c201e..9200c7ffd 100644 --- a/packages/ui/src/features/inbox/components/onboarding/useInboxOnboardingState.ts +++ b/packages/ui/src/features/inbox/components/onboarding/useInboxOnboardingState.ts @@ -1,23 +1,29 @@ import { useSignalSourceToggles } from "@posthog/ui/features/inbox/hooks/useSignalSourceToggles"; import { useSignalTeamConfig } from "@posthog/ui/features/inbox/hooks/useSignalTeamConfig"; import { useIntegrationSelectors } from "@posthog/ui/features/integrations/store"; -import { useRepositoryIntegration } from "@posthog/ui/hooks/useIntegrations"; +import { useRepositoryIntegration } from "@posthog/ui/features/integrations/useIntegrations"; import { create } from "zustand"; -export type InboxOnboardingStep = - | "slack" - | "github" - | "sources" - | "notifications"; +export type InboxOnboardingStep = "welcome" | "github" | "slack" | "activate"; -const STEP_ORDER: InboxOnboardingStep[] = [ - "slack", +export const STEP_ORDER: InboxOnboardingStep[] = [ + "welcome", "github", - "sources", - "notifications", + "slack", + "activate", ]; +function clampIndex(index: number): number { + return Math.max(0, Math.min(STEP_ORDER.length - 1, index)); +} + interface OnboardingSessionStore { + /** + * Cursor into `STEP_ORDER`. Unlike the old derived-step model, the step is + * now explicit so the user can move backward as well as forward. Session + * scoped: a fresh session starts at the welcome step. + */ + stepIndex: number; /** * Slack skip is session-scoped: if the user finishes onboarding without * Slack we just never re-show the takeover, which naturally means no nag. @@ -25,32 +31,60 @@ interface OnboardingSessionStore { */ slackSkipped: boolean; /** - * The welcome scene appears once per session before the stepper starts. - * Acknowledging it via "Set it up" drops the user straight into the - * first incomplete step on subsequent renders. + * Latches whether the takeover is showing this session. Decided once (from + * `isComplete`) when onboarding first loads, then held so completing the + * final step doesn't yank the pane out from under the user mid-flow — they + * leave by clicking "Activate agents" (`finish`). */ - welcomeAcknowledged: boolean; + active: boolean | null; + /** Set once the user explicitly finishes on the Activate step. */ + finished: boolean; + goToStep: (index: number) => void; + goNext: () => void; + goBack: () => void; skipSlack: () => void; - acknowledgeWelcome: () => void; + setActive: (active: boolean) => void; + finish: () => void; reset: () => void; } export const useInboxOnboardingSessionStore = create( (set) => ({ + stepIndex: 0, slackSkipped: false, - welcomeAcknowledged: false, + active: null, + finished: false, + goToStep: (index) => set({ stepIndex: clampIndex(index) }), + goNext: () => set((s) => ({ stepIndex: clampIndex(s.stepIndex + 1) })), + goBack: () => set((s) => ({ stepIndex: clampIndex(s.stepIndex - 1) })), skipSlack: () => set({ slackSkipped: true }), - acknowledgeWelcome: () => set({ welcomeAcknowledged: true }), - reset: () => set({ slackSkipped: false, welcomeAcknowledged: false }), + setActive: (active) => set({ active }), + finish: () => set({ finished: true }), + reset: () => + set({ + stepIndex: 0, + slackSkipped: false, + active: null, + finished: false, + }), }), ); +export interface InboxOnboardingStepInfo { + step: InboxOnboardingStep; + done: boolean; +} + export interface InboxOnboardingState { - slack: { done: boolean; skipped: boolean }; - github: { done: boolean }; - sources: { done: boolean }; - notifications: { done: boolean; applicable: boolean }; - currentStep: InboxOnboardingStep | null; + /** All steps in order, each with its completion flag. Always length 4. */ + steps: InboxOnboardingStepInfo[]; + currentStep: InboxOnboardingStep; + currentIndex: number; + /** Whether the current step's requirement is satisfied (gates Continue). */ + currentStepDone: boolean; + isLastStep: boolean; + /** Slack is connected and not skipped, so a default channel can be chosen. */ + slackChannelApplicable: boolean; isComplete: boolean; isLoading: boolean; } @@ -64,64 +98,51 @@ export function useInboxOnboardingState(): InboxOnboardingState { const { data: teamConfig, isLoading: teamConfigLoading } = useSignalTeamConfig(); const { displayValues, isLoading: sourcesLoading } = useSignalSourceToggles(); + const stepIndex = useInboxOnboardingSessionStore((s) => s.stepIndex); const slackSkipped = useInboxOnboardingSessionStore((s) => s.slackSkipped); - const slackDone = hasSlackIntegration || slackSkipped; const githubDone = hasGithubIntegration && repositories.length > 0; + const slackDone = hasSlackIntegration || slackSkipped; const sourcesDone = Object.values(displayValues).some(Boolean); - const notificationsApplicable = hasSlackIntegration && !slackSkipped; - const notificationsDone = - !notificationsApplicable || - !!teamConfig?.default_slack_notification_channel; + const slackChannelApplicable = hasSlackIntegration && !slackSkipped; + const channelDone = + !slackChannelApplicable || !!teamConfig?.default_slack_notification_channel; + // The Activate step bundles source selection and the Slack channel choice. + const activateDone = sourcesDone && channelDone; - const isLoading = teamConfigLoading || sourcesLoading; - const isComplete = - slackDone && githubDone && sourcesDone && notificationsDone; + const doneByStep: Record = { + welcome: true, + github: githubDone, + slack: slackDone, + activate: activateDone, + }; - let currentStep: InboxOnboardingStep | null = null; - if (!isComplete) { - const stepDone: Record = { - slack: slackDone, - github: githubDone, - sources: sourcesDone, - notifications: notificationsDone, - }; - currentStep = - STEP_ORDER.find( - (step) => !stepDone[step] && stepApplies(step, slackSkipped), - ) ?? null; - } + const currentIndex = clampIndex(stepIndex); + const currentStep = STEP_ORDER[currentIndex]; return { - slack: { done: slackDone, skipped: slackSkipped }, - github: { done: githubDone }, - sources: { done: sourcesDone }, - notifications: { - done: notificationsDone, - applicable: notificationsApplicable, - }, + steps: STEP_ORDER.map((step) => ({ step, done: doneByStep[step] })), currentStep, - isComplete, - isLoading, + currentIndex, + currentStepDone: doneByStep[currentStep], + isLastStep: currentIndex === STEP_ORDER.length - 1, + slackChannelApplicable, + isComplete: githubDone && slackDone && activateDone, + isLoading: teamConfigLoading || sourcesLoading, }; } -function stepApplies( - step: InboxOnboardingStep, - slackSkipped: boolean, -): boolean { - if (step === "notifications") return !slackSkipped; - return true; -} - +/** + * Progress across the actionable steps (everything but the informational + * welcome). Used by the Agents-view callout to nudge "N of M done". + */ export function inboxOnboardingProgress(state: InboxOnboardingState): { doneCount: number; totalCount: number; } { - const steps = [state.slack.done, state.github.done, state.sources.done]; - if (state.notifications.applicable) steps.push(state.notifications.done); + const actionable = state.steps.filter((s) => s.step !== "welcome"); return { - doneCount: steps.filter(Boolean).length, - totalCount: steps.length, + doneCount: actionable.filter((s) => s.done).length, + totalCount: actionable.length, }; } diff --git a/packages/ui/src/features/settings/sections/GitHubIntegrationSection.tsx b/packages/ui/src/features/settings/sections/GitHubIntegrationSection.tsx index 929d2fbb6..437e2ecea 100644 --- a/packages/ui/src/features/settings/sections/GitHubIntegrationSection.tsx +++ b/packages/ui/src/features/settings/sections/GitHubIntegrationSection.tsx @@ -130,7 +130,7 @@ export function GitHubIntegrationSection({ ? describeGithubConnectError(connectError) : timedOut ? "We didn't hear back from GitHub. Try again." - : "Required for the Inbox pipeline to work"} + : "Required for PostHog agents to work."} )} From 4d7f933d4dc79f570a17e6dd629c4c91c6eb91e3 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Thu, 11 Jun 2026 19:01:39 +0100 Subject: [PATCH 3/3] feat(code): polish inbox onboarding copy, Agents step, sticky footer - Rename the final step to "Agents" (heading "Set up agents") - Make the Back/Continue footer a sticky bottom bar so the primary action stays visible while scrolling - Swap the PR/Slack mock to a believable Hogflix product bug (playback resume) instead of the analytics pageview issue - Tighten the welcome beat descriptions to <=2 lines and drop the anti-spam over-explanation - Collapse the PostHog answer's analysis behind an inline [...] toggle, keeping the first sentence and putting the ship-it question on its own line --- .../inbox/components/onboarding/FakeSlack.tsx | 63 ++++++++----------- .../onboarding/InboxOnboardingPane.tsx | 12 ++-- .../onboarding/InboxOnboardingWelcome.tsx | 21 ++++--- 3 files changed, 46 insertions(+), 50 deletions(-) diff --git a/packages/ui/src/features/inbox/components/onboarding/FakeSlack.tsx b/packages/ui/src/features/inbox/components/onboarding/FakeSlack.tsx index 4e76dfab5..35f0cb186 100644 --- a/packages/ui/src/features/inbox/components/onboarding/FakeSlack.tsx +++ b/packages/ui/src/features/inbox/components/onboarding/FakeSlack.tsx @@ -146,16 +146,16 @@ export function SlackReportNotificationPreview() { {/* header block */} - Stop sending duplicate $pageview events on SPA history push + Resume playback from the saved position, not the start {/* section block */} - ❗ P1 · Error tracking · PostHog/hogflix + ❗ P1 · Session replay · PostHog/hogflix - 5.4% of $pageview events were duplicates — inflated funnels and - ~$1.2k/month over-billing across 2,317 customers. + 8.4% of “Continue watching” resumes restart the title from 0:00 — + affected sessions run 14% shorter and resume churn is up 3×. {/* context block */} @@ -178,20 +178,22 @@ export function SlackReportNotificationPreview() { ); } -// First three sentences are always shown; the rest sit behind "Show more". -const ANSWER_VISIBLE = - "found it — the dashboard p75 load time jumped from 0.8s to 3.2s last Tuesday, right when we shipped PostHog/hogflix#4821 (“cache homepage rows per-user”). Session replays of the slow loads show the rows skeleton hanging 3–4s on first paint for ~22% of views, and Error tracking has a matching spike in HomeRowsTimeout over the same window. The cause is the new per-user cache key — it dropped the shared-row cache hit rate from 91% to 12%, so nearly every visit recomputes the homepage from scratch."; +// The answer keeps the first sentence and the value-punch question, collapsing +// the analysis in between behind an inline "[…]" toggle — enough to tease the +// depth without dumping a wall of text into the preview. +const ANSWER_FIRST = + "found it — dashboard p75 load time jumped from 0.8s to 3.2s last Tuesday, right when we shipped PostHog/hogflix#4821 (“cache homepage rows per-user”)."; -const ANSWER_HIDDEN = - "I traced it to getHomeRowsCacheKey() in src/server/cache.ts, where #4821 appends the viewer's user id to every key. The fix keeps the shared key for the non-personalized rows and only scopes the user id to the “Because you watched” row, which restores the hit rate without mixing up anyone's recommendations. I've drafted and tested it against the last week of traffic — in the replay of those sessions p75 drops back to ~0.85s."; +const ANSWER_MIDDLE = + "Session replays show the rows skeleton hanging 3–4s on first paint for ~22% of views, and Error tracking has a matching spike in HomeRowsTimeout. The cause is the new per-user cache key — it dropped the shared-row hit rate from 91% to 12%, so nearly every visit recomputes the homepage. I traced it to getHomeRowsCacheKey() in src/server/cache.ts, where #4821 appends the viewer's user id to every key. The fix keeps the shared key and only scopes the user id to the “Because you watched” row; tested against the last week, p75 drops back to ~0.85s."; -const ANSWER_QUESTION = "Do you want me to ship this fix as a pull request?"; +const ANSWER_LAST = "Do you want me to ship this fix as a pull request?"; /** * Beat 3 preview: a teammate asks PostHog a one-off in a normal channel, and * PostHog answers — grounded in analytics, error tracking, replay, and the - * codebase — then offers to ship the fix. The answer reveals real depth behind - * a "Show more" toggle and ends on the value punch: a PR on request. + * codebase — then offers to ship the fix. The analysis collapses behind an + * inline "[…]" toggle, ending on the value punch: a PR on request. */ export function SlackAskPostHogPreview() { const [expanded, setExpanded] = useState(false); @@ -212,30 +214,19 @@ export function SlackAskPostHogPreview() { badge="App" timestamp="10:42 AM" > - - {ANSWER_VISIBLE} - - {expanded && ( - <> - {" "} - {ANSWER_HIDDEN} - - {ANSWER_QUESTION} - - - )} - {!expanded && ( - - )} + {ANSWER_FIRST}{" "} + {" "} + {expanded && <>{ANSWER_MIDDLE} } + {ANSWER_LAST} ); diff --git a/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx index 75c04b135..11957445f 100644 --- a/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx +++ b/packages/ui/src/features/inbox/components/onboarding/InboxOnboardingPane.tsx @@ -29,7 +29,7 @@ const STEP_LABEL: Record = { welcome: "Welcome", github: "GitHub", slack: "Slack", - activate: "Activate", + activate: "Agents", }; const STEP_META: Record< @@ -47,7 +47,7 @@ const STEP_META: Record< "Slack is where your agents deliver reports and take requests. Connect your workspace so everything lands where your team already works.", }, activate: { - title: "Activate agents", + title: "Set up agents", subtitle: "Choose what your agents watch and where reports land. Flip these on and self-driving starts working.", }, @@ -91,7 +91,7 @@ export function InboxOnboardingPane() { return (
@@ -137,7 +137,11 @@ export function InboxOnboardingPane() { )} - +
{currentIndex > 0 && (