mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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 | <img width="823" height="452" alt="image" src="https://github.com/user-attachments/assets/a6a01832-ea73-48c1-b31d-25a0ef970e4e" /> | | after | <img width="823" height="716" alt="image" src="https://github.com/user-attachments/assets/f5014ad0-1555-4578-b4b1-5bf2733b4d37" /> | <!-- ## Preview links If relevant, include links to changed pages for easy review access. Copy the preview base URL from the Vercel bot comment on this PR. Use the following table as an example template. | Site | Live | Preview | Search for | | -------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------- | | WWW | [/blog/your-post](https://supabase.com/blog/your-post) | [/blog/your-post](https://zone-www-dot-com-git-branch-name-supabase.vercel.app/blog/your-post) | unique phrase from the change | | Docs | [/docs/guides/your-page](https://supabase.com/docs/guides/your-page) | [/docs/guides/your-page](https://docs-git-branch-name-supabase.vercel.app/docs/guides/your-page) | unique phrase from the change | | Studio | [/dashboard](https://supabase.com/dashboard) | [/dashboard](https://studio-git-branch-name-supabase.vercel.app/dashboard) | unique phrase from the change | | Design system | [/design-system](https://supabase.com/design-system) | [/design-system](https://design-system-git-branch-name-supabase.vercel.app/design-system) | unique phrase from the change | | UI library | [/library](https://supabase.com/library) | [/library](https://ui-library-git-branch-name-supabase.vercel.app/library) | unique phrase from the change | | Knowledge base | [/kb/guides/your-page](https://supabase.com/kb/guides/your-page) | [/kb/guides/your-page](https://kb-git-branch-name-supabase.vercel.app/kb/guides/your-page) | unique phrase from the change | --> <!-- ## Additional context Optionally add any other context or screenshots. --> ## 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) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
f0e0865acc
commit
4a9b4b0a9e
9 files changed
+221
-88
No files matched your search
@@ -7,16 +7,18 @@ interface IStep {
|
||||
}
|
||||
|
||||
interface IStepHikeCompactSubcomponents {
|
||||
Step: FC<IStep>
|
||||
Details: FC<IDetails>
|
||||
Code: FC<ICode>
|
||||
Step: FC<PropsWithChildren<IStep>>
|
||||
Details: FC<PropsWithChildren<IDetails>>
|
||||
Code: FC<PropsWithChildren<ICode>>
|
||||
}
|
||||
interface IDetails {
|
||||
title?: string
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
interface ICode {}
|
||||
interface ICode {
|
||||
className?: string
|
||||
}
|
||||
|
||||
interface IStepHikeCompact {
|
||||
title: string
|
||||
@@ -65,7 +67,7 @@ const Step: FC<PropsWithChildren<IStep>> = ({ 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<PropsWithChildren<IDetails>> = ({ children, title, fullWidth =
|
||||
)
|
||||
}
|
||||
|
||||
const Code: FC<PropsWithChildren<ICode>> = ({ children }) => {
|
||||
const Code: FC<PropsWithChildren<ICode>> = ({ 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<PropsWithChildren<ICode>> = ({ 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<PropsWithChildren<ICode>> = ({ children }) => {
|
||||
StepHikeCompact.Step = Step
|
||||
StepHikeCompact.Details = Details
|
||||
StepHikeCompact.Code = Code
|
||||
export default StepHikeCompact
|
||||
export { StepHikeCompact }
|
||||
@@ -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.`,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
`<svg xmlns='http://www.w3.org/2000/svg' width='${width + 10}' height='42'><path d='M0 40A8 8 0 0 0 8 32V16A8 8 0 0 1 16 8H${width}A8 8 0 0 0 ${width + 8} 0H${width + 10}V42H0Z'/></svg>`
|
||||
)}")`
|
||||
|
||||
const getSourcePath = (source: string | URL): string => {
|
||||
const [, org, repo, , , ...path] = new URL(source).pathname.split('/')
|
||||
@@ -123,24 +132,38 @@ function SingleSource({ children, source }: PropsWithChildren<SingleSourceProps>
|
||||
)
|
||||
}
|
||||
|
||||
function SourceFrame({ children, footer }: PropsWithChildren<SourceFrameProps>) {
|
||||
export function SourceFrame({
|
||||
children,
|
||||
footer,
|
||||
notchWidth = DEFAULT_NOTCH_WIDTH,
|
||||
}: PropsWithChildren<SourceFrameProps>) {
|
||||
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 (
|
||||
<div className="group/source code-sample-source shiki-wrapper relative w-full [&_.shiki]:pb-8 [&_.shiki]:shadow-none!">
|
||||
<div
|
||||
style={notchStyle}
|
||||
className="group/source code-sample-source shiki-wrapper relative w-full [&_.shiki]:pb-8 [&_.shiki]:shadow-none!"
|
||||
>
|
||||
<div className="codeblock-drop-shadow relative has-[.code-scroll:focus-visible]:z-1">
|
||||
{children}
|
||||
</div>
|
||||
<svg
|
||||
aria-hidden
|
||||
width="125"
|
||||
width={notchWidth + 8}
|
||||
height="40"
|
||||
viewBox="0 0 125 40"
|
||||
viewBox={`0 0 ${notchWidth + 8} 40`}
|
||||
fill="none"
|
||||
className="pointer-events-none absolute right-0 bottom-0 z-1 overflow-visible"
|
||||
>
|
||||
<path d={SOURCE_NOTCH_OUTLINE} className="stroke-background-200" />
|
||||
<path d={SOURCE_NOTCH_OUTLINE} className="stroke-border" />
|
||||
<path d={outline} className="stroke-background-200" />
|
||||
<path d={outline} className="stroke-border" />
|
||||
<path
|
||||
d={SOURCE_NOTCH_FOCUS_RING}
|
||||
d={getNotchFocusRing(notchWidth)}
|
||||
strokeWidth={2}
|
||||
className="hidden stroke-ring group-has-[.code-scroll:focus-visible]/source:inline"
|
||||
/>
|
||||
|
||||
@@ -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<CodeTabsProps>) {
|
||||
const tabs = Children.toArray(children).filter(isValidElement<CodeTabPanelProps>)
|
||||
|
||||
return (
|
||||
<TabsRoot
|
||||
defaultValue={tabs[0]?.props.id}
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
className="group/code-tabs shiki-wrapper w-full isolate [&_.shiki]:my-0!"
|
||||
>
|
||||
<TabsList
|
||||
@@ -51,7 +59,10 @@ export function CodeTabs({ children }: PropsWithChildren) {
|
||||
value={tab.props.id}
|
||||
className="relative z-1 rounded-md py-2 text-xs data-[state=active]:shadow-none"
|
||||
>
|
||||
<span className="px-3">{tab.props.label ?? tab.props.id}</span>
|
||||
<span className="flex items-center gap-2 px-3 [&_img]:size-3.5">
|
||||
{tab.props.icon}
|
||||
{tab.props.label ?? tab.props.id}
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
<TabsIndicator
|
||||
|
||||
@@ -19,7 +19,7 @@ import { ProjectConfigVariables } from '~/components/ProjectConfigVariables'
|
||||
import { RealtimeLimitsEstimator } from '~/components/RealtimeLimitsEstimator'
|
||||
import { RegionsList, SmartRegionsList } from '~/components/RegionsList'
|
||||
import { SharedData } from '~/components/SharedData'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
import { StepHikeCompact } from '~/components/StepHikeCompact'
|
||||
import Table from '~/components/Table'
|
||||
import { TerraformProviderSchema } from '~/components/TerraformProviderSchema'
|
||||
import { WrapperDashboardIntegration } from '~/components/WrapperDashboardIntegration'
|
||||
|
||||
@@ -1,28 +1,66 @@
|
||||
'use client'
|
||||
|
||||
import { StepHikeCompact } from '~/components/StepHikeCompact'
|
||||
import {
|
||||
AGENT_PROMPT_ANCHOR,
|
||||
getMonitoringAgent,
|
||||
getMonitoringAgentHarnesses,
|
||||
type MonitoringAgentHarnessSetup,
|
||||
} from '~/data/monitoring-agents.utils'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { type ReactNode } from 'react'
|
||||
import { SOURCE_FOOTER_CLASSES, SourceFrame } from '~/features/directives/CodeSample.client'
|
||||
import { CodeTabs } from '~/features/directives/CodeTabs.components'
|
||||
import Image from 'next/image'
|
||||
import { useState, type MouseEvent, type ReactNode } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { ConnectionIcon } from 'ui-patterns/McpUrlBuilder'
|
||||
import { cn } from 'ui'
|
||||
import { getMcpClientIconSrc } from 'ui-patterns/McpUrlBuilder'
|
||||
|
||||
import { AiPrompt } from './AiPrompt'
|
||||
import { PromptCode } from './PromptPanel'
|
||||
import { TabPanel, Tabs } from './Tabs'
|
||||
import { TabPanel } from './Tabs'
|
||||
import { useTabsWithQueryParams } from './useTabsWithQueryParams'
|
||||
|
||||
type AgentSetupProps = {
|
||||
id: string
|
||||
}
|
||||
|
||||
type HarnessProps = {
|
||||
harness: MonitoringAgentHarnessSetup
|
||||
}
|
||||
|
||||
const DOCS_NOTCH_WIDTH = 100
|
||||
const PROMPT_FLASH_DURATION = 1600
|
||||
|
||||
const handleInPageLinkClick = (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
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 (
|
||||
<a href={href} onClick={handleInPageLinkClick} className="text-primary hover:underline">
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
const external = /^(?:[a-z][a-z0-9+\-.]*:|\/\/)/i.test(href)
|
||||
return (
|
||||
<a
|
||||
@@ -37,9 +75,107 @@ const markdownComponents = {
|
||||
code: PromptCode,
|
||||
}
|
||||
|
||||
function HarnessBody({ harness }: { harness: MonitoringAgentHarnessSetup }) {
|
||||
function AgentSetup({ id }: AgentSetupProps) {
|
||||
const agent = getMonitoringAgent(id)
|
||||
const harnesses = getMonitoringAgentHarnesses(agent)
|
||||
const { queryTab, onTabSelected } = useTabsWithQueryParams({
|
||||
tabIds: harnesses.map((harness) => harness.key),
|
||||
queryGroup: 'agent-setup',
|
||||
})
|
||||
const [selectedHarness, setSelectedHarness] = useState<string>(harnesses[0].key)
|
||||
const activeHarness = queryTab ?? selectedHarness
|
||||
|
||||
const handleHarnessChange = (key: string) => {
|
||||
setSelectedHarness(key)
|
||||
onTabSelected(key)
|
||||
}
|
||||
|
||||
return (
|
||||
<StepHikeCompact title={`Set up ${agent.name}`}>
|
||||
<StepHikeCompact.Step step={1} title="Copy the prompt">
|
||||
<StepHikeCompact.Details title="Copy the prompt" />
|
||||
<StepHikeCompact.Code className="mt-0!">
|
||||
<div
|
||||
id={AGENT_PROMPT_ANCHOR}
|
||||
tabIndex={-1}
|
||||
className="rounded-lg outline-2 outline-offset-4 outline-transparent"
|
||||
>
|
||||
<AiPrompt id={agent.promptId} telemetry={{ source: 'agent_setup' }} />
|
||||
</div>
|
||||
</StepHikeCompact.Code>
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={2} title="Schedule it in your agent">
|
||||
<StepHikeCompact.Details title="Schedule it in your agent" />
|
||||
<StepHikeCompact.Code className="mt-0!">
|
||||
<CodeTabs value={activeHarness} onValueChange={handleHarnessChange}>
|
||||
{harnesses.map((harness) => (
|
||||
<TabPanel
|
||||
key={harness.key}
|
||||
id={harness.key}
|
||||
label={harness.label}
|
||||
icon={<HarnessIcon harness={harness} />}
|
||||
>
|
||||
<SourceFrame
|
||||
notchWidth={DOCS_NOTCH_WIDTH}
|
||||
footer={
|
||||
<a
|
||||
href={harness.docsUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
aria-label={`View ${harness.label} docs`}
|
||||
className={cn(SOURCE_FOOTER_CLASSES, 'pl-2')}
|
||||
>
|
||||
<HarnessIcon harness={harness} />
|
||||
View docs
|
||||
</a>
|
||||
}
|
||||
>
|
||||
<HarnessBody harness={harness} />
|
||||
</SourceFrame>
|
||||
</TabPanel>
|
||||
))}
|
||||
</CodeTabs>
|
||||
</StepHikeCompact.Code>
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
)
|
||||
}
|
||||
|
||||
function HarnessIcon({ harness }: HarnessProps) {
|
||||
const getSrc = (useDarkVariant: boolean) =>
|
||||
getMcpClientIconSrc({
|
||||
icon: harness.icon,
|
||||
useDarkVariant,
|
||||
hasDistinctDarkIcon: harness.hasDistinctDarkIcon,
|
||||
})
|
||||
|
||||
if (!harness.hasDistinctDarkIcon) {
|
||||
return <Image src={getSrc(false)} alt="" width={14} height={14} className="size-3.5 shrink-0" />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src={getSrc(false)}
|
||||
alt=""
|
||||
width={14}
|
||||
height={14}
|
||||
className="size-3.5 shrink-0 dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src={getSrc(true)}
|
||||
alt=""
|
||||
width={14}
|
||||
height={14}
|
||||
className="hidden size-3.5 shrink-0 dark:block"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function HarnessBody({ harness }: HarnessProps) {
|
||||
return (
|
||||
<div className="code-sample-surface rounded-lg border border-default bg-200 px-5 pt-4 pb-10 text-sm [&>:first-child]:mt-0 [&>:last-child]:mb-0">
|
||||
<p>{harness.intro}</p>
|
||||
<ol>
|
||||
{harness.steps.map((step) => (
|
||||
@@ -53,54 +189,7 @@ function HarnessBody({ harness }: { harness: MonitoringAgentHarnessSetup }) {
|
||||
<ReactMarkdown components={markdownComponents}>{harness.note}</ReactMarkdown>
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<a
|
||||
href={harness.docsUrl}
|
||||
className="text-primary hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{harness.label} docs
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentSetup({ id }: AgentSetupProps) {
|
||||
const agent = getMonitoringAgent(id)
|
||||
const harnesses = getMonitoringAgentHarnesses(agent)
|
||||
const { resolvedTheme } = useTheme()
|
||||
const theme = resolvedTheme?.includes('dark') ? 'dark' : 'light'
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultActiveId="prompt"
|
||||
type="underlined"
|
||||
size="small"
|
||||
wrappable
|
||||
queryGroup="agent-setup"
|
||||
>
|
||||
<TabPanel id="prompt" label="Prompt" icon={<Sparkles size={14} />}>
|
||||
<AiPrompt id={agent.promptId} telemetry={{ source: 'agent_setup' }} />
|
||||
</TabPanel>
|
||||
{harnesses.map((harness) => (
|
||||
<TabPanel
|
||||
key={harness.key}
|
||||
id={harness.key}
|
||||
label={harness.label}
|
||||
icon={
|
||||
<ConnectionIcon
|
||||
theme={theme}
|
||||
connection={harness.icon}
|
||||
hasDistinctDarkIcon={harness.hasDistinctDarkIcon}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HarnessBody harness={harness} />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)')
|
||||
|
||||
@@ -9,8 +9,10 @@ type HandlerContext = {
|
||||
props: Record<string, unknown>
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user