diff --git a/packages/shared/doctor.config.json b/packages/shared/doctor.config.json index 6720bd05..fc285b61 100644 --- a/packages/shared/doctor.config.json +++ b/packages/shared/doctor.config.json @@ -3,7 +3,12 @@ "ignore": { "overrides": [ { - "files": ["marketing/components/landing/composable-workbench/**"], + "files": [ + "marketing/components/landing/agent-anatomy/**", + "marketing/components/landing/code-block.tsx", + "marketing/components/landing/landing-primitives.tsx", + "marketing/components/landing/landing-production.tsx" + ], "rules": [ "react-doctor/design-no-em-dash-in-jsx-text", "react-doctor/design-no-redundant-padding-axes", diff --git a/packages/shared/marketing/components/landing-home-page.tsx b/packages/shared/marketing/components/landing-home-page.tsx index 9dbf1045..9bf94a91 100644 --- a/packages/shared/marketing/components/landing-home-page.tsx +++ b/packages/shared/marketing/components/landing-home-page.tsx @@ -1,11 +1,13 @@ 'use client' -import { LandingComposableWorkbench } from '@outname/shared/marketing/components/landing/composable-workbench/landing-composable-workbench' -import { LandingChatShowcase } from '@outname/shared/marketing/components/landing/landing-chat-showcase' +import { LandingAgentAnatomy } from '@outname/shared/marketing/components/landing/agent-anatomy/landing-agent-anatomy' +import { LandingBindings } from '@outname/shared/marketing/components/landing/landing-bindings' +import { LandingFinalCta } from '@outname/shared/marketing/components/landing/landing-final-cta' import { LandingFooter } from '@outname/shared/marketing/components/landing/landing-footer' -import { LandingHeartbeatCloser } from '@outname/shared/marketing/components/landing/landing-heartbeat-closer' import { LandingHeroDemo } from '@outname/shared/marketing/components/landing/landing-hero-demo' import { LandingNav } from '@outname/shared/marketing/components/landing/landing-nav' +import { LandingPrimitives } from '@outname/shared/marketing/components/landing/landing-primitives' +import { LandingProduction } from '@outname/shared/marketing/components/landing/landing-production' import { useReducedMotion } from 'motion/react' export function LandingHomePage() { @@ -16,10 +18,24 @@ export function LandingHomePage() {
+ {/* 1 — Positioning, command, and the directory artifact */} - - - + + {/* 2 — An agent is a directory: overview + alternating file walkthrough */} + + + {/* 3 — What you bind around the folder */} + + + {/* 4 — What it's built on */} + + + {/* 5 — Production, proven live in stacked cards */} + + + {/* 6 — Final call to action */} + +
) diff --git a/packages/shared/marketing/components/landing/agent-anatomy/agent-file-tree.tsx b/packages/shared/marketing/components/landing/agent-anatomy/agent-file-tree.tsx new file mode 100644 index 00000000..6d5ea16b --- /dev/null +++ b/packages/shared/marketing/components/landing/agent-anatomy/agent-file-tree.tsx @@ -0,0 +1,138 @@ +'use client' + +import { + type AgentTreeNode, + type AnatomyStepId, + agentSlug, + agentTree, + type FileOwner, +} from '@outname/shared/marketing/data/agent-anatomy' +import { cn } from '@outname/ui/lib/utils' +import { FileTextIcon, FolderIcon } from 'lucide-react' + +const DEPTH_INDENT_REM = 0.75 + +const ownerDot: Record = { + agent: 'border border-current opacity-50', + shared: 'bg-current opacity-50', + user: 'bg-brand', +} + +function rowClasses(active: boolean, interactive: boolean) { + return cn( + 'ease flex w-full items-center gap-2 border-l-2 py-1.5 pr-3 text-left font-mono text-xs tracking-normal transition-colors duration-150', + active + ? 'border-brand bg-foreground text-background' + : 'border-transparent text-muted-foreground', + interactive && !active && 'hover:bg-muted hover:text-foreground' + ) +} + +function TreeRow({ + active, + node, + onSelectStep, +}: { + active: boolean + node: AgentTreeNode + onSelectStep?: (id: AnatomyStepId) => void +}) { + const Icon = node.kind === 'dir' ? FolderIcon : FileTextIcon + const isStepNode = Boolean(node.stepId) + const paddingLeft = `${node.depth * DEPTH_INDENT_REM + 0.5}rem` + + const inner = ( + <> + + + {node.label} + + {node.owner ? ( + + ) : null} + + ) + + return ( +
  • + {node.stepId && onSelectStep ? ( + + ) : ( + + {inner} + + )} +
  • + ) +} + +export function AgentFileTree({ + activeStepId, + className, + onSelectStep, +}: { + activeStepId?: AnatomyStepId + className?: string + onSelectStep?: (id: AnatomyStepId) => void +}) { + return ( +
    +
    + + + {agentSlug}/ + + + sandbox + +
    +
      + {agentTree.map((node) => ( + + ))} +
    +
    + + you author + + + {' '} + shared + + + {' '} + agent writes + +
    +
    + ) +} diff --git a/packages/shared/marketing/components/landing/agent-anatomy/constants.ts b/packages/shared/marketing/components/landing/agent-anatomy/constants.ts new file mode 100644 index 00000000..c556fdf7 --- /dev/null +++ b/packages/shared/marketing/components/landing/agent-anatomy/constants.ts @@ -0,0 +1,25 @@ +import type { AnatomyStepId } from '@outname/shared/marketing/data/agent-anatomy' +import { + BrainIcon, + CalendarIcon, + ContactIcon, + ListChecksIcon, + type LucideIcon, + MoonIcon, + ScrollTextIcon, + SparklesIcon, + TargetIcon, + UserIcon, +} from 'lucide-react' + +export const stepIcons: Record = { + calendar: CalendarIcon, + dreams: MoonIcon, + goals: TargetIcon, + identity: ContactIcon, + instructions: ScrollTextIcon, + memory: BrainIcon, + soul: SparklesIcon, + tasks: ListChecksIcon, + user: UserIcon, +} diff --git a/packages/shared/marketing/components/landing/agent-anatomy/landing-agent-anatomy.tsx b/packages/shared/marketing/components/landing/agent-anatomy/landing-agent-anatomy.tsx new file mode 100644 index 00000000..510d127b --- /dev/null +++ b/packages/shared/marketing/components/landing/agent-anatomy/landing-agent-anatomy.tsx @@ -0,0 +1,239 @@ +'use client' + +import { CodeLines } from '@outname/shared/marketing/components/landing/code-block' +import { + revealVariants, + staggerVariants, +} from '@outname/shared/marketing/components/landing/landing-motion' +import { + type AnatomyStep, + agentSlug, + anatomySteps, + ownerLabel, +} from '@outname/shared/marketing/data/agent-anatomy' +import { cn } from '@outname/ui/lib/utils' +import { + domAnimation, + LazyMotion, + m as motion, + useMotionValueEvent, + useScroll, +} from 'motion/react' +import { useRef, useState } from 'react' +import { stepIcons } from './constants' + +const ACTIVE_LINE_RATIO = 0.4 +const fileNameByNode: Record = { + calendar: 'CALENDAR.md', + dreams: 'DREAMS.md', + goals: 'GOALS.md', + identity: 'IDENTITY.md', + instructions: 'AGENTS.md', + memory: 'MEMORY.md', + soul: 'SOUL.md', + tasks: 'TASKS.md', + user: 'USER.md', +} + +function fileNameFor(step: AnatomyStep): string { + return fileNameByNode[step.node] ?? step.node +} + +function CodePanel({ step }: { step: AnatomyStep }) { + const Icon = stepIcons[step.id] + const fileName = fileNameFor(step) + + return ( +
    +
    + + + + {agentSlug}/ + + + + {step.index} / {String(anatomySteps.length).padStart(2, '0')} + +
    + +
    +
      + {anatomySteps.map((entry) => { + const isActive = entry.id === step.id + const EntryIcon = stepIcons[entry.id] + return ( +
    • + + + {fileNameFor(entry)} + +
    • + ) + })} +
    + +
    +
    + + + {fileName} + + + {ownerLabel[step.owner]} + +
    +
    + +
    +
    +
    +
    + ) +} + +function StepBlock({ + step, + active, + isLast, + registerRef, +}: { + step: AnatomyStep + active: boolean + isLast: boolean + registerRef: (el: HTMLLIElement | null) => void +}) { + return ( +
  • +
    +
    + + {step.index} + +

    + {step.title} +

    + + {fileNameFor(step)} + +
    +

    + {step.caption} +

    +
    + runs in + + Vercel Sandbox + +
    + +
    + +
    +
    +
  • + ) +} + +export function LandingAgentAnatomy({ + shouldReduceMotion, +}: { + shouldReduceMotion: boolean +}) { + const stepEls = useRef<(HTMLLIElement | null)[]>([]) + const [active, setActive] = useState(0) + const { scrollY } = useScroll() + + useMotionValueEvent(scrollY, 'change', () => { + const targetY = window.innerHeight * ACTIVE_LINE_RATIO + let next = 0 + stepEls.current.forEach((el, index) => { + if (el && el.getBoundingClientRect().top <= targetY) { + next = index + } + }) + setActive((prev) => (prev === next ? prev : next)) + }) + + const activeStep = anatomySteps[active] ?? anatomySteps[0] + + return ( +
    + + + +
    +

    + The mental model +

    +

    + An agent is a directory. +

    +
    +

    + Nine canonical markdown files in a sandbox, each with a job. Some + you author; the rest it keeps current itself. Scroll the folder. +

    +
    + +
    +
      + {anatomySteps.map((step, index) => ( + { + stepEls.current[index] = el + }} + step={step} + /> + ))} +
    + +
    +
    + +
    +
    +
    +
    +
    +
    + ) +} diff --git a/packages/shared/marketing/components/landing/brand-glyph.tsx b/packages/shared/marketing/components/landing/brand-glyph.tsx new file mode 100644 index 00000000..2f4d58b4 --- /dev/null +++ b/packages/shared/marketing/components/landing/brand-glyph.tsx @@ -0,0 +1,122 @@ +import { + SiBetterauth, + SiCaldotcom, + SiGithub, + SiNextdotjs, + SiOpenrouter, + SiPostgresql, + SiPosthog, + SiResend, + SiSupabase, + SiUpstash, + SiV0, + SiVercel, + SiX, +} from '@icons-pack/react-simple-icons' +import { + Context7Icon, + FirecrawlIcon, + ParallelIcon, + SlackIcon, + TypefullyIcon, +} from '@outname/shared/marketing/components/landing/brand-icons' +import { cn } from '@outname/ui/lib/utils' +import { MessagesSquareIcon } from 'lucide-react' +import type { ComponentType } from 'react' + +type GlyphComponent = ComponentType<{ className?: string }> + +// Official monochrome marks. Simple Icons for what it ships (Neon uses the +// Postgres elephant); hand-built faithful marks for the rest. All render in +// currentColor so they stay inside the monochrome + red system. +const ICONS: Record = { + betterauth: SiBetterauth, + calcom: SiCaldotcom, + context7: Context7Icon, + firecrawl: FirecrawlIcon, + github: SiGithub, + neon: SiPostgresql, + nextjs: SiNextdotjs, + openrouter: SiOpenrouter, + parallel: ParallelIcon, + posthog: SiPosthog, + resend: SiResend, + slack: SlackIcon, + supabase: SiSupabase, + typefully: TypefullyIcon, + upstash: SiUpstash, + v0: SiV0, + vercel: SiVercel, + x: SiX, +} + +// Non-brand surfaces get a Lucide glyph instead of a logo. +const LUCIDE: Record = { + inappchat: MessagesSquareIcon, +} + +// Conceptual products with no real mark fall back to a two-letter monogram. +const MONOGRAMS: Record = { + chatsdk: 'Ch', + llmgateway: 'LM', +} + +function brandKey(name: string): string { + const lower = name.toLowerCase() + if (lower.includes('in-app')) { + return 'inappchat' + } + if (lower.includes('vercel')) { + return 'vercel' + } + if (lower.includes('llm')) { + return 'llmgateway' + } + if (lower.includes('openrouter')) { + return 'openrouter' + } + if (lower.includes('next')) { + return 'nextjs' + } + if (lower.includes('neon')) { + return 'neon' + } + if (lower.includes('upstash')) { + return 'upstash' + } + if (lower.includes('better')) { + return 'betterauth' + } + if (lower.includes('chat sdk')) { + return 'chatsdk' + } + return lower.replace(/[^a-z0-9]/g, '') +} + +export function BrandGlyph({ + className, + name, +}: { + className?: string + name: string +}) { + const key = brandKey(name) + const Icon = ICONS[key] ?? LUCIDE[key] + + if (Icon) { + return + } + + const monogram = MONOGRAMS[key] ?? name.slice(0, 2) + return ( + + {monogram} + + ) +} diff --git a/packages/shared/marketing/components/landing/brand-icons.tsx b/packages/shared/marketing/components/landing/brand-icons.tsx new file mode 100644 index 00000000..81f2d29d --- /dev/null +++ b/packages/shared/marketing/components/landing/brand-icons.tsx @@ -0,0 +1,94 @@ +// Brand marks for integrations Simple Icons doesn't ship. Sourced from each +// project's official assets (svgl.app / brand kits) and reduced to a single +// monochrome path set rendered in currentColor, to stay inside the design +// system. Used only to identify the services outname integrates with. + +function SlackIcon({ className }: { className?: string }) { + return ( + + Slack + + + + + + ) +} + +function FirecrawlIcon({ className }: { className?: string }) { + return ( + + Firecrawl + + + ) +} + +function ParallelIcon({ className }: { className?: string }) { + return ( + + Parallel + + + + + + + + + + ) +} + +function TypefullyIcon({ className }: { className?: string }) { + return ( + + Typefully + + + ) +} + +function Context7Icon({ className }: { className?: string }) { + return ( + + Context7 + + + ) +} + +export { Context7Icon, FirecrawlIcon, ParallelIcon, SlackIcon, TypefullyIcon } diff --git a/packages/shared/marketing/components/landing/code-block.tsx b/packages/shared/marketing/components/landing/code-block.tsx new file mode 100644 index 00000000..8d7fa441 --- /dev/null +++ b/packages/shared/marketing/components/landing/code-block.tsx @@ -0,0 +1,76 @@ +import { cn } from '@outname/ui/lib/utils' + +const PARENTHETICAL = /(\([^)]*\))/g + +function lineClass(line: string): string { + const trimmed = line.trimStart() + if (trimmed.startsWith('#')) { + return 'font-semibold text-foreground' + } + if (trimmed.startsWith('+')) { + return 'text-brand' + } + if (trimmed.startsWith('- [x]')) { + return 'text-muted-foreground line-through' + } + if (trimmed.startsWith('└') || trimmed.startsWith('├')) { + return 'text-muted-foreground' + } + return 'text-foreground/80' +} + +function renderInline(line: string) { + if (line.length === 0) { + return ' ' + } + return line.split(PARENTHETICAL).map((part, index) => { + const key = `${index}-${part}` + if (part.startsWith('(') && part.endsWith(')')) { + return ( + + {part} + + ) + } + return {part} + }) +} + +/** Light, chrome-less syntax-highlighted code lines with a faint number gutter. */ +export function CodeLines({ + code, + className, +}: { + code: string + className?: string +}) { + const lines = code.split('\n') + + return ( +
    +      
    +        {lines.map((line, index) => {
    +          const key = index
    +          return (
    +            
    +              
    +                {index + 1}
    +              
    +              
    +                {renderInline(line)}
    +              
    +            
    +          )
    +        })}
    +      
    +    
    + ) +} diff --git a/packages/shared/marketing/components/landing/command-pill.tsx b/packages/shared/marketing/components/landing/command-pill.tsx new file mode 100644 index 00000000..1e516d82 --- /dev/null +++ b/packages/shared/marketing/components/landing/command-pill.tsx @@ -0,0 +1,55 @@ +'use client' + +import { CheckIcon, CopyIcon } from 'lucide-react' +import { useEffect, useState } from 'react' + +const RESET_MS = 1600 + +export function CommandPill({ + command, + copyText, +}: { + command: string + /** What actually lands on the clipboard; defaults to the shown command. */ + copyText?: string +}) { + const [copied, setCopied] = useState(false) + + useEffect(() => { + if (!copied) { + return + } + const timer = setTimeout(() => setCopied(false), RESET_MS) + return () => clearTimeout(timer) + }, [copied]) + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(copyText ?? command) + setCopied(true) + } catch { + // Clipboard unavailable (e.g. insecure context); leave state unchanged. + } + } + + return ( +
    + $ + + {command} + + +
    + ) +} diff --git a/packages/shared/marketing/components/landing/composable-workbench/agent-shell-card.tsx b/packages/shared/marketing/components/landing/composable-workbench/agent-shell-card.tsx deleted file mode 100644 index 5ba3aec7..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/agent-shell-card.tsx +++ /dev/null @@ -1,118 +0,0 @@ -'use client' - -import { composabilityStages } from '@outname/shared/marketing/data/composability-demo' -import { Badge } from '@outname/ui/components/ui/badge' -import { cn } from '@outname/ui/lib/utils' -import { stageIcons } from './constants' - -export function AgentShellCard({ - compact = false, - slotCounts, -}: { - compact?: boolean - slotCounts: readonly number[] -}) { - const allFilled = - slotCounts.length === composabilityStages.length && - slotCounts.every( - (count, idx) => count === composabilityStages[idx].parts.length - ) - - return ( -
    -
    -
    -
    -

    - Agent -

    -

    - INBOX SENTINEL -

    -
    - - {allFilled ? 'composed' : 'incomplete'} - -
    - -
    - {composabilityStages.map((stage, idx) => { - const count = slotCounts[idx] ?? 0 - const total = stage.parts.length - const filled = count >= total - const Icon = stageIcons[stage.id] - return ( -
    - -

    - {stage.label} -

    -

    - {count} / {total} -

    -
    - ) - })} -
    - -

    - {allFilled - ? 'Eight parts. One agent. Yours.' - : 'Waiting for parts to attach…'} -

    -
    -
    - ) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/assembly-visual.tsx b/packages/shared/marketing/components/landing/composable-workbench/assembly-visual.tsx deleted file mode 100644 index 2d18e1e5..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/assembly-visual.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client' - -import { composabilityStages } from '@outname/shared/marketing/data/composability-demo' -import { AgentShellCard } from './agent-shell-card' -import { FlyingChip } from './flying-chip' -import { partSnapProgress } from './utils' - -export function AssemblyVisual({ - progress, - slotCounts, -}: { - progress: number - slotCounts: readonly number[] -}) { - return ( -
    -
    - {composabilityStages.map((stage) => - stage.parts.map((part, partIndex) => ( - - )) - )} -
    - -
    - -
    -
    - ) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/caption-rail.tsx b/packages/shared/marketing/components/landing/composable-workbench/caption-rail.tsx deleted file mode 100644 index c7db69c2..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/caption-rail.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client' - -import { composabilityStages } from '@outname/shared/marketing/data/composability-demo' -import { cn } from '@outname/ui/lib/utils' -import { type MotionValue, m as motion } from 'motion/react' -import { stageIcons, totalParts } from './constants' - -export function CaptionRail({ - activeIndex, - progressScaleX, - slotCounts, -}: { - activeIndex: number - progressScaleX: MotionValue - slotCounts: readonly number[] -}) { - const attached = slotCounts.reduce((sum, count) => sum + count, 0) - const activeStage = composabilityStages[activeIndex] - const Icon = stageIcons[activeStage.id] - - return ( - - ) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/composability-mobile-story.tsx b/packages/shared/marketing/components/landing/composable-workbench/composability-mobile-story.tsx deleted file mode 100644 index bf3a8274..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/composability-mobile-story.tsx +++ /dev/null @@ -1,126 +0,0 @@ -'use client' - -import { composabilityStages } from '@outname/shared/marketing/data/composability-demo' -import { Badge } from '@outname/ui/components/ui/badge' -import { cn } from '@outname/ui/lib/utils' -import { m as motion } from 'motion/react' -import { useRef } from 'react' -import { AgentShellCard } from './agent-shell-card' -import { - mobileMarkerTone, - stageCount, - stageSlotCounts, - totalParts, -} from './constants' -import { MobileStageCard } from './mobile-stage-card' -import { MobileStageFlight } from './mobile-stage-flight' -import { useElementSize } from './use-element-size' -import { useMobileStageActiveIndex } from './use-mobile-stage-active-index' - -export function ComposabilityMobileStory() { - const stageRefs = useRef>([]) - const visualRef = useRef(null) - const stickyRef = useRef(null) - const visualSize = useElementSize(visualRef) - const activeIndex = useMobileStageActiveIndex(stageRefs) - const activeStage = composabilityStages[activeIndex] ?? composabilityStages[0] - const slotCounts = stageSlotCounts(activeIndex) - const attached = slotCounts.reduce((sum, count) => sum + count, 0) - - const size = visualSize ?? { width: 320, height: 272 } - - return ( -
    -
    -
    -
    -
    -
    -

    - {activeStage.eyebrow} -

    -

    - Scroll to compose -

    -
    - - {attached} / {totalParts} attached - -
    - -
    -
    -
    - -
    -
    - -
    -
    -
    - -
    - {composabilityStages.map((stage, index) => { - const isActive = index === activeIndex - const isAttached = index < activeIndex - - return ( -
    - - {String(index + 1).padStart(2, '0')} - - - {stage.label} - -
    - ) - })} -
    - -
    - -
    - -

    - {activeStage.label} -

    -

    - {activeStage.caption} -

    -
    -
    - -
    - {composabilityStages.map((stage, index) => ( - { - stageRefs.current[index] = node - }} - stage={stage} - stageIndex={index} - stickyRef={stickyRef} - /> - ))} -
    -
    -
    - ) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/composability-pinned.tsx b/packages/shared/marketing/components/landing/composable-workbench/composability-pinned.tsx deleted file mode 100644 index eaf3ab58..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/composability-pinned.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client' - -import { composabilityStages } from '@outname/shared/marketing/data/composability-demo' -import { - useMotionValue, - useMotionValueEvent, - useScroll, - useTransform, -} from 'motion/react' -import { useRef, useState } from 'react' -import { AssemblyVisual } from './assembly-visual' -import { CaptionRail } from './caption-rail' -import { stageCount } from './constants' -import { partSnapProgress } from './utils' - -export function ComposabilityPinned() { - const scrollTargetRef = useRef(null) - const sectionProgress = useMotionValue(0) - const [activeIndex, setActiveIndex] = useState(0) - const [progressSnapshot, setProgressSnapshot] = useState(0) - const progressScaleX = useTransform(sectionProgress, [0, 1], [0, 1]) - const { scrollY } = useScroll() - - useMotionValueEvent(scrollY, 'change', () => { - const target = scrollTargetRef.current - if (!target) { - return - } - const rect = target.getBoundingClientRect() - const scrollableDistance = Math.max(1, rect.height - window.innerHeight) - const next = Math.min(1, Math.max(0, -rect.top / scrollableDistance)) - sectionProgress.set(next) - setProgressSnapshot(next) - const nextIndex = Math.min( - stageCount - 1, - Math.max(0, Math.floor(next * stageCount)) - ) - setActiveIndex((currentIndex) => - currentIndex === nextIndex ? currentIndex : nextIndex - ) - }) - - const slotCounts = composabilityStages.map((stage) => - stage.parts.reduce( - (count, part) => - partSnapProgress(part.id, progressSnapshot) >= 0.92 ? count + 1 : count, - 0 - ) - ) - - return ( -
    -
    -
    - - -
    -
    -
    - ) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/composability-stacked.tsx b/packages/shared/marketing/components/landing/composable-workbench/composability-stacked.tsx deleted file mode 100644 index 81763013..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/composability-stacked.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client' - -import { composabilityStages } from '@outname/shared/marketing/data/composability-demo' -import { cornerLabels, stageIcons } from './constants' - -export function ComposabilityStacked() { - return ( -
    - {composabilityStages.map((stage) => { - const Icon = stageIcons[stage.id] - return ( -
    -
    -
    -
    -

    - {stage.eyebrow} -

    -

    - {stage.label} -

    -
    - - - -
    - -

    - {stage.caption} -

    - -
    - {stage.parts.map((part) => ( - - {part.label} - - ))} -
    - -

    - Attaches to {cornerLabels[stage.corner]} slot of the agent - shell. -

    -
    -
    - ) - })} -
    - ) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/constants.ts b/packages/shared/marketing/components/landing/composable-workbench/constants.ts deleted file mode 100644 index 7ef69d73..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/constants.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - type Corner, - composabilityStages, - type StageId, -} from '@outname/shared/marketing/data/composability-demo' -import { - BrainIcon, - GitBranchIcon, - HammerIcon, - RadioTowerIcon, -} from 'lucide-react' - -export const stageIcons: Record = { - channels: RadioTowerIcon, - memory: BrainIcon, - subagents: GitBranchIcon, - tools: HammerIcon, -} - -export const cornerStart: Record = { - ne: { left: 86, top: 14 }, - nw: { left: 14, top: 14 }, - se: { left: 86, top: 86 }, - sw: { left: 14, top: 86 }, -} - -export const centerTarget = { left: 50, top: 50 } - -export const cornerLabels: Record = { - ne: 'top-right', - nw: 'top-left', - se: 'bottom-right', - sw: 'bottom-left', -} - -export const stageCount = composabilityStages.length -export const totalParts = composabilityStages.reduce( - (sum, stage) => sum + stage.parts.length, - 0 -) - -interface PartProgressMeta { - partIndex: number - stageEnd: number - stageIndex: number - stageStart: number -} - -export const partProgressMeta = new Map() -composabilityStages.forEach((stage, stageIndex) => { - const stageSlice = 1 / stageCount - const stageStart = stageIndex * stageSlice - stage.parts.forEach((part, partIndex) => { - const partSlice = stageSlice / stage.parts.length - const start = stageStart + partIndex * partSlice - const end = start + partSlice - partProgressMeta.set(part.id, { - partIndex, - stageEnd: end, - stageIndex, - stageStart: start, - }) - }) -}) - -export function stageSlotCounts(activeIndex: number) { - return composabilityStages.map((stage, index) => - index <= activeIndex ? stage.parts.length : 0 - ) -} - -export function mostVisibleStageIndex( - stageVisibility: ReadonlyMap -) { - let nextIndex = 0 - let bestRatio = 0 - - for (const [index, ratio] of stageVisibility.entries()) { - if (ratio > bestRatio) { - bestRatio = ratio - nextIndex = index - } - } - - return bestRatio > 0 ? nextIndex : null -} - -export function mobileMarkerTone(isActive: boolean, isAttached: boolean) { - if (isActive) { - return 'bg-foreground text-background' - } - if (isAttached) { - return 'bg-brand/35' - } - return 'bg-background' -} - -export function mobileStageSurfaceTone(active: boolean, attached: boolean) { - if (active) { - return 'bg-brand/25' - } - if (attached) { - return 'bg-muted' - } - return 'bg-background' -} - -export function mobileStageStatus(active: boolean, attached: boolean) { - if (active) { - return 'Attaching now' - } - if (attached) { - return 'Attached' - } - return 'Queued' -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/flying-chip.tsx b/packages/shared/marketing/components/landing/composable-workbench/flying-chip.tsx deleted file mode 100644 index 01df74e5..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/flying-chip.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client' - -import type { - ComposabilityPart, - Corner, -} from '@outname/shared/marketing/data/composability-demo' -import { cn } from '@outname/ui/lib/utils' -import { centerTarget, cornerStart } from './constants' - -export function FlyingChip({ - corner, - indexInCluster, - part, - snap, - stageColor, - total, -}: { - corner: Corner - indexInCluster: number - part: ComposabilityPart - snap: number - stageColor: 'accent' | 'background' - total: number -}) { - const start = cornerStart[corner] - // Spread chips inside same cluster along the corner-to-center axis so they - // don't stack on top of each other at the corner. Offset is a percentage of - // the container, scaled down as chips fly in (so they converge cleanly). - const spread = total > 1 ? indexInCluster - (total - 1) / 2 : 0 - const axisX = corner === 'ne' || corner === 'se' ? -1 : 1 - const axisY = corner === 'sw' || corner === 'se' ? -1 : 1 - const startLeft = start.left + spread * 6 * axisX - const startTop = start.top + spread * 6 * axisY - - const left = startLeft + (centerTarget.left - startLeft) * snap - const top = startTop + (centerTarget.top - startTop) * snap - const scale = 1 - snap * 0.35 - const isVisible = snap < 0.92 - - return ( - - {part.label} - - ) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/landing-composable-workbench.tsx b/packages/shared/marketing/components/landing/composable-workbench/landing-composable-workbench.tsx deleted file mode 100644 index 4c13b0bc..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/landing-composable-workbench.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client' - -import { - revealVariants, - staggerVariants, -} from '@outname/shared/marketing/components/landing/landing-motion' -import { domAnimation, LazyMotion, m as motion } from 'motion/react' -import { ComposabilityMobileStory } from './composability-mobile-story' -import { ComposabilityPinned } from './composability-pinned' -import { ComposabilityStacked } from './composability-stacked' -import { useIsDesktopViewport } from './use-is-desktop-viewport' - -export function LandingComposableWorkbench({ - shouldReduceMotion, -}: { - shouldReduceMotion: boolean -}) { - const isDesktop = useIsDesktopViewport() - - return ( -
    - - - -
    -

    - Anatomy of an agent -

    -

    - An agent is what you attach to it. -

    -
    -

    - The agent is a shell. Capabilities snap into named slots. You see - what's attached, what ran, what changed. -

    -
    -
    - - {(() => { - if (shouldReduceMotion || isDesktop === undefined) { - return - } - return isDesktop ? ( - - ) : ( - - ) - })()} -
    -
    - ) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/mobile-stage-card.tsx b/packages/shared/marketing/components/landing/composable-workbench/mobile-stage-card.tsx deleted file mode 100644 index 2cb67c3e..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/mobile-stage-card.tsx +++ /dev/null @@ -1,122 +0,0 @@ -'use client' - -import type { ComposabilityStage } from '@outname/shared/marketing/data/composability-demo' -import { cn } from '@outname/ui/lib/utils' -import { - m as motion, - useMotionValue, - useMotionValueEvent, - useScroll, -} from 'motion/react' -import { type RefObject, useRef } from 'react' -import { - cornerLabels, - mobileStageStatus, - mobileStageSurfaceTone, - stageIcons, -} from './constants' -import { clamp01 } from './utils' - -export function MobileStageCard({ - active, - attached, - setRef, - stage, - stageIndex, - stickyRef, -}: { - active: boolean - attached: boolean - setRef: (node: HTMLElement | null) => void - stage: ComposabilityStage - stageIndex: number - stickyRef: RefObject -}) { - const Icon = stageIcons[stage.id] - const articleRef = useRef(null) - const opacity = useMotionValue(1) - const scale = useMotionValue(1) - const { scrollY } = useScroll() - - useMotionValueEvent(scrollY, 'change', () => { - const node = articleRef.current - const sticky = stickyRef.current - if (!(node && sticky)) { - return - } - const cardTop = node.getBoundingClientRect().top - const stickyBottom = sticky.getBoundingClientRect().bottom - const fadeStart = -180 - const fadeEnd = -60 - const diff = cardTop - stickyBottom - const next = clamp01((diff - fadeStart) / (fadeEnd - fadeStart)) - opacity.set(next) - scale.set(0.94 + next * 0.06) - }) - - const assignRef = (node: HTMLElement | null) => { - articleRef.current = node - setRef(node) - } - - return ( - -
    -
    -
    -

    {stage.eyebrow}

    -

    - {stage.label} -

    -
    -
    - - - - - {mobileStageStatus(active, attached)} - -
    -
    - -

    - {stage.caption} -

    - -
    - {stage.parts.map((part) => ( - - {part.label} - - ))} -
    - -

    - Attaches to {cornerLabels[stage.corner]} slot of the agent shell. -

    -
    -
    - ) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/mobile-stage-flight.tsx b/packages/shared/marketing/components/landing/composable-workbench/mobile-stage-flight.tsx deleted file mode 100644 index 2b9cd436..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/mobile-stage-flight.tsx +++ /dev/null @@ -1,80 +0,0 @@ -'use client' - -import { composabilityStages } from '@outname/shared/marketing/data/composability-demo' -import { cn } from '@outname/ui/lib/utils' -import { m as motion } from 'motion/react' -import { centerTarget, cornerStart } from './constants' -import type { ElementSize } from './use-element-size' - -export function MobileStageFlight({ - activeIndex, - size, -}: { - activeIndex: number - size: ElementSize -}) { - const activeStage = composabilityStages[activeIndex] ?? composabilityStages[0] - const width = size.width || 320 - const height = size.height || 272 - - return ( -
    - {activeStage.parts.map((part, partIndex) => { - const start = cornerStart[activeStage.corner] - const spread = - activeStage.parts.length > 1 - ? partIndex - (activeStage.parts.length - 1) / 2 - : 0 - const axisX = - activeStage.corner === 'ne' || activeStage.corner === 'se' ? -1 : 1 - const axisY = - activeStage.corner === 'sw' || activeStage.corner === 'se' ? -1 : 1 - const startLeft = start.left + spread * 6 * axisX - const startTop = start.top + spread * 6 * axisY - const deltaX = ((centerTarget.left - startLeft) / 100) * width - const deltaY = ((centerTarget.top - startTop) / 100) * height - const stageColor = activeStage.id === 'memory' ? 'accent' : 'background' - - return ( - - - - {part.label} - - - - ) - })} -
    - ) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/use-element-size.ts b/packages/shared/marketing/components/landing/composable-workbench/use-element-size.ts deleted file mode 100644 index 7b60bd29..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/use-element-size.ts +++ /dev/null @@ -1,45 +0,0 @@ -'use client' - -import { type RefObject, useLayoutEffect, useState } from 'react' - -export interface ElementSize { - height: number - width: number -} - -export function useElementSize( - ref: RefObject -): ElementSize | null { - const [size, setSize] = useState(null) - - useLayoutEffect(() => { - const element = ref.current - if (!element) { - return - } - - const readSize = () => { - const width = element.clientWidth - const height = element.clientHeight - if (width <= 0 && height <= 0) { - return - } - setSize({ width: width || 320, height: height || 272 }) - } - - readSize() - - if (typeof ResizeObserver === 'undefined') { - return - } - - const observer = new ResizeObserver(() => { - readSize() - }) - observer.observe(element) - - return () => observer.disconnect() - }, [ref]) - - return size -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/use-is-desktop-viewport.ts b/packages/shared/marketing/components/landing/composable-workbench/use-is-desktop-viewport.ts deleted file mode 100644 index 220a74b6..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/use-is-desktop-viewport.ts +++ /dev/null @@ -1,27 +0,0 @@ -'use client' - -import { useSyncExternalStore } from 'react' - -const LG_BREAKPOINT_PX = 1024 - -function subscribeDesktopViewport(onStoreChange: () => void) { - const mql = window.matchMedia(`(min-width: ${LG_BREAKPOINT_PX}px)`) - mql.addEventListener('change', onStoreChange) - return () => mql.removeEventListener('change', onStoreChange) -} - -function readDesktopViewport() { - return window.matchMedia(`(min-width: ${LG_BREAKPOINT_PX}px)`).matches -} - -function getServerDesktopViewport() { - return false -} - -export function useIsDesktopViewport() { - return useSyncExternalStore( - subscribeDesktopViewport, - readDesktopViewport, - getServerDesktopViewport - ) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/use-mobile-stage-active-index.ts b/packages/shared/marketing/components/landing/composable-workbench/use-mobile-stage-active-index.ts deleted file mode 100644 index 2172f89b..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/use-mobile-stage-active-index.ts +++ /dev/null @@ -1,80 +0,0 @@ -'use client' - -import { type RefObject, useEffect, useRef, useSyncExternalStore } from 'react' -import { mostVisibleStageIndex } from './constants' - -type Listener = () => void - -function createStageIndexStore(initialIndex: number) { - let index = initialIndex - const listeners = new Set() - - return { - getIndex: () => index, - setIndex: (next: number) => { - if (index === next) { - return - } - index = next - for (const listener of listeners) { - listener() - } - }, - subscribe: (listener: Listener) => { - listeners.add(listener) - return () => { - listeners.delete(listener) - } - }, - } -} - -export function useMobileStageActiveIndex( - stageRefs: RefObject> -) { - const storeRef = useRef(createStageIndexStore(0)) - const store = storeRef.current - - useEffect(() => { - if (typeof IntersectionObserver === 'undefined') { - return - } - - const stageVisibility = new Map() - const observer = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - const index = Number((entry.target as HTMLElement).dataset.stageIndex) - if (Number.isNaN(index)) { - continue - } - stageVisibility.set( - index, - entry.isIntersecting ? entry.intersectionRatio : 0 - ) - } - - const nextIndex = mostVisibleStageIndex(stageVisibility) - if (nextIndex !== null) { - store.setIndex(nextIndex) - } - }, - { - rootMargin: '-42% 0px -28% 0px', - threshold: [0.25, 0.4, 0.55, 0.7, 0.85], - } - ) - - for (const [index, node] of stageRefs.current.entries()) { - if (!node) { - continue - } - stageVisibility.set(index, 0) - observer.observe(node) - } - - return () => observer.disconnect() - }, [stageRefs, store]) - - return useSyncExternalStore(store.subscribe, store.getIndex, () => 0) -} diff --git a/packages/shared/marketing/components/landing/composable-workbench/utils.ts b/packages/shared/marketing/components/landing/composable-workbench/utils.ts deleted file mode 100644 index 0e58559d..00000000 --- a/packages/shared/marketing/components/landing/composable-workbench/utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { partProgressMeta } from './constants' - -export function clamp01(value: number) { - if (value < 0) { - return 0 - } - if (value > 1) { - return 1 - } - return value -} - -export function partSnapProgress(partId: string, sectionProgress: number) { - const meta = partProgressMeta.get(partId) - if (!meta) { - return 0 - } - return clamp01( - (sectionProgress - meta.stageStart) / (meta.stageEnd - meta.stageStart) - ) -} diff --git a/packages/shared/marketing/components/landing/hero-artifact.tsx b/packages/shared/marketing/components/landing/hero-artifact.tsx new file mode 100644 index 00000000..7c01bc7b --- /dev/null +++ b/packages/shared/marketing/components/landing/hero-artifact.tsx @@ -0,0 +1,95 @@ +'use client' + +import { AgentFileTree } from '@outname/shared/marketing/components/landing/agent-anatomy/agent-file-tree' +import { CodeLines } from '@outname/shared/marketing/components/landing/code-block' +import { cn } from '@outname/ui/lib/utils' +import { FileTextIcon } from 'lucide-react' +import { useState } from 'react' + +type ArtifactView = 'human' | 'agent' + +// A real excerpt of the AGENTS.md the runtime seeds — the first thing the +// agent reads on every event. +const AGENT_VIEW_CODE = `# AGENTS.md +Your operational manual. Read it at +the start of every event. + +## Conventions +- Dates are ISO-8601. +- Append one bullet to today's log. +- Terse output; bullets over prose. + +## User custom instructions +- Triage the #ops Slack channel by 09:00. +- Never send external email without a + confirm. +- Keep replies short; prefer "Tomas".` + +const views: readonly { id: ArtifactView; label: string }[] = [ + { id: 'human', label: 'For you' }, + { id: 'agent', label: 'For the agent' }, +] + +export function HeroArtifact() { + const [view, setView] = useState('human') + + return ( +
    +
    + {views.map((option) => { + const active = view === option.id + return ( + + ) + })} +
    + + {/* Both views share one grid cell so the panel keeps the taller height + and switching tabs never shifts the layout. */} +
    +
    + +
    +
    +
    + + AGENTS.md +
    +
    + +
    +
    +
    +
    + ) +} diff --git a/packages/shared/marketing/components/landing/landing-bindings.tsx b/packages/shared/marketing/components/landing/landing-bindings.tsx new file mode 100644 index 00000000..d214c8a4 --- /dev/null +++ b/packages/shared/marketing/components/landing/landing-bindings.tsx @@ -0,0 +1,183 @@ +'use client' + +import { BrandGlyph } from '@outname/shared/marketing/components/landing/brand-glyph' +import { + revealVariants, + staggerVariants, +} from '@outname/shared/marketing/components/landing/landing-motion' +import { + CpuIcon, + GitBranchIcon, + HammerIcon, + HeartPulseIcon, + type LucideIcon, + MoonIcon, + PuzzleIcon, + RadioTowerIcon, + WalletIcon, +} from 'lucide-react' +import { domAnimation, LazyMotion, m as motion } from 'motion/react' + +interface Binding { + // When true, each chip is a real brand and gets its logo glyph. + brandChips?: boolean + chips: readonly string[] + icon: LucideIcon + id: string + text: string + title: string +} + +// Every value below is real: providers and models from the inference layer, +// connectors from the connection registry, schedule modes from the agent form, +// budget periods from the budget schema. +const bindings: readonly Binding[] = [ + { + brandChips: true, + chips: ['Vercel AI Gateway', 'LLM Gateway', 'OpenRouter'], + icon: CpuIcon, + id: 'model', + text: 'Choose the inference provider and model per agent. The runtime stays model-agnostic, so you can switch without touching the files.', + title: 'Model', + }, + { + chips: ['every 30 min', 'or daily at set times'], + icon: HeartPulseIcon, + id: 'heartbeat', + text: 'Wake it on an interval (5 min to daily) or at specific times. Each run does one small useful unit of work.', + title: 'Heartbeat', + }, + { + chips: ['once per day'], + icon: MoonIcon, + id: 'dreaming', + text: 'A separate reflection pass that reviews recent logs and sharpens long-running memory, even when heartbeat work is off.', + title: 'Dreaming', + }, + { + chips: ['daily', 'weekly', 'monthly'], + icon: WalletIcon, + id: 'budget', + text: 'Set a spend ceiling in USD per agent or across all of them, with estimated and actual cost tracked per run.', + title: 'Budget', + }, + { + chips: ['in-app chat', 'Slack'], + icon: RadioTowerIcon, + id: 'channels', + text: 'Bind the channels it listens and speaks on. The same agent answers in-app and in your Slack workspace.', + title: 'Channels', + }, + { + brandChips: true, + chips: ['GitHub', 'Cal.com', 'Resend', 'Firecrawl', '+8'], + icon: HammerIcon, + id: 'tools', + text: 'Attach maintainer tools backed by real connections. The agent only ever calls what you bind to it.', + title: 'Tools', + }, + { + chips: ['attach an agent as a tool'], + icon: GitBranchIcon, + id: 'subagents', + text: 'Bind another agent as a callable tool. The parent delegates work and gets a traced run back inline.', + title: 'Sub-agents', + }, + { + chips: ['from GitHub', 'or a SKILL.md'], + icon: PuzzleIcon, + id: 'skills', + text: 'Install capability packages that run in a dedicated, persistent Skill Sandbox, isolated from the memory files.', + title: 'Skills', + }, +] + +export function LandingBindings({ + shouldReduceMotion, +}: { + shouldReduceMotion: boolean +}) { + return ( +
    + + + +
    +

    + What you bind to it +

    +

    + The files are the agent. These are its powers. +

    +
    +

    + Around that folder you wire up the model it thinks with, when it + wakes, what it can touch, and what it may spend. Every binding is + a setting, not a guess. +

    +
    + + + {bindings.map((binding) => { + const Icon = binding.icon + return ( + +
    + + + +

    + {binding.title} +

    +
    +

    + {binding.text} +

    +
    + {binding.chips.map((chip) => { + const showGlyph = + binding.brandChips && !chip.startsWith('+') + return ( + + {showGlyph ? ( + + ) : null} + {chip} + + ) + })} +
    +
    + ) + })} +
    +
    +
    +
    + ) +} diff --git a/packages/shared/marketing/components/landing/landing-chat-showcase.tsx b/packages/shared/marketing/components/landing/landing-chat-showcase.tsx deleted file mode 100644 index 5003b0f1..00000000 --- a/packages/shared/marketing/components/landing/landing-chat-showcase.tsx +++ /dev/null @@ -1,756 +0,0 @@ -'use client' - -import type { - AgentChatMessage, - WorkflowStatusData, -} from '@outname/ai/agent-runtime/server/chat-status' -import { AgentChatTranscript } from '@outname/ai/chat/components/agent-chat-transcript' -import { - PromptInput, - PromptInputFooter, - PromptInputSubmit, - PromptInputTextarea, -} from '@outname/ai/components/ai-elements/prompt-input' -import { - revealVariants, - staggerVariants, -} from '@outname/shared/marketing/components/landing/landing-motion' -import { Button } from '@outname/ui/components/ui/button' -import { cn } from '@outname/ui/lib/utils' -import { BotIcon, ChevronRightIcon, CircleIcon } from 'lucide-react' -import { - domAnimation, - LazyMotion, - m as motion, - useReducedMotion, -} from 'motion/react' -import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react' - -interface ChatShowcaseScenario { - description: string - id: 'triage' | 'research' | 'digest' - messages: AgentChatMessage[] - model: string - prompt: string - relativeTime: string - status: WorkflowStatusData - title: string -} - -const agentMeta = { - attached: '3 tools · 1 sub-agent · 2 channels', - defaultModel: 'claude-sonnet-4-6', - name: 'INBOX SENTINEL', -} - -const showcaseScenarios: readonly ChatShowcaseScenario[] = [ - { - description: - 'Reads Slack + email overnight, drafts replies, surfaces calendar conflicts to confirm.', - id: 'triage', - model: agentMeta.defaultModel, - prompt: 'Check the morning queue and prep what needs my attention.', - relativeTime: '2m ago', - status: { - message: 'Resolving attached tools…', - phase: 'agent-event', - timestamp: '2026-05-13T07:30:00.000Z', - }, - title: 'Morning triage', - messages: [ - { - id: 'triage-user', - parts: [ - { - text: 'Check the morning queue and prep what needs my attention.', - type: 'text', - }, - ], - role: 'user', - }, - { - id: 'triage-assistant', - parts: [ - { - state: 'done', - text: 'I should scan Slack first since the team posts overnight, then cross-check the calendar for any conflict before 10:00. I will hold off on email until both signals are in.', - type: 'reasoning', - }, - { - input: { - channel: '#ops', - includeThreads: true, - since: 'today 06:00', - }, - output: { - channel: '#ops', - flagged: 2, - threads: 14, - }, - state: 'output-available', - toolCallId: 'triage-slack', - toolName: 'slack.search_threads', - type: 'dynamic-tool', - }, - { - input: { - from: '2026-05-13T09:00', - to: '2026-05-13T18:00', - }, - output: { - conflicts: [ - { - at: 'tue 15:00', - suggested: 'wed 10:00', - with: 'Design review', - }, - ], - }, - state: 'output-available', - toolCallId: 'triage-cal', - toolName: 'cal.find_conflicts', - type: 'dynamic-tool', - }, - { - text: 'Three items need you. Two flagged Slack threads in #ops (engineering blocker + customer reply). One calendar conflict at 15:00 with Design review — I drafted a reschedule to wed 10:00, awaiting your confirm.', - type: 'text', - }, - ], - role: 'assistant', - }, - ], - }, - { - description: - 'Delegates synthesis to a specialist sub-agent. Parent waits and surfaces the child trace inline.', - id: 'research', - model: agentMeta.defaultModel, - prompt: "Compare today's product feedback with last week's notes.", - relativeTime: '14m ago', - status: { - message: 'Calling sub-agent: research-synthesizer', - phase: 'agent-stream', - timestamp: '2026-05-13T07:16:00.000Z', - }, - title: 'Research relay', - messages: [ - { - id: 'research-user', - parts: [ - { - text: "Compare today's product feedback with last week's notes.", - type: 'text', - }, - ], - role: 'user', - }, - { - id: 'research-assistant', - parts: [ - { - state: 'done', - text: 'This is comparative work across two sources — better to hand to research-synthesizer than do it inline. I will pass both windows and let it return a structured summary.', - type: 'reasoning', - }, - { - input: { - agent: 'research-synthesizer', - task: 'Compare today vs last week. Surface recurring asks and sentiment shifts.', - windows: ['2026-05-13', '2026-05-06..2026-05-12'], - }, - output: { - childAgentId: 'research-synthesizer', - childName: 'research-synthesizer', - finalText: - 'Recurring asks: composability (8/12 mentions, up from 5/12) and audit trails (4/12, flat). Sentiment up on private deployment, down on onboarding length.', - kind: 'sub_agent', - messages: [ - { - id: 'sub-research-1', - parts: [ - { - text: 'Loading both feedback windows from the memory mirror.', - type: 'text', - }, - ], - role: 'assistant', - }, - { - id: 'sub-research-2', - parts: [ - { - input: { window: '2026-05-06..2026-05-12' }, - output: { entries: 12, sentiment: 'mixed' }, - state: 'output-available', - toolCallId: 'sub-research-mem', - toolName: 'memory.read', - type: 'dynamic-tool', - }, - { - text: 'Comparison done. 8/12 mentions point to composability — up from 5/12 last week.', - type: 'text', - }, - ], - role: 'assistant', - }, - ], - status: 'completed', - toolName: 'agent_research_synthesizer', - }, - state: 'output-available', - toolCallId: 'research-subagent', - toolName: 'agent_research_synthesizer', - type: 'dynamic-tool', - }, - { - text: 'Composability is the strongest repeated request this week (8 of 12 mentions, up from 5). Sentiment is up on private deployment and down on onboarding length. Full trace in the sub-agent step above.', - type: 'text', - }, - ], - role: 'assistant', - }, - ], - }, - { - description: - 'Heartbeat-fired weekly run. Appends to memory, drafts the digest, sends it. No human prompt.', - id: 'digest', - model: agentMeta.defaultModel, - prompt: '(heartbeat) Prepare and send the weekly digest.', - relativeTime: 'yesterday', - status: { - message: 'Saving sandbox memory…', - phase: 'agent-stream', - timestamp: '2026-05-12T14:00:00.000Z', - }, - title: 'Weekly digest', - messages: [ - { - id: 'digest-user', - parts: [ - { - text: '(heartbeat) Prepare and send the weekly digest.', - type: 'text', - }, - ], - role: 'user', - }, - { - id: 'digest-assistant', - parts: [ - { - state: 'done', - text: 'No human in the loop right now — heartbeat fired this run. I will append three lines to DREAMS.md, draft the digest email, and send it to myself.', - type: 'reasoning', - }, - { - input: { - append: - '+ Follow up on channel bindings\n+ Validate webhook intake design\n+ Reduce onboarding to 3 steps', - file: 'DREAMS.md', - }, - output: { - appended: 3, - file: 'DREAMS.md', - status: 'committed', - }, - state: 'output-available', - toolCallId: 'digest-mem', - toolName: 'memory.append', - type: 'dynamic-tool', - }, - { - input: { - subject: 'Week of May 4 — composability is the recurring ask', - to: 'self', - }, - output: { - draftId: 'd_8f12', - status: 'drafted', - }, - state: 'output-available', - toolCallId: 'digest-gmail', - toolName: 'gmail.draft', - type: 'dynamic-tool', - }, - { - input: { - draftId: 'd_8f12', - }, - output: { - messageId: 'm_a01c', - status: 'sent', - }, - state: 'output-available', - toolCallId: 'digest-send', - toolName: 'gmail.send', - type: 'dynamic-tool', - }, - { - text: 'Digest sent (m_a01c). Three lines appended to DREAMS.md. Ready for you in the morning.', - type: 'text', - }, - ], - role: 'assistant', - }, - ], - }, -] - -interface Frame { - delayBefore: number - messages: AgentChatMessage[] - workflowStatus: WorkflowStatusData | null -} - -function cloneMessages(messages: AgentChatMessage[]): AgentChatMessage[] { - return messages.map((m) => ({ ...m, parts: m.parts.map((p) => ({ ...p })) })) -} - -function buildFrames(scenario: ChatShowcaseScenario): Frame[] { - const frames: Frame[] = [] - const currentMessages: AgentChatMessage[] = [] - - for (const message of scenario.messages) { - if (message.role === 'user') { - currentMessages.push({ ...message, parts: [...message.parts] }) - frames.push({ - delayBefore: frames.length === 0 ? 200 : 700, - messages: cloneMessages(currentMessages), - workflowStatus: null, - }) - continue - } - - // Assistant: emit workflow status before any content - frames.push({ - delayBefore: 600, - messages: cloneMessages(currentMessages), - workflowStatus: scenario.status, - }) - - // Start with empty assistant shell - currentMessages.push({ ...message, parts: [] }) - const assistantIdx = currentMessages.length - 1 - - for (const part of message.parts) { - const last = currentMessages[assistantIdx] - if (part.type === 'dynamic-tool') { - // First emit running state - const runningPart = { - input: part.input, - state: 'input-available' as const, - toolCallId: part.toolCallId, - toolName: part.toolName, - type: 'dynamic-tool' as const, - } - currentMessages[assistantIdx] = { - ...last, - parts: [...last.parts, runningPart], - } - frames.push({ - delayBefore: 650, - messages: cloneMessages(currentMessages), - workflowStatus: scenario.status, - }) - - // Promote to completed (full part) - const finalParts = [...currentMessages[assistantIdx].parts] - finalParts[finalParts.length - 1] = { ...part } - currentMessages[assistantIdx] = { - ...currentMessages[assistantIdx], - parts: finalParts, - } - frames.push({ - delayBefore: 700, - messages: cloneMessages(currentMessages), - workflowStatus: scenario.status, - }) - } else { - // reasoning, text, anything else: append intact - currentMessages[assistantIdx] = { - ...last, - parts: [...last.parts, part], - } - frames.push({ - delayBefore: 700, - messages: cloneMessages(currentMessages), - workflowStatus: scenario.status, - }) - } - } - - // Clear workflow status as final frame for this assistant turn - frames.push({ - delayBefore: 250, - messages: cloneMessages(currentMessages), - workflowStatus: null, - }) - } - - return frames -} - -const framesByScenario = new Map() -for (const scenario of showcaseScenarios) { - framesByScenario.set(scenario.id, buildFrames(scenario)) -} - -const EMPTY_FRAMES: readonly Frame[] = [] -const EMPTY_FRAME: Frame = { - delayBefore: 0, - messages: [], - workflowStatus: null, -} - -interface ShowcaseState { - activeId: ChatShowcaseScenario['id'] - frameIndex: number - input: string - isPlaying: boolean -} - -type ShowcaseAction = - | { type: 'input'; value: string } - | { type: 'play' } - | { scenario: ChatShowcaseScenario; type: 'select-scenario' } - | { frameIndex: number; type: 'show-frame' } - | { type: 'stop' } - -const initialShowcaseState: ShowcaseState = { - activeId: showcaseScenarios[0].id, - frameIndex: 0, - input: showcaseScenarios[0].prompt, - isPlaying: false, -} - -function showcaseReducer( - state: ShowcaseState, - action: ShowcaseAction -): ShowcaseState { - switch (action.type) { - case 'input': - return { ...state, input: action.value } - case 'play': - return { ...state, frameIndex: 0, isPlaying: true } - case 'select-scenario': - return { - ...state, - activeId: action.scenario.id, - frameIndex: 0, - input: action.scenario.prompt, - isPlaying: true, - } - case 'show-frame': - return { ...state, frameIndex: action.frameIndex } - case 'stop': - return { ...state, isPlaying: false } - default: - return state - } -} - -export function LandingChatShowcase({ - shouldReduceMotion, -}: { - shouldReduceMotion: boolean -}) { - const reduceMotion = useReducedMotion() - const reduceMotionFlag = shouldReduceMotion || Boolean(reduceMotion) - - const [state, dispatch] = useReducer(showcaseReducer, initialShowcaseState) - const { activeId, frameIndex, input, isPlaying } = state - const hasAutoPlayedRef = useRef(false) - const sectionRef = useRef(null) - const timeoutRef = useRef | null>(null) - - const activeScenario = useMemo( - () => - showcaseScenarios.find((scenario) => scenario.id === activeId) ?? - showcaseScenarios[0], - [activeId] - ) - const frames = framesByScenario.get(activeScenario.id) ?? EMPTY_FRAMES - const totalFrames = frames.length - const currentFrame = - frames[Math.min(frameIndex, totalFrames - 1)] ?? EMPTY_FRAME - - const clearTimer = useCallback(() => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - timeoutRef.current = null - } - }, []) - - const play = useCallback(() => { - clearTimer() - dispatch({ type: 'play' }) - }, [clearTimer]) - - // Animation tick: schedule next frame based on its delay. - useEffect(() => { - if (reduceMotionFlag || !isPlaying) { - return - } - if (frameIndex >= totalFrames - 1) { - dispatch({ type: 'stop' }) - return - } - const nextIndex = frameIndex + 1 - const delay = frames[nextIndex]?.delayBefore ?? 600 - const timeoutId = setTimeout(() => { - dispatch({ frameIndex: nextIndex, type: 'show-frame' }) - }, delay) - timeoutRef.current = timeoutId - return () => { - clearTimeout(timeoutId) - if (timeoutRef.current === timeoutId) { - timeoutRef.current = null - } - } - }, [frameIndex, frames, isPlaying, reduceMotionFlag, totalFrames]) - - // Auto-play once when section enters viewport. - useEffect(() => { - if (reduceMotionFlag || hasAutoPlayedRef.current) { - return - } - const section = sectionRef.current - if (!section || typeof IntersectionObserver === 'undefined') { - return - } - const observer = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - if (entry.isIntersecting) { - hasAutoPlayedRef.current = true - play() - observer.disconnect() - break - } - } - }, - { threshold: 0.35 } - ) - observer.observe(section) - return () => observer.disconnect() - }, [play, reduceMotionFlag]) - - function handleScenarioSelect(scenario: ChatShowcaseScenario) { - clearTimer() - dispatch({ scenario, type: 'select-scenario' }) - } - - const displayMessages = reduceMotionFlag - ? activeScenario.messages - : currentFrame.messages - const displayStatus = reduceMotionFlag ? null : currentFrame.workflowStatus - - return ( -
    - - - -
    -

    - Live agent · /chat/:id -

    -

    - Same agent. Every surface. -

    -
    -

    - Heartbeat runs, in-app chat, Slack, Discord, Telegram. -

    -
    - - -
    - - -
    -
    -
    - - - -
    -

    - {agentMeta.name} -

    -

    - {activeScenario.model} -

    -
    -
    -
    - - - {isPlaying ? 'streaming' : 'idle'} - -
    -
    - -
    - -
    - -
    - { - /* static demo */ - }} - > - - dispatch({ - type: 'input', - value: event.currentTarget.value, - }) - } - placeholder="Ask this agent…" - value={input} - /> - -
    - - - -
    -
    -
    - - - -
    - ) -} diff --git a/packages/shared/marketing/components/landing/landing-final-cta.tsx b/packages/shared/marketing/components/landing/landing-final-cta.tsx new file mode 100644 index 00000000..4cefe9cf --- /dev/null +++ b/packages/shared/marketing/components/landing/landing-final-cta.tsx @@ -0,0 +1,55 @@ +'use client' + +import { getAppLoginUrl } from '@outname/shared/app-url' +import { + revealVariants, + staggerVariants, +} from '@outname/shared/marketing/components/landing/landing-motion' +import { PrimaryLink } from '@outname/shared/marketing/components/landing/primary-link' +import { SecondaryLink } from '@outname/shared/marketing/components/landing/secondary-link' +import { domAnimation, LazyMotion, m as motion } from 'motion/react' + +export function LandingFinalCta({ + shouldReduceMotion, +}: { + shouldReduceMotion: boolean +}) { + return ( +
    + + + +

    + Start your first agent +

    +
    +

    + Give it a sandbox. Watch it work. +

    +
    + + Create an account + + + Login + +
    +
    +
    +
    +
    +
    + ) +} diff --git a/packages/shared/marketing/components/landing/landing-footer.tsx b/packages/shared/marketing/components/landing/landing-footer.tsx index 68900494..f3ab1cb2 100644 --- a/packages/shared/marketing/components/landing/landing-footer.tsx +++ b/packages/shared/marketing/components/landing/landing-footer.tsx @@ -1,4 +1,5 @@ import { SiGithub, SiX } from '@icons-pack/react-simple-icons' +import { getAppLoginUrl } from '@outname/shared/app-url' import { LandingSocialLink } from '@outname/shared/marketing/components/landing/landing-social-link' import { githubRepositoryUrl, @@ -6,33 +7,121 @@ import { } from '@outname/shared/marketing/data/social-links' import Link from 'next/link' +interface FooterColumn { + id: string + links: readonly { external?: boolean; href: string; label: string }[] + title: string +} + +const footerColumns: readonly FooterColumn[] = [ + { + id: 'product', + links: [ + { href: '/#anatomy', label: 'How it works' }, + { href: '/#bindings', label: 'Bindings' }, + { href: '/#primitives', label: 'Built on' }, + { href: '/#production', label: 'Production' }, + ], + title: 'Product', + }, + { + id: 'resources', + links: [ + { href: '/blog', label: 'Blog' }, + { href: '/support', label: 'Support' }, + { + external: true, + href: githubRepositoryUrl, + label: 'GitHub', + }, + ], + title: 'Resources', + }, + { + id: 'account', + links: [ + { href: getAppLoginUrl('/agents/new'), label: 'Create an agent' }, + { href: getAppLoginUrl('/dashboard'), label: 'Login' }, + ], + title: 'Account', + }, + { + id: 'legal', + links: [ + { href: '/terms', label: 'Terms' }, + { href: '/privacy', label: 'Privacy' }, + ], + title: 'Legal', + }, +] + export function LandingFooter() { return (
    -
    - - OUTNA.ME - - +
    +
    +
    + + + OUTNA.ME + +

    + Personal AI agents that remember, learn, and keep working, even + when you're not there. +

    + +
    + +
    + {footerColumns.map((column) => ( + + ))} +
    +
    + +
    +

    © OUTNA.ME · MIT licensed · Open source

    +

    Agents that keep working.

    +
    ) diff --git a/packages/shared/marketing/components/landing/landing-heartbeat-closer.tsx b/packages/shared/marketing/components/landing/landing-heartbeat-closer.tsx deleted file mode 100644 index a58dc570..00000000 --- a/packages/shared/marketing/components/landing/landing-heartbeat-closer.tsx +++ /dev/null @@ -1,333 +0,0 @@ -'use client' - -import { getAppLoginUrl } from '@outname/shared/app-url' -import { - revealVariants, - staggerVariants, -} from '@outname/shared/marketing/components/landing/landing-motion' -import { PrimaryLink } from '@outname/shared/marketing/components/landing/primary-link' -import { SecondaryLink } from '@outname/shared/marketing/components/landing/secondary-link' -import { - type HeartbeatEvent, - type HeartbeatKind, - heartbeatEvents, - heartbeatStats, -} from '@outname/shared/marketing/data/heartbeat-demo' -import { Badge } from '@outname/ui/components/ui/badge' -import { cn } from '@outname/ui/lib/utils' -import { - domAnimation, - LazyMotion, - m as motion, - useMotionValueEvent, - useScroll, -} from 'motion/react' -import { useRef, useState } from 'react' - -export function LandingHeartbeatCloser({ - shouldReduceMotion, -}: { - shouldReduceMotion: boolean -}) { - return ( -
    - - - -
    -

    - One day · INBOX SENTINEL · 2026-05-11 -

    -

    - It runs while you sleep. It learns while it runs. -

    -
    -

    - Schedules fire. Channels light up. Sub-agents return. The memory - file grows. The agent gets sharper. You read the log in the - morning. -

    -
    - - - {heartbeatStats.map((stat) => ( -
    -

    - {stat.label} -

    -

    - {stat.value} -

    -
    - ))} -
    - - - {heartbeatEvents.map((entry) => ( - - ))} - - - - - - - -

    - Start your first agent. -

    -
    -
    - - Create an account - - - Login - -
    -
    -
    -
    -
    -
    - ) -} - -const kindLabel: Record = { - cal: 'Calendar', - cron: 'Cron', - gmail: 'Email', - heartbeat: 'Heartbeat', - memory: 'Memory', - slack: 'Slack', - subagent: 'Sub-agent', -} - -function HeartbeatRow({ entry }: { entry: HeartbeatEvent }) { - const isMemory = entry.emphasis === 'memory' - const isHighlight = entry.emphasis === 'highlight' - - return ( -
  • - - {entry.time} - - - {entry.event} - - - {entry.detail} - - - {kindLabel[entry.kind]} - -
  • - ) -} - -function clamp01(value: number) { - if (value < 0) { - return 0 - } - if (value > 1) { - return 1 - } - return value -} - -function HeartbeatTerminalPinned() { - const containerRef = useRef(null) - const [revealed, setRevealed] = useState(0) - const { scrollY } = useScroll() - - useMotionValueEvent(scrollY, 'change', () => { - const node = containerRef.current - if (!node) { - return - } - const rect = node.getBoundingClientRect() - const scrollable = Math.max(1, rect.height - window.innerHeight) - const progress = clamp01(-rect.top / scrollable) - const next = Math.min( - heartbeatEvents.length, - Math.floor(progress * (heartbeatEvents.length + 0.6)) - ) - setRevealed((current) => (current === next ? current : next)) - }) - - const visible = heartbeatEvents.slice(0, revealed) - const memoryCount = visible.filter( - (entry) => entry.emphasis === 'memory' - ).length - const isDone = revealed >= heartbeatEvents.length - - return ( -
    -
    -
    -
    -
    - - $ outname watch inbox-sentinel - - 2026-05-11 -
    - -
      - {visible.map((entry) => { - const isMemory = entry.emphasis === 'memory' - const isHighlight = entry.emphasis === 'highlight' - - return ( - -
      - - {entry.time} - - - {entry.event} - -
      -
      - - ▸ - - - {entry.detail} - -
      - {isHighlight ? ( - - ✓ Done - - ) : null} -
      - ) - })} - - {revealed > 0 ? ( -
    1. - - - {isDone ? 'idle.' : 'streaming…'} - -
    2. - ) : ( -
    3. - Scroll to watch the day -
    4. - )} -
    - -
    -
    -

    Runs

    -

    - {revealed.toString().padStart(2, '0')} -

    -
    -
    -

    Memory

    -

    - +{memoryCount} -

    -
    -
    -

    Questions

    -

    - 00 -

    -
    -
    -
    -
    - -

    - scroll to advance · {revealed} / {heartbeatEvents.length} events -

    -
    -
    - ) -} diff --git a/packages/shared/marketing/components/landing/landing-hero-demo.tsx b/packages/shared/marketing/components/landing/landing-hero-demo.tsx index ad55bdcb..e0c9e6b0 100644 --- a/packages/shared/marketing/components/landing/landing-hero-demo.tsx +++ b/packages/shared/marketing/components/landing/landing-hero-demo.tsx @@ -1,6 +1,8 @@ 'use client' import { getAppLoginUrl } from '@outname/shared/app-url' +import { CommandPill } from '@outname/shared/marketing/components/landing/command-pill' +import { HeroArtifact } from '@outname/shared/marketing/components/landing/hero-artifact' import { PrimaryLink } from '@outname/shared/marketing/components/landing/primary-link' import { SecondaryLink } from '@outname/shared/marketing/components/landing/secondary-link' import { TextLoop } from '@outname/shared/marketing/components/motion-primitives/text-loop' @@ -20,7 +22,7 @@ export function LandingHeroDemo({ }) { return (
    -
    +

    OUTNA.ME /

    -
    -

    - Agents that keep working. -

    -
    +
    +
    +

    + Agents that keep working. +

    +

    + Open-source personal agents. Markdown they read, real tools you + bind, and a heartbeat that keeps working while you sleep. +

    + +
    + +
    + +
    + + Start building + + + Login + +
    + +

    + Open source · MIT licensed · bring your own keys +

    +
    -
    -

    - They remember. They learn. They call other agents. Every run - sharpens the next. -

    -
    - - Start building - - - Login - +
    +
    diff --git a/packages/shared/marketing/components/landing/landing-nav.tsx b/packages/shared/marketing/components/landing/landing-nav.tsx index 9d78f627..5d683dcf 100644 --- a/packages/shared/marketing/components/landing/landing-nav.tsx +++ b/packages/shared/marketing/components/landing/landing-nav.tsx @@ -20,6 +20,12 @@ export function LandingNav() { OUTNA.ME
    +
    + How it works + Bindings + Built on + Production +
    Blog Login diff --git a/packages/shared/marketing/components/landing/landing-primitives.tsx b/packages/shared/marketing/components/landing/landing-primitives.tsx new file mode 100644 index 00000000..645ace09 --- /dev/null +++ b/packages/shared/marketing/components/landing/landing-primitives.tsx @@ -0,0 +1,142 @@ +'use client' + +import { BrandGlyph } from '@outname/shared/marketing/components/landing/brand-glyph' +import { + revealVariants, + staggerVariants, +} from '@outname/shared/marketing/components/landing/landing-motion' +import { + channelsCard, + type PrimitiveProduct, + primitiveCards, +} from '@outname/shared/marketing/data/primitives' +import { domAnimation, LazyMotion, m as motion } from 'motion/react' + +function ProductRow({ product }: { product: PrimitiveProduct }) { + return ( +
    + +
    +

    {product.name}

    +

    + {product.role} +

    +
    +
    + ) +} + +export function LandingPrimitives({ + shouldReduceMotion, +}: { + shouldReduceMotion: boolean +}) { + return ( +
    + + + +
    +

    + Built on primitives +

    +

    + Nothing you can't host yourself. +

    +
    +

    + Open source, sitting on building blocks you already trust. Bring + your own keys, swap the providers, run the whole thing on your own + infrastructure. +

    +
    + + + {primitiveCards.map((card) => ( +
    +

    {card.eyebrow}

    +

    + {card.summary} +

    +
    + {card.products.map((product) => ( + + ))} +
    +
    + ))} + +
    +
    +

    + {channelsCard.eyebrow} +

    +

    + {channelsCard.summary} +

    +
    + +
    +
    + +
    +
    +

    Channels

    +
      + {channelsCard.channels.map((channel) => ( +
    • + + + {channel} + +
    • + ))} +
    +
    +
    +

    + Connections +

    +
      + {channelsCard.connections.map((connection) => ( +
    • + + + {connection} + +
    • + ))} +
    +
    +
    +
    +
    +
    +
    +
    + ) +} diff --git a/packages/shared/marketing/components/landing/landing-production.tsx b/packages/shared/marketing/components/landing/landing-production.tsx new file mode 100644 index 00000000..a9cc7b3a --- /dev/null +++ b/packages/shared/marketing/components/landing/landing-production.tsx @@ -0,0 +1,243 @@ +'use client' + +import { + revealVariants, + staggerVariants, +} from '@outname/shared/marketing/components/landing/landing-motion' +import { cn } from '@outname/ui/lib/utils' +import { + ActivityIcon, + GaugeIcon, + type LucideIcon, + RefreshCwIcon, +} from 'lucide-react' +import { domAnimation, LazyMotion, m as motion } from 'motion/react' +import type { ReactNode } from 'react' + +// Durable execution: the phases a single run checkpoints through, so a crash +// resumes mid-step instead of restarting. +const runPhases = [ + { fill: 1, label: 'load memory', state: 'completed' }, + { fill: 1, label: 'scan channels', state: 'completed' }, + { fill: 0.66, label: 'call sub-agent', state: 'running' }, + { fill: 0.2, label: 'write memory', state: 'queued' }, + { fill: 0, label: 'append log', state: 'queued' }, +] as const + +// Observability: the Events log — every run is an Event with a real type and +// status from the runtime vocabulary. +const eventLog = [ + { id: 'heartbeat · 06:00', status: 'completed', meta: '4.2s' }, + { id: 'dreaming · 14:01', status: 'completed', meta: '11s' }, + { id: 'invocation · now', status: 'running', meta: 'live' }, + { id: 'heartbeat · 18:00', status: 'queued', meta: '—' }, +] as const + +const statusTone: Record = { + completed: 'bg-muted-foreground/40', + queued: 'bg-muted-foreground/25', + running: 'bg-brand', +} + +// Governance: real budget periods (USD spend vs. ceiling) plus the per-run +// step limit. +const budgetRules = [ + { ceiling: '$5.00', fill: 0.48, label: 'daily', spent: '$2.40' }, + { ceiling: '$25.00', fill: 0.47, label: 'weekly', spent: '$11.80' }, + { ceiling: '$100.00', fill: 0.36, label: 'monthly', spent: '$36.00' }, +] as const + +interface Pillar { + icon: LucideIcon + id: string + text: string + title: string +} + +const pillars: readonly Pillar[] = [ + { + icon: RefreshCwIcon, + id: 'durable', + text: 'A crash or redeploy resumes the run mid-step — no lost work, no double-sends. Every run is an event-driven Vercel Workflow.', + title: 'Durable execution', + }, + { + icon: ActivityIcon, + id: 'events', + text: 'Heartbeats, dreaming passes, and sub-agent calls all land in one Events log — type, status, and duration on every run.', + title: 'Observable by default', + }, + { + icon: GaugeIcon, + id: 'bounded', + text: 'Per-agent and account-wide budgets in USD, plus a step limit on every run. Estimated and actual cost is tracked per Event.', + title: 'Bounded spend', + }, +] + +function DurableMock() { + return ( +
      + {runPhases.map((phase) => ( +
    • +
      + {phase.label} + {phase.state} +
      +
      +
      +
      +
    • + ))} +
    + ) +} + +function EventsMock() { + return ( +
      + {eventLog.map((event) => ( +
    • +
      + + + {event.id} + + {event.meta} +
      +
      +
    • + ))} +
    + ) +} + +function BudgetMock() { + return ( +
    +
      + {budgetRules.map((rule) => ( +
    • +
      + {rule.label} + + {rule.spent} / {rule.ceiling} + +
      +
      +
      +
      +
    • + ))} +
    +
    + step limit + medium · 40 / run +
    +
    + ) +} + +const mocks: Record ReactNode> = { + bounded: BudgetMock, + durable: DurableMock, + events: EventsMock, +} + +export function LandingProduction({ + shouldReduceMotion, +}: { + shouldReduceMotion: boolean +}) { + return ( +
    + + + +
    +

    + Production agents +

    +

    + Everything you need for production agents. +

    +
    +

    + Durability, observability, and governance come standard. Focus on + what the agent does, not the plumbing that keeps it running. +

    +
    + + {/* Shared visual panel: three mocks in equal-height cells. */} + + {pillars.map((pillar) => { + const Mock = mocks[pillar.id] + return ( +
    +
    + +
    +
    + ) + })} +
    + + {/* Aligned text row: one label + description per column. */} + + {pillars.map((pillar) => { + const Icon = pillar.icon + return ( +
    +
    + +

    + {pillar.title} +

    +
    +

    + {pillar.text} +

    +
    + ) + })} +
    +
    +
    +
    + ) +} diff --git a/packages/shared/marketing/data/agent-anatomy.ts b/packages/shared/marketing/data/agent-anatomy.ts new file mode 100644 index 00000000..57be09f7 --- /dev/null +++ b/packages/shared/marketing/data/agent-anatomy.ts @@ -0,0 +1,317 @@ +// The agent-anatomy section walks a real outname agent's sandbox. Every file +// below is canonical in the runtime (see the AGENTS.md template and +// sandbox-file-helpers/paths.ts). Snippets reflect the documented conventions +// for each file — nothing here is invented product surface. + +export type AnatomyStepId = + | 'instructions' + | 'identity' + | 'soul' + | 'user' + | 'memory' + | 'tasks' + | 'calendar' + | 'goals' + | 'dreams' + +/** Who owns each file: the operator authors some, the agent maintains others. */ +export type FileOwner = 'user' | 'agent' | 'shared' + +export interface AgentTreeNode { + /** Indentation level inside the tree. Root children are depth 1. */ + depth: number + /** Stable id; matches a step id when the node is the focus of a step. */ + id: string + kind: 'dir' | 'file' + label: string + owner?: FileOwner + /** The step that highlights this node, when any. */ + stepId?: AnatomyStepId +} + +export interface AnatomyStep { + caption: string + /** Short markdown excerpt rendered as a mono block. */ + code: string + id: AnatomyStepId + /** Two-digit ordinal, e.g. "01". */ + index: string + /** Tree node this step focuses. */ + node: string + /** A real convention from the runtime, shown as a tag. */ + note: string + owner: FileOwner + title: string +} + +export const agentSlug = 'inbox-sentinel' + +export const ownerLabel: Record = { + agent: 'Agent writes it', + shared: 'Shared', + user: 'You author it', +} + +export const agentTree: readonly AgentTreeNode[] = [ + { + depth: 1, + id: 'instructions', + kind: 'file', + label: 'AGENTS.md', + owner: 'user', + stepId: 'instructions', + }, + { + depth: 1, + id: 'identity', + kind: 'file', + label: 'IDENTITY.md', + owner: 'user', + stepId: 'identity', + }, + { + depth: 1, + id: 'soul', + kind: 'file', + label: 'SOUL.md', + owner: 'user', + stepId: 'soul', + }, + { + depth: 1, + id: 'user', + kind: 'file', + label: 'USER.md', + owner: 'shared', + stepId: 'user', + }, + { + depth: 1, + id: 'memory', + kind: 'file', + label: 'MEMORY.md', + owner: 'agent', + stepId: 'memory', + }, + { + depth: 1, + id: 'tasks', + kind: 'file', + label: 'TASKS.md', + owner: 'agent', + stepId: 'tasks', + }, + { + depth: 1, + id: 'calendar', + kind: 'file', + label: 'CALENDAR.md', + owner: 'agent', + stepId: 'calendar', + }, + { + depth: 1, + id: 'goals', + kind: 'file', + label: 'GOALS.md', + owner: 'agent', + stepId: 'goals', + }, + { + depth: 1, + id: 'dreams', + kind: 'file', + label: 'DREAMS.md', + owner: 'agent', + stepId: 'dreams', + }, + { depth: 1, id: 'logs', kind: 'dir', label: 'logs/', owner: 'agent' }, +] + +export const anatomySteps: readonly AnatomyStep[] = [ + { + caption: + 'Its operational manual. You write the custom instructions; the agent reads them at the start of every event.', + code: `# AGENTS.md +Your operational manual. Read it at the +start of every event. + +## Conventions +- Dates are ISO-8601. +- Append one bullet to today's log. +- Terse output; bullets over prose. + +## User custom instructions +- Triage the #ops Slack channel by 09:00. +- Never send external email without a + confirm. +- Keep replies short; prefer "Tomas".`, + id: 'instructions', + index: '01', + node: 'instructions', + note: 'Read every event', + owner: 'user', + title: 'How it should behave', + }, + { + caption: + 'A compact identity card: name, role, vibe. Short by design, it is injected into every prompt the agent runs.', + code: `# Inbox Sentinel +Role: personal chief of staff +Vibe: terse, calm, proactive +Emoji: 🛰️ + +Read every turn — keep it short. +First-impression cues only; the +deeper self-model lives in SOUL.md.`, + id: 'identity', + index: '02', + node: 'identity', + note: 'Injected every turn', + owner: 'user', + title: 'Who it is, at a glance', + }, + { + caption: + 'Its persona, voice, and self-model. Also injected every turn — if behavior drifts from it, the agent flags the contradiction.', + code: `# SOUL.md +I default to action over explanation. +Bullets over prose. I do one small +useful thing well, then stop. + +I surface contradictions instead of +working around them. If my behavior +drifts from this file, I raise it. + +I protect the user's focus: batch the +low-signal updates, interrupt only for +what truly needs them.`, + id: 'soul', + index: '03', + node: 'soul', + note: 'Injected every turn', + owner: 'user', + title: 'Its voice and self-model', + }, + { + caption: + 'The profile of the human it serves. You can seed it; the agent keeps it current as conversations reveal stable facts.', + code: `## Basic Info +- Preferred name: Tomas +- Timezone: Europe/Rome +- Language: English + +## My World +- Founder, shipping outname. +- Mornings are deep-work; protect them. + +## Hard Boundaries +- Ask before sending external email. +- Never invent facts about people.`, + id: 'user', + index: '04', + node: 'user', + note: 'It maintains, you can edit', + owner: 'shared', + title: 'What it knows about you', + }, + { + caption: + 'Broader durable facts, commitments, and evidence. Append-only by convention, with a citation back to where each fact came from.', + code: `## 2026-05-13 +- Skip auto-summary on Sundays. +- Prefers "Tomas" in replies. (msg_8f12) +- #ops blocker is owned by Dana. (msg_77a1) + +## 2026-05-11 +- Weekly digest ships Mondays 18:00. +- Cal.com is the source of truth for + meetings, not email. (msg_5c0e)`, + id: 'memory', + index: '05', + node: 'memory', + note: 'Append-only', + owner: 'agent', + title: 'Durable facts it commits', + }, + { + caption: + 'Active tactical items with status and dependencies, kept current without waiting for a reminder. Plain GitHub-flavored checkboxes.', + code: `## In progress +- [ ] Confirm Tue 15:00 → Wed 10:00 move +- [ ] Chase invoice #204 (due Fri) + +## Done +- [x] Draft weekly digest +- [x] Summarize #ops overnight threads +- [x] Update USER.md timezone + +## Blocked +- [ ] Onboarding rewrite — waiting on Dana`, + id: 'tasks', + index: '06', + node: 'tasks', + note: 'Checkbox conventions', + owner: 'agent', + title: 'The work it is tracking', + }, + { + caption: + 'Known time-bound events and deadlines, ISO-8601 dated. The agent adds, updates, and removes entries as plans change.', + code: `# CALENDAR.md +ISO-8601 dated. Add, update, and remove +entries as plans change. + +- 2026-05-14T10:00Z Design review +- 2026-05-14T15:00Z Tomas / 1:1 (moved) +- 2026-05-16 Invoice #204 due +- 2026-05-18T09:00Z Weekly planning`, + id: 'calendar', + index: '07', + node: 'calendar', + note: 'ISO-8601 dated', + owner: 'agent', + title: 'Its time-bound context', + }, + { + caption: + 'Long-horizon objectives, updated rarely. The agent consults them before deciding what is worth surfacing in a heartbeat.', + code: `# GOALS.md +Long-horizon. Updated rarely; consult +before surfacing work in a heartbeat. + +- Keep the inbox under 10 open threads. +- Protect Tomas's deep-work mornings. +- Reduce onboarding to three steps. +- Zero missed invoices this quarter.`, + id: 'goals', + index: '08', + node: 'goals', + note: 'Steers every heartbeat', + owner: 'agent', + title: 'The long horizon', + }, + { + caption: + 'Notes from dreaming passes: pattern anticipation and self-evaluation, written only when there is real signal, with log citations.', + code: `# DREAMS.md +Pattern notes from dreaming passes. +Written only when there is real signal. + +## 2026-05-12 +- Replies spike on Mondays; pre-draft + the digest Sunday night. + (logs/2026-05-11.md:14) +- "Tomas" preference is firm across 12 + threads — promote it to USER.md. + (logs/2026-05-10.md:6)`, + id: 'dreams', + index: '09', + node: 'dreams', + note: 'Written while dreaming', + owner: 'agent', + title: 'What it learns in its sleep', + }, +] + +export const anatomyStepCount = anatomySteps.length diff --git a/packages/shared/marketing/data/composability-demo.ts b/packages/shared/marketing/data/composability-demo.ts deleted file mode 100644 index 77ca4fb5..00000000 --- a/packages/shared/marketing/data/composability-demo.ts +++ /dev/null @@ -1,65 +0,0 @@ -export type StageId = 'tools' | 'subagents' | 'channels' | 'memory' -export type Corner = 'ne' | 'nw' | 'se' | 'sw' - -export interface ComposabilityPart { - id: string - label: string -} - -export interface ComposabilityStage { - caption: string - corner: Corner - eyebrow: string - id: StageId - label: string - parts: readonly ComposabilityPart[] -} - -export const composabilityStages: readonly ComposabilityStage[] = [ - { - caption: - 'Typed contracts. Rate-limited. Scoped per agent. The agent only calls what you bound.', - corner: 'ne', - eyebrow: '01 / 04', - id: 'tools', - label: 'Tools', - parts: [ - { id: 'tool-slack', label: 'slack.search_threads' }, - { id: 'tool-gmail', label: 'gmail.draft' }, - { id: 'tool-cal', label: 'cal.create_event' }, - ], - }, - { - caption: - 'Delegate work. Each call is a traced run on its own. The parent waits or fires-and-forgets.', - corner: 'nw', - eyebrow: '02 / 04', - id: 'subagents', - label: 'Sub-agents', - parts: [ - { id: 'sub-research', label: 'research-synthesizer' }, - { id: 'sub-digester', label: 'weekly-digester' }, - ], - }, - { - caption: - 'Where the agent listens and speaks. Slack DMs, email threads, webhook intake — bound, not guessed.', - corner: 'se', - eyebrow: '03 / 04', - id: 'channels', - label: 'Channels', - parts: [ - { id: 'channel-slack', label: 'slack:@you' }, - { id: 'channel-email', label: 'email:inbound' }, - ], - }, - { - caption: - 'One markdown file per agent. The agent appends its own notes. You read them anytime.', - corner: 'sw', - eyebrow: '04 / 04', - id: 'memory', - label: 'Memory', - parts: [{ id: 'memory-dreams', label: 'DREAMS.md · 47 entries' }], - }, -] diff --git a/packages/shared/marketing/data/heartbeat-demo.ts b/packages/shared/marketing/data/heartbeat-demo.ts deleted file mode 100644 index c5926cb4..00000000 --- a/packages/shared/marketing/data/heartbeat-demo.ts +++ /dev/null @@ -1,88 +0,0 @@ -export type HeartbeatKind = - | 'cron' - | 'slack' - | 'memory' - | 'heartbeat' - | 'cal' - | 'subagent' - | 'gmail' - -export interface HeartbeatEvent { - detail: string - emphasis?: 'memory' | 'highlight' - event: string - kind: HeartbeatKind - time: string -} - -export const heartbeatStats = [ - { label: 'Unprompted runs', value: '21' }, - { label: 'Memory entries written', value: '+8' }, - { label: 'Questions asked', value: '0' }, -] as const - -export const heartbeatEvents: readonly HeartbeatEvent[] = [ - { - detail: 'daily.triage queued', - event: 'cron.fire', - kind: 'cron', - time: '06:00', - }, - { - detail: '14 threads scanned · 2 flagged', - event: 'slack.read', - kind: 'slack', - time: '06:00', - }, - { - detail: '+ skip auto-summary on Sundays', - emphasis: 'memory', - event: 'memory.write', - kind: 'memory', - time: '06:01', - }, - { - detail: 'calendar conflict spotted · draft sent', - event: 'heartbeat', - kind: 'heartbeat', - time: '09:14', - }, - { - detail: 'tue 15:00 → wed 10:00 proposed', - event: 'cal.draft', - kind: 'cal', - time: '09:14', - }, - { - detail: 'research-synthesizer · 4.2s', - event: 'subagent.call', - kind: 'subagent', - time: '11:02', - }, - { - detail: '+ user prefers "Tomas" in replies', - emphasis: 'memory', - event: 'memory.write', - kind: 'memory', - time: '11:02', - }, - { - detail: 'weekly.digest queued', - event: 'cron.fire', - kind: 'cron', - time: '14:00', - }, - { - detail: '5 threads summarized · digest ready', - event: 'gmail.draft', - kind: 'gmail', - time: '14:01', - }, - { - detail: 'weekly digest sent · 0 follow-ups', - emphasis: 'highlight', - event: 'gmail.send', - kind: 'gmail', - time: '18:00', - }, -] diff --git a/packages/shared/marketing/data/primitives.ts b/packages/shared/marketing/data/primitives.ts new file mode 100644 index 00000000..a31ca17a --- /dev/null +++ b/packages/shared/marketing/data/primitives.ts @@ -0,0 +1,94 @@ +// "Built on primitives" mirrors eve's "Leverages all Vercel AI primitives": +// a row of category cards, each listing the real products behind it, plus a +// channels/connections block. Everything here is real to the outname stack. + +export interface PrimitiveProduct { + name: string + role: string +} + +export interface PrimitiveCard { + eyebrow: string + id: string + products: readonly PrimitiveProduct[] + summary: string +} + +export const primitiveCards: readonly PrimitiveCard[] = [ + { + eyebrow: 'Runtime', + id: 'runtime', + products: [ + { + name: 'Vercel Workflow', + role: 'Heartbeat, dreaming, and sub-agent runs — checkpointed and resumable.', + }, + ], + summary: 'Durable, event-driven execution.', + }, + { + eyebrow: 'Compute', + id: 'compute', + products: [ + { + name: 'Vercel Sandbox', + role: 'A persistent, isolated filesystem and skill execution per agent.', + }, + ], + summary: 'A filesystem of its own.', + }, + { + eyebrow: 'Inference', + id: 'inference', + products: [ + { + name: 'Vercel AI Gateway', + role: 'Default gateway for model calls and streaming.', + }, + { name: 'LLM Gateway', role: 'Alternate provider, same interface.' }, + { name: 'OpenRouter', role: 'Hundreds of models behind one key.' }, + ], + summary: 'Swap providers without touching the agent.', + }, + { + eyebrow: 'Foundation', + id: 'foundation', + products: [ + { name: 'Next.js 16', role: 'The single control plane.' }, + { + name: 'Neon Postgres', + role: 'Typed control-plane database via Drizzle.', + }, + { + name: 'Upstash Redis', + role: 'Coordination, caching, and rate limits.', + }, + { name: 'Better Auth', role: 'Passwordless email one-time codes.' }, + ], + summary: 'Control plane, data, and sign-in.', + }, +] + +export const channelsCard = { + channels: ['in-app chat', 'Slack'], + connections: [ + 'GitHub', + 'Cal.com', + 'Resend', + 'Firecrawl', + 'PostHog', + 'Parallel', + 'Typefully', + 'X', + 'Supabase', + 'v0', + 'Vercel', + 'Context7', + ], + eyebrow: 'Channels & connections', + product: { + name: 'Chat SDK', + role: 'In-app chat and Slack today; new channels drop in on the same agent.', + }, + summary: 'One agent, every channel.', +} as const