From 4a9b4b0a9edff2b4942ef84975c8fd4ecd6521d6 Mon Sep 17 00:00:00 2001 From: Anthony Lio Date: Mon, 28 Sep 2026 17:28:11 +0300 Subject: [PATCH] feat(docs): o11y agent setup stepper (#50962) ## Problem on monitoring agent pages the prompt lives in a "prompt" tab next to agent ones, while each agent tab ended with "paste the prompt" users have to work out that the prompt is sitting in that previous tab ## Solution this pr is a proposal to set agent setup as a two stepper `1` for the prompt panel `2` holds the agent tabs: - extracts prompt into a first step - moves agent tabs within their own step - polishes agent docs to match recent ui updates - sets `prompt` as an anchor link within agent tabs | state | preview | | -------|------| | before | image | | after | image | ## Review instructions Provide a clear numbered procedure that the PR reviewer can walk through. 1. visit [/automate-with-agents/health](https://docs-git-docs-agent-setup-stepper-supabase.vercel.app/docs/guides/observability/automate-with-agents/health#set-up-the-agent) ## Checklist Check all before review: - [x] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) - [x] If I wrote a new docs topic or edited an existing topic, I used the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs [style guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide) ## Summary by CodeRabbit * **New Features** * Agent setup instructions are organized into prompt-copying and scheduling steps, with links to harness documentation. * Code tabs support icons and controlled selection. * Source code samples support adjustable footer notches. * **Bug Fixes** * Step numbers now display the correct shadow. * Links to page anchors now scroll to, focus, and highlight their targets, while respecting reduced-motion preferences. --- .../docs/components/StepHikeCompact/index.tsx | 19 +- apps/docs/data/monitoring-agents.utils.ts | 8 +- .../features/directives/CodeSample.client.tsx | 49 +++-- .../directives/CodeTabs.components.tsx | 17 +- apps/docs/features/docs/MdxBase.shared.tsx | 2 +- apps/docs/features/ui/AgentSetup.tsx | 197 +++++++++++++----- .../markdown-schema/AgentSetup.test.ts | 5 +- .../internals/markdown-schema/AgentSetup.ts | 7 +- apps/docs/styles/code-block.css | 5 +- 9 files changed, 221 insertions(+), 88 deletions(-) diff --git a/apps/docs/components/StepHikeCompact/index.tsx b/apps/docs/components/StepHikeCompact/index.tsx index 33e15d219cb..1b6ac3eb8e6 100644 --- a/apps/docs/components/StepHikeCompact/index.tsx +++ b/apps/docs/components/StepHikeCompact/index.tsx @@ -7,16 +7,18 @@ interface IStep { } interface IStepHikeCompactSubcomponents { - Step: FC - Details: FC - Code: FC + Step: FC> + Details: FC> + Code: FC> } interface IDetails { title?: string fullWidth?: boolean } -interface ICode {} +interface ICode { + className?: string +} interface IStepHikeCompact { title: string @@ -65,7 +67,7 @@ const Step: FC> = ({ children, title, step }) => { className="border bg-surface-100 border-control flex items-center justify-center rounded-full w-6 h-6 text-xs text-foreground font-normal font-mono - dropshadow-sm + drop-shadow-sm " > {step} @@ -107,7 +109,7 @@ const Details: FC> = ({ children, title, fullWidth = ) } -const Code: FC> = ({ children }) => { +const Code: FC> = ({ children, className }) => { // Not `not-prose`: steps interleave labels and admonitions with their code samples, and // stripping prose leaves that text unstyled and flush against the samples. return ( @@ -118,7 +120,8 @@ const Code: FC> = ({ children }) => { // `Step` spaces the block as a whole, so samples don't carry margins of their own... '[&_.shiki]:!my-0 [&_.shiki-wrapper]:!my-0', // ...but back-to-back samples have no prose between them to separate them. - '[&_.shiki+.shiki]:!mt-6' + '[&_.shiki+.shiki]:!mt-6', + className )} > {children} @@ -129,4 +132,4 @@ const Code: FC> = ({ children }) => { StepHikeCompact.Step = Step StepHikeCompact.Details = Details StepHikeCompact.Code = Code -export default StepHikeCompact +export { StepHikeCompact } diff --git a/apps/docs/data/monitoring-agents.utils.ts b/apps/docs/data/monitoring-agents.utils.ts index 5c084726600..b6c975bf19a 100644 --- a/apps/docs/data/monitoring-agents.utils.ts +++ b/apps/docs/data/monitoring-agents.utils.ts @@ -26,6 +26,8 @@ export type MonitoringAgentHarnessSetup = { const WEEKDAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const const DAY_LABELS = ['12am', '6am', '12pm', '6pm'] as const +export const AGENT_PROMPT_ANCHOR = 'agent-prompt' + const MCP_STEP = 'Connect the [Supabase MCP server](/docs/guides/ai-tools/mcp) with `project_ref` and `read_only=true`.' @@ -103,7 +105,7 @@ export function getMonitoringAgentHarnesses(agent: MonitoringAgent): MonitoringA isSubHourly ? 'In the Claude Code Desktop app, open **Routines**, click **New routine**, and choose **Local**.' : 'Open [Claude routines](https://claude.ai/code/routines) or run `/schedule` in Claude Code.', - `Name it ${agent.name}. Paste the prompt. Set the schedule to ${cadence}.`, + `Name it ${agent.name}. Paste the [prompt](#${AGENT_PROMPT_ANCHOR}). Set the schedule to ${cadence}.`, ], note: isSubHourly ? 'Cloud routines have a 1-hour minimum. Use a [Desktop scheduled task](https://code.claude.com/docs/en/desktop-scheduled-tasks) for this cadence.' @@ -119,7 +121,7 @@ export function getMonitoringAgentHarnesses(agent: MonitoringAgent): MonitoringA steps: [ MCP_STEP, 'Open **Scheduled** in the ChatGPT desktop app, or ask Codex to create a standalone scheduled task.', - `Name it ${agent.name}. Paste the prompt. Set the schedule to ${cadence}. Each run should start a new chat.`, + `Name it ${agent.name}. Paste the [prompt](#${AGENT_PROMPT_ANCHOR}). Set the schedule to ${cadence}. Each run should start a new chat.`, ], }, { @@ -132,7 +134,7 @@ export function getMonitoringAgentHarnesses(agent: MonitoringAgent): MonitoringA steps: [ MCP_STEP, 'Create an automation in the Agents Window, at [cursor.com/automations](https://cursor.com/automations), or with the `/automate` skill.', - `Name it ${agent.name}. Use a scheduled trigger (${cadence}, cron \`${cron}\`). Paste the prompt. Keep the agent read-only, with no repository.`, + `Name it ${agent.name}. Use a scheduled trigger (${cadence}, cron \`${cron}\`). Paste the [prompt](#${AGENT_PROMPT_ANCHOR}). Keep the agent read-only, with no repository.`, ], }, ] diff --git a/apps/docs/features/directives/CodeSample.client.tsx b/apps/docs/features/directives/CodeSample.client.tsx index 8e6f2c15a9f..e609d3236ca 100644 --- a/apps/docs/features/directives/CodeSample.client.tsx +++ b/apps/docs/features/directives/CodeSample.client.tsx @@ -1,7 +1,7 @@ 'use client' import Link from 'next/link' -import { type PropsWithChildren, type ReactNode } from 'react' +import { type CSSProperties, type PropsWithChildren, type ReactNode } from 'react' import { cn, DropdownMenu, @@ -26,18 +26,27 @@ interface SingleSourceProps { interface SourceFrameProps { footer: ReactNode + notchWidth?: number } -const SOURCE_FOOTER_CLASSES = cn( - 'not-prose absolute right-0 bottom-0 z-1 flex h-8 w-29.25 items-center gap-2 px-3 whitespace-nowrap', +export const SOURCE_FOOTER_CLASSES = cn( + 'not-prose absolute right-0 bottom-0 z-1 flex h-8 w-(--source-notch-width) items-center gap-2 px-3 whitespace-nowrap', 'text-xs text-foreground-lighter no-underline transition-colors hover:text-foreground', 'focus-inset rounded-md' ) -const SOURCE_NOTCH_OUTLINE = - 'M0 39.5A7.5 7.5 0 0 0 7.5 32V16A8.5 8.5 0 0 1 16 7.5H117A7.5 7.5 0 0 0 124.5 0' +const DEFAULT_NOTCH_WIDTH = 117 -const SOURCE_NOTCH_FOCUS_RING = 'M0 41A9 9 0 0 0 9 32V16A7 7 0 0 1 16 9H117A9 9 0 0 0 126 0' +const getNotchOutline = (width: number): string => + `M0 39.5A7.5 7.5 0 0 0 7.5 32V16A8.5 8.5 0 0 1 16 7.5H${width}A7.5 7.5 0 0 0 ${width + 7.5} 0` + +const getNotchFocusRing = (width: number): string => + `M0 41A9 9 0 0 0 9 32V16A7 7 0 0 1 16 9H${width}A9 9 0 0 0 ${width + 9} 0` + +const getNotchMask = (width: number): string => + `url("data:image/svg+xml,${encodeURIComponent( + `` + )}")` const getSourcePath = (source: string | URL): string => { const [, org, repo, , , ...path] = new URL(source).pathname.split('/') @@ -123,24 +132,38 @@ function SingleSource({ children, source }: PropsWithChildren ) } -function SourceFrame({ children, footer }: PropsWithChildren) { +export function SourceFrame({ + children, + footer, + notchWidth = DEFAULT_NOTCH_WIDTH, +}: PropsWithChildren) { + const outline = getNotchOutline(notchWidth) + const notchStyle = { + '--source-notch-width': `${notchWidth}px`, + '--source-notch-mask': getNotchMask(notchWidth), + '--source-notch-mask-width': `${notchWidth + 10}px`, + } as CSSProperties + return ( -
+
{children}
- - + + diff --git a/apps/docs/features/directives/CodeTabs.components.tsx b/apps/docs/features/directives/CodeTabs.components.tsx index 21ee142894e..5b754e0facb 100644 --- a/apps/docs/features/directives/CodeTabs.components.tsx +++ b/apps/docs/features/directives/CodeTabs.components.tsx @@ -1,9 +1,15 @@ -import { Children, isValidElement, type PropsWithChildren } from 'react' +import { Children, isValidElement, type PropsWithChildren, type ReactNode } from 'react' import { cn, TabsIndicator, TabsList, Tabs as TabsRoot, TabsTrigger } from 'ui' interface CodeTabPanelProps { id: string label?: string + icon?: ReactNode +} + +interface CodeTabsProps { + value?: string + onValueChange?: (value: string) => void } export function NamedCodeBlock({ name, children }: PropsWithChildren<{ name: string }>) { @@ -31,12 +37,14 @@ export function NamedCodeBlock({ name, children }: PropsWithChildren<{ name: str ) } -export function CodeTabs({ children }: PropsWithChildren) { +export function CodeTabs({ children, value, onValueChange }: PropsWithChildren) { const tabs = Children.toArray(children).filter(isValidElement) return ( - {tab.props.label ?? tab.props.id} + + {tab.props.icon} + {tab.props.label ?? tab.props.id} + ))} ) => { + const target = document.getElementById(event.currentTarget.hash.slice(1)) + if (!target) return + + event.preventDefault() + const { hash } = event.currentTarget + if (window.location.hash !== hash) window.history.pushState(null, '', hash) + + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches + target.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth', block: 'center' }) + target.focus({ preventScroll: true }) + + const brand = getComputedStyle(target).getPropertyValue('--brand-default') + target.animate([{ outlineColor: `hsl(${brand})` }, { outlineColor: `hsl(${brand} / 0)` }], { + duration: PROMPT_FLASH_DURATION, + easing: 'ease-out', + }) +} + const markdownComponents = { p: ({ children }: { children?: ReactNode }) => <>{children}, a: ({ href, children }: { href?: string; children?: ReactNode }) => { if (!href) return <>{children} + if (href.startsWith('#')) { + return ( + + {children} + + ) + } const external = /^(?:[a-z][a-z0-9+\-.]*:|\/\/)/i.test(href) return ( harness.key), + queryGroup: 'agent-setup', + }) + const [selectedHarness, setSelectedHarness] = useState(harnesses[0].key) + const activeHarness = queryTab ?? selectedHarness + + const handleHarnessChange = (key: string) => { + setSelectedHarness(key) + onTabSelected(key) + } + + return ( + + + + +
+ +
+
+
+ + + + + {harnesses.map((harness) => ( + } + > + + + View docs +
+ } + > + + + + ))} + + + + + ) +} + +function HarnessIcon({ harness }: HarnessProps) { + const getSrc = (useDarkVariant: boolean) => + getMcpClientIconSrc({ + icon: harness.icon, + useDarkVariant, + hasDistinctDarkIcon: harness.hasDistinctDarkIcon, + }) + + if (!harness.hasDistinctDarkIcon) { + return + } + return ( <> + + + + ) +} + +function HarnessBody({ harness }: HarnessProps) { + return ( +

{harness.intro}

    {harness.steps.map((step) => ( @@ -53,54 +189,7 @@ function HarnessBody({ harness }: { harness: MonitoringAgentHarnessSetup }) { {harness.note}

    )} -

    - - {harness.label} docs - -

    - - ) -} - -function AgentSetup({ id }: AgentSetupProps) { - const agent = getMonitoringAgent(id) - const harnesses = getMonitoringAgentHarnesses(agent) - const { resolvedTheme } = useTheme() - const theme = resolvedTheme?.includes('dark') ? 'dark' : 'light' - - return ( - - }> - - - {harnesses.map((harness) => ( - - } - > - - - ))} - +
) } diff --git a/apps/docs/internals/markdown-schema/AgentSetup.test.ts b/apps/docs/internals/markdown-schema/AgentSetup.test.ts index f11a2d1e86a..e054a21a64a 100644 --- a/apps/docs/internals/markdown-schema/AgentSetup.test.ts +++ b/apps/docs/internals/markdown-schema/AgentSetup.test.ts @@ -21,7 +21,8 @@ describe('AgentSetup markdown schema', () => { it('serializes the prompt and harness setup for a registered agent', () => { const markdown = AgentSetup({ props: { id: 'health' } }) - expect(markdown).toContain('**Prompt**') + expect(markdown).toContain('**Step 1: Copy the prompt**') + expect(markdown).toContain('**Step 2: Schedule it in your agent**') expect(markdown).toContain('You are "Health monitor"') expect(markdown).toContain('```text') expect(markdown).toContain('**Claude**') @@ -29,6 +30,8 @@ describe('AgentSetup markdown schema', () => { expect(markdown).toContain('**Cursor**') expect(markdown).toContain('claude.ai/code/routines') expect(markdown).toContain('`0 * * * *`') + expect(markdown).toContain('Paste the prompt.') + expect(markdown).not.toContain('](#') expect(markdown).toContain('[Claude docs](https://code.claude.com/docs/en/routines)') expect(markdown).toContain('[Codex docs](https://developers.openai.com/codex/app/automations)') expect(markdown).toContain('[Cursor docs](https://cursor.com/docs/cloud-agent/automations)') diff --git a/apps/docs/internals/markdown-schema/AgentSetup.ts b/apps/docs/internals/markdown-schema/AgentSetup.ts index 670b2d71630..ce57bd743a7 100644 --- a/apps/docs/internals/markdown-schema/AgentSetup.ts +++ b/apps/docs/internals/markdown-schema/AgentSetup.ts @@ -9,8 +9,10 @@ type HandlerContext = { props: Record } +const IN_PAGE_LINK = /\[([^\]]+)\]\(#[^)]+\)/g + function renderMarkdownSteps(steps: string[]): string { - return steps.map((step, index) => `${index + 1}. ${step}`).join('\n') + return steps.map((step, index) => `${index + 1}. ${step.replace(IN_PAGE_LINK, '$1')}`).join('\n') } export function AgentSetup({ props }: HandlerContext): string { @@ -19,7 +21,8 @@ export function AgentSetup({ props }: HandlerContext): string { const harnesses = getMonitoringAgentHarnesses(agent) const sections = [ - `**Prompt**\n\n${toMarkdown({ type: 'code', lang: 'text', value: prompt }).trimEnd()}`, + `**Step 1: Copy the prompt**\n\n${toMarkdown({ type: 'code', lang: 'text', value: prompt }).trimEnd()}`, + '**Step 2: Schedule it in your agent**', ...harnesses.map((harness) => { const parts = [`**${harness.label}**`, harness.intro, renderMarkdownSteps(harness.steps)] if (harness.note) parts.push(harness.note) diff --git a/apps/docs/styles/code-block.css b/apps/docs/styles/code-block.css index 7db2cf9c248..762e9e829c3 100644 --- a/apps/docs/styles/code-block.css +++ b/apps/docs/styles/code-block.css @@ -130,11 +130,10 @@ filter: var(--drop-shadow-codeblock, none); } -.code-sample-source .shiki { +.code-sample-source :is(.shiki, .code-sample-surface) { mask: linear-gradient(#000 0 0), - url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='127' height='42'%3E%3Cpath d='M0 40A8 8 0 0 0 8 32V16A8 8 0 0 1 16 8H117A8 8 0 0 0 125 0H127V42H0Z'/%3E%3C/svg%3E") - right -2px bottom -2px / 127px 42px no-repeat; + var(--source-notch-mask) right -2px bottom -2px / var(--source-notch-mask-width) 42px no-repeat; mask-composite: exclude; mask-clip: no-clip; }