))}
@@ -88,7 +70,7 @@ export function FeatureList({ items }: FeatureListProps) {
{items.map((item, index) => (
-
+
{index < lastItemIndex &&
}
))}
diff --git a/docs/app/(home)/components/TweetWall/TweetWall.tsx b/docs/app/(home)/components/TweetWall/TweetWall.tsx
index 5e5ba4a0b..6f359c6e0 100644
--- a/docs/app/(home)/components/TweetWall/TweetWall.tsx
+++ b/docs/app/(home)/components/TweetWall/TweetWall.tsx
@@ -72,9 +72,8 @@ export function TweetWall() {
const columns = splitIntoColumns(HOME_TWEETS, columnCount);
useEffect(() => {
- if (window.twttr?.widgets?.createTweet) {
- setScriptReady(true);
- }
+ if (!window.twttr?.widgets?.createTweet) return;
+ queueMicrotask(() => setScriptReady(true));
}, []);
useEffect(() => {
@@ -91,11 +90,11 @@ export function TweetWall() {
);
if (!shouldRehydrate) {
- setIsWallReady(true);
+ queueMicrotask(() => setIsWallReady(true));
return;
}
- setIsWallReady(false);
+ queueMicrotask(() => setIsWallReady(false));
async function hydrateEmbeds() {
await Promise.allSettled(
diff --git a/docs/app/(home)/page.module.css b/docs/app/(home)/page.module.css
index 7535425e6..e7d6e4886 100644
--- a/docs/app/(home)/page.module.css
+++ b/docs/app/(home)/page.module.css
@@ -24,10 +24,18 @@
background: var(--openui-background);
}
+.contentShellTight {
+ gap: 4rem;
+}
+
@media (min-width: 768px) {
.contentShell {
gap: 15rem;
}
+
+ .contentShellTight {
+ gap: 6rem;
+ }
}
@media (min-width: 1024px) {
diff --git a/docs/app/(home)/sections/FeaturesSection/FeaturesSection.module.css b/docs/app/(home)/sections/FeaturesSection/FeaturesSection.module.css
index d424dc0e7..548b639ee 100644
--- a/docs/app/(home)/sections/FeaturesSection/FeaturesSection.module.css
+++ b/docs/app/(home)/sections/FeaturesSection/FeaturesSection.module.css
@@ -1,6 +1,6 @@
.section {
width: 100%;
- padding-inline: 1.25rem;
+ padding: 2rem 1.25rem;
}
.container {
@@ -57,7 +57,7 @@
@media (min-width: 1024px) {
.section {
- padding-inline: 2rem;
+ padding: 3rem 2rem;
}
.ctaWrap {
diff --git a/docs/app/(home)/sections/FeaturesSection/FeaturesSection.tsx b/docs/app/(home)/sections/FeaturesSection/FeaturesSection.tsx
index d3c7b9724..906bf1d30 100644
--- a/docs/app/(home)/sections/FeaturesSection/FeaturesSection.tsx
+++ b/docs/app/(home)/sections/FeaturesSection/FeaturesSection.tsx
@@ -1,74 +1,139 @@
"use client";
+import {
+ Browsers,
+ Eye,
+ MagicWand,
+ PushPin,
+ SlidersHorizontal,
+ SquaresFour,
+} from "@phosphor-icons/react";
import svgPaths from "@/imports/svg-urruvoh2be";
import { PillLink } from "../../components/Button/Button";
import { FeatureList, type FeatureListItem } from "../../components/FeatureList/FeatureList";
import styles from "./FeaturesSection.module.css";
-// ---------------------------------------------------------------------------
-// Data
-// ---------------------------------------------------------------------------
+const PHOSPHOR_ICON_SIZE = 18;
-const FEATURES: FeatureListItem[] = [
+function SvgPathIcon({ path, index }: { path: string; index: number }) {
+ const clipId = `clip_feat_${index}`;
+ return (
+
+ );
+}
+
+const DEFAULT_FEATURES: FeatureListItem[] = [
{
title: "Performance Optimized",
description: "3x faster rendering than json-render",
- iconPath: svgPaths.p7658f00,
+ icon:
,
},
{
title: "Token efficient",
description: "Up to 67.1% lesser tokens than json-render",
- iconPath: svgPaths.p2a8ddd80,
+ icon:
,
},
{
title: "Live data",
description: "Query your tools and MCP servers at runtime",
- iconPath: svgPaths.p10e86100,
+ icon:
,
},
{
title: "Works across platforms",
description: "React, React Native, Vue, etc",
- iconPath: svgPaths.p2cbb5d00,
+ icon:
,
},
{
title: "Native Streaming",
description: "UI renders in real time",
- iconPath: svgPaths.p33780400,
+ icon:
,
},
{
title: "Interactive",
description: "Reactive state, inputs, and tool-connected actions",
- iconPath: svgPaths.p17c7f700,
+ icon:
,
},
{
title: "Safe by Default",
description: "No arbitrary code execution",
- iconPath: svgPaths.p16eec200,
+ icon:
,
},
];
-// ---------------------------------------------------------------------------
-// Main component
-// ---------------------------------------------------------------------------
+export const OPENCLAW_FEATURES: FeatureListItem[] = [
+ {
+ title: "Generative UI",
+ description: "Build apps, dashboards, and artifacts on demand",
+ icon:
,
+ },
+ {
+ title: "Persistent apps",
+ description: "Apps stay in place and refresh with live data automatically",
+ icon:
,
+ },
+ {
+ title: "Structured workspace",
+ description: "Agents, sessions, artifacts, and apps in one organized space",
+ icon:
,
+ },
+ {
+ title: "Full visibility",
+ description: "Inspect tool calls, context, and agent actions in real time",
+ icon:
,
+ },
+ {
+ title: "Direct control",
+ description: "Permissions, schedules, and execution from one interface",
+ icon:
,
+ },
+ {
+ title: "Elegant interface",
+ description: "Built for clarity with responsive layouts and themes",
+ icon:
,
+ },
+];
-export function FeaturesSection() {
+export function FeaturesSection({
+ features = DEFAULT_FEATURES,
+ showCta = true,
+}: {
+ features?: FeatureListItem[];
+ showCta?: boolean;
+} = {}) {
return (
-
+
- {/* CTA button */}
-
-
-
- Detailed comparison
- View Comparison
-
-
-
+ {showCta && (
+
+
+
+ Detailed comparison
+ View Comparison
+
+
+
+ )}
);
diff --git a/docs/app/(home)/sections/HeroSection/HeroSection.module.css b/docs/app/(home)/sections/HeroSection/HeroSection.module.css
index e0fc7d729..a7b91d2ef 100644
--- a/docs/app/(home)/sections/HeroSection/HeroSection.module.css
+++ b/docs/app/(home)/sections/HeroSection/HeroSection.module.css
@@ -18,12 +18,26 @@
letter-spacing: -3px;
}
+.desktopTitleCompact {
+ font-size: 6rem;
+ letter-spacing: -2px;
+}
+
+.titleAccent {
+ color: #9ca3af;
+}
+
.mobileTitle {
text-align: center;
font-size: clamp(96px, 25vw, 120px);
letter-spacing: -2px;
}
+.mobileTitleCompact {
+ font-size: clamp(40px, 10vw, 52px);
+ letter-spacing: -1px;
+}
+
.desktopSubtitle,
.mobileSubtitle,
.tagline {
@@ -55,6 +69,11 @@
line-height: 1.4;
}
+.taglineCompact {
+ font-size: 16px;
+ color: var(--openui-text-neutral-primary);
+}
+
.taglineBreak {
display: none;
}
@@ -99,6 +118,41 @@
/* ── NPM button ── */
+.npmButtonWrapper {
+ position: relative;
+ z-index: 5;
+ display: inline-block;
+ width: max-content;
+ max-width: 100%;
+}
+
+.copyToast {
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ z-index: 50;
+ margin-top: 0.375rem;
+ padding: 0.3125rem 0.75rem;
+ border-radius: var(--openui-radius-full);
+ background: rgba(0, 0, 0, 0.06);
+ color: var(--openui-text-neutral-secondary);
+ font-family: "Inter", sans-serif;
+ font-size: 0.75rem;
+ line-height: 1;
+ white-space: nowrap;
+ pointer-events: none;
+ opacity: 0;
+ transform: translate(-50%, -6px);
+ transition:
+ opacity 0.2s ease,
+ transform 0.2s ease;
+}
+
+.copyToastVisible {
+ opacity: 1;
+ transform: translate(-50%, 0);
+}
+
.npmButton {
display: flex;
height: 3rem;
@@ -342,6 +396,42 @@
margin-top: 1rem;
}
+.desktopCtaStackShadowRoom {
+ padding-bottom: 0.75rem;
+}
+
+.subtitleLogo {
+ display: block;
+ margin: 0.75rem auto 0;
+ width: 64px;
+ height: 64px;
+}
+
+.subtitleLogoTile {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.3em;
+ height: 1.3em;
+ margin: 0 0.3em 0 0.15em;
+ padding: 0.2em;
+ border-radius: 0.35em;
+ background: #ffffff;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ box-shadow:
+ 0 1px 2px rgba(15, 23, 42, 0.06),
+ 0 4px 8px rgba(15, 23, 42, 0.05);
+ vertical-align: middle;
+ position: relative;
+ top: -0.15em;
+}
+
+.subtitleLogoTile img {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
/* ── Mobile hero layout ── */
.mobileHero {
@@ -379,7 +469,7 @@
align-items: center;
margin-top: 40px;
gap: 1rem;
- padding: 0.5rem 2.5rem 4rem;
+ padding: 0.5rem 2.5rem 1rem;
}
.mobileCtaButtonWidth {
@@ -408,6 +498,10 @@
max-width: 75rem;
}
+.previewFrameWide {
+ max-width: 130rem;
+}
+
.mobileIllustrationImage,
.previewImage {
display: block;
@@ -415,6 +509,12 @@
height: auto;
}
+.previewFrameWide .previewImage {
+ width: 118%;
+ margin-inline: -9%;
+ max-width: none;
+}
+
.previewSection {
width: 100%;
overflow: hidden;
@@ -422,6 +522,10 @@
padding-inline: 1rem;
}
+.previewSectionTight {
+ margin-top: 0.5rem;
+}
+
.previewDesktopOnly {
display: none;
width: 100%;
@@ -475,7 +579,6 @@
display: block;
position: relative;
width: 100%;
- overflow: hidden;
padding: 8rem 2rem 0;
}
@@ -499,7 +602,7 @@
.previewSection {
margin-top: 2.5rem;
- padding-inline: 2rem;
+ padding-inline: 1rem;
}
.previewDesktopOnly {
@@ -515,6 +618,10 @@
font-size: 28px;
}
+ .taglineCompact {
+ font-size: 18px;
+ }
+
.taglineBreak {
display: inline;
}
diff --git a/docs/app/(home)/sections/HeroSection/HeroSection.tsx b/docs/app/(home)/sections/HeroSection/HeroSection.tsx
index 4acac1b62..7bfd87232 100644
--- a/docs/app/(home)/sections/HeroSection/HeroSection.tsx
+++ b/docs/app/(home)/sections/HeroSection/HeroSection.tsx
@@ -3,9 +3,12 @@
import { GitHubIcon } from "@/components/brand-logo";
import { ArrowRight } from "lucide-react";
import Link from "next/link";
+import { useEffect, useRef, useState, type ReactNode } from "react";
import { ClipboardCommandButton, PillLink } from "../../components/Button/Button";
import styles from "./HeroSection.module.css";
+export const heroStyles = styles;
+
// CTAs
const primaryCTA = "npx @openuidev/cli@latest create";
const secondaryCTA = "Try Playground";
@@ -34,24 +37,58 @@ function TrailingArrow() {
);
}
-function NpmButton({ className = "" }: { className?: string }) {
+const COPY_TOAST_MS = 1800;
+
+export function NpmButton({ className = "", command }: { className?: string; command: string }) {
+ const [showToast, setShowToast] = useState(false);
+ const toastTimeoutRef = useRef
| null>(null);
+
+ useEffect(() => {
+ return () => {
+ if (toastTimeoutRef.current) {
+ clearTimeout(toastTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ const handleCopyChange = (copied: boolean) => {
+ if (!copied) return;
+ setShowToast(true);
+ if (toastTimeoutRef.current) {
+ clearTimeout(toastTimeoutRef.current);
+ }
+ toastTimeoutRef.current = setTimeout(() => {
+ setShowToast(false);
+ }, COPY_TOAST_MS);
+ };
+
return (
-
- {primaryCTA}
-
-
- {primaryCTA}
-
- {primaryCTA}
+
+
+ {command}
+
+
+ {command}
+
+ {command}
+
-
-
+
+
+ Copied. Paste in your terminal to install.
+
+
);
}
@@ -136,19 +173,41 @@ function GitHubBanner({ className = "" }: { className?: string }) {
// Desktop hero
// ---------------------------------------------------------------------------
-function DesktopHero() {
+function DesktopHero({
+ title,
+ subtitle,
+ command,
+ compact,
+ showBanner,
+ showPlaygroundButton,
+}: {
+ title: ReactNode;
+ subtitle: ReactNode;
+ command: string;
+ compact: boolean;
+ showBanner: boolean;
+ showPlaygroundButton: boolean;
+}) {
return (
-
-
OpenUI
-
The Open Standard for Generative UI
+ {showBanner &&
}
+
+ {title}
+
+
{subtitle}
-
-
-
+
+
+ {showPlaygroundButton && }
@@ -159,39 +218,82 @@ function DesktopHero() {
// Mobile hero
// ---------------------------------------------------------------------------
-function MobileHero({ theme }: { theme: HeroTheme }) {
- const mobileHeroImage = theme === "dark" ? MOBILE_HERO_IMAGE.dark : MOBILE_HERO_IMAGE.light;
+function MobileHero({
+ theme,
+ title,
+ subtitle,
+ command,
+ compact,
+ showBanner,
+ showPlaygroundButton,
+ showGitHubBanner,
+ mobileImageOverride,
+ mobileImageAlt,
+ mobileImageWidth,
+ mobileImageHeight,
+ mobileImageCropTopPercent = 0,
+}: {
+ theme: HeroTheme;
+ title: ReactNode;
+ subtitle: ReactNode;
+ command: string;
+ compact: boolean;
+ showBanner: boolean;
+ showPlaygroundButton: boolean;
+ showGitHubBanner: boolean;
+ mobileImageOverride?: string;
+ mobileImageAlt?: string;
+ mobileImageWidth?: number;
+ mobileImageHeight?: number;
+ mobileImageCropTopPercent?: number;
+}) {
+ const mobileHeroImage =
+ mobileImageOverride ?? (theme === "dark" ? MOBILE_HERO_IMAGE.dark : MOBILE_HERO_IMAGE.light);
+
+ const naturalWidth = mobileImageWidth ?? MOBILE_HERO_IMAGE.width;
+ const naturalHeight = mobileImageHeight ?? MOBILE_HERO_IMAGE.height;
+ const cropTop = Math.max(0, Math.min(100, mobileImageCropTopPercent));
+ const cropped = cropTop > 0;
+ const viewportStyle = cropped
+ ? { aspectRatio: `${naturalWidth} / ${naturalHeight * (1 - cropTop / 100)}` }
+ : undefined;
+ const imageStyle = cropped
+ ? ({ height: "100%", objectFit: "cover", objectPosition: "bottom" } as const)
+ : undefined;
return (
-
+ {showBanner &&
}
{/* Subtitle */}
-
The Open Standard for Generative UI
+
{subtitle}
{/* CTA buttons */}
-
-
-
+
+ {showPlaygroundButton && }
+ {showGitHubBanner && }
{/* Mobile hero image */}
-
+

+
-
+
-
- An open source toolkit to make your
- AI apps respond with your UI.
+
+ {children ?? (
+ <>
+ An open source toolkit to make your
+ AI apps respond with your UI.
+ >
+ )}
@@ -249,15 +375,85 @@ function Tagline() {
// Main export
// ---------------------------------------------------------------------------
-export function HeroSection() {
+export function HeroSection({
+ title = "OpenUI",
+ subtitle = "The Open Standard for Generative UI",
+ command = primaryCTA,
+ compact = false,
+ showBanner = true,
+ showPlaygroundButton = true,
+ desktopPreviewImage,
+ desktopPreviewImageAlt,
+ desktopPreviewImageWidth,
+ desktopPreviewImageHeight,
+ widePreview = false,
+ showTagline = true,
+ tagline,
+ taglineCompact = false,
+ showGitHubBanner = true,
+ mobilePreviewImage,
+ mobilePreviewImageAlt,
+ mobilePreviewImageWidth,
+ mobilePreviewImageHeight,
+ mobilePreviewImageCropTopPercent,
+}: {
+ title?: ReactNode;
+ subtitle?: ReactNode;
+ command?: string;
+ compact?: boolean;
+ showBanner?: boolean;
+ showPlaygroundButton?: boolean;
+ desktopPreviewImage?: string;
+ desktopPreviewImageAlt?: string;
+ desktopPreviewImageWidth?: number;
+ desktopPreviewImageHeight?: number;
+ widePreview?: boolean;
+ showTagline?: boolean;
+ tagline?: ReactNode;
+ taglineCompact?: boolean;
+ showGitHubBanner?: boolean;
+ mobilePreviewImage?: string;
+ mobilePreviewImageAlt?: string;
+ mobilePreviewImageWidth?: number;
+ mobilePreviewImageHeight?: number;
+ mobilePreviewImageCropTopPercent?: number;
+} = {}) {
const theme: HeroTheme = "light";
return (
-
-
-
-
+
+
+
+ {showTagline && {tagline}}
);
}
diff --git a/docs/app/(home)/sections/Navbar/Navbar.module.css b/docs/app/(home)/sections/Navbar/Navbar.module.css
index 7e0b2894d..948305efd 100644
--- a/docs/app/(home)/sections/Navbar/Navbar.module.css
+++ b/docs/app/(home)/sections/Navbar/Navbar.module.css
@@ -7,6 +7,47 @@
--site-primary-nav-hover-bg: var(--home-color-surface-soft);
}
+.navBanner {
+ display: flex;
+ min-height: 2.5rem;
+ align-items: center;
+ justify-content: center;
+ gap: 0.75rem;
+ padding: 0.5rem 1rem;
+ background: #000;
+ color: #f8fafc;
+ text-decoration: none;
+ font-family: "Inter", sans-serif;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+.navBannerText {
+ display: inline-flex;
+ align-items: center;
+}
+
+.navBannerButton {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ padding: 0.2rem 0.65rem;
+ border-radius: 999px;
+ border: 1px solid rgb(255 255 255 / 25%);
+ background: rgb(255 255 255 / 10%);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ transition: background-color 0.2s ease;
+}
+
+.navBannerStar {
+ color: rgb(234 197 79);
+}
+
+.navBanner:hover .navBannerButton {
+ background: rgb(255 255 255 / 18%);
+}
+
.desktopActions {
display: none;
}
@@ -139,6 +180,11 @@
}
@media (min-width: 1024px) {
+ .navBanner {
+ min-height: 2.75rem;
+ gap: 0.875rem;
+ }
+
.desktopActions {
display: flex;
}
diff --git a/docs/app/(home)/sections/Navbar/Navbar.tsx b/docs/app/(home)/sections/Navbar/Navbar.tsx
index ec2367df1..44013d44e 100644
--- a/docs/app/(home)/sections/Navbar/Navbar.tsx
+++ b/docs/app/(home)/sections/Navbar/Navbar.tsx
@@ -1,7 +1,34 @@
"use client";
import { SiteMarketingHeader } from "@/components/site-marketing-header";
+import { Star } from "lucide-react";
+import { usePathname } from "next/navigation";
+import styles from "./Navbar.module.css";
export function Navbar() {
- return
;
+ const pathname = usePathname();
+ const normalizedPath = pathname?.toLowerCase();
+ const showNavBanner = normalizedPath === "/" || normalizedPath === "/openclaw-os";
+
+ return (
+ <>
+ {showNavBanner && (
+
+
+ Thesys is going open source!
+
+
+
+ Star us on GitHub
+
+
+ )}
+
+ >
+ );
}
diff --git a/docs/app/(home)/sections/PossibilitiesSection/PossibilitiesSection.module.css b/docs/app/(home)/sections/PossibilitiesSection/PossibilitiesSection.module.css
index 49c722470..c0f545da1 100644
--- a/docs/app/(home)/sections/PossibilitiesSection/PossibilitiesSection.module.css
+++ b/docs/app/(home)/sections/PossibilitiesSection/PossibilitiesSection.module.css
@@ -10,7 +10,9 @@
.header {
display: flex;
- justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.75rem;
width: 100%;
margin-bottom: 2rem;
}
@@ -25,6 +27,16 @@
color: var(--openui-text-neutral-primary);
}
+.subtitle {
+ margin: 0;
+ max-width: 42rem;
+ text-align: center;
+ font-family: "Inter", sans-serif;
+ font-size: 16px;
+ line-height: 1.5;
+ color: var(--openui-text-neutral-secondary);
+}
+
.cardsContainer {
max-width: 80rem;
margin-inline: auto;
@@ -60,13 +72,6 @@
color: inherit;
text-decoration: none;
cursor: pointer;
- transition:
- transform 160ms ease,
- box-shadow 160ms ease;
-}
-
-.cardLink:hover {
- transform: translateY(-2px);
}
.cardLink:focus-visible {
@@ -74,10 +79,8 @@
outline-offset: 4px;
}
-.cardLink:hover .cardOverlay,
.cardLink:focus-visible .cardOverlay {
border-color: var(--openui-border-accent);
- box-shadow: var(--openui-shadow-l);
}
.cardInner {
@@ -99,6 +102,10 @@
mask-image: linear-gradient(to bottom, #000 0%, #000 82%, transparent 100%);
}
+.cardImagePlaceholder {
+ background: var(--openui-highlight-strong);
+}
+
.cardBody {
width: 100%;
padding: 1rem;
@@ -112,6 +119,12 @@
color: var(--openui-text-neutral-primary);
}
+.cardTitlePrefix {
+ display: block;
+ font-weight: 400;
+ color: var(--openui-text-neutral-secondary);
+}
+
.cardOverlay {
position: absolute;
inset: 0;
@@ -155,4 +168,19 @@
font-family: "Inter Display", sans-serif;
font-size: 18px;
}
+
+ .cardLink {
+ transition:
+ transform 160ms ease,
+ box-shadow 160ms ease;
+ }
+
+ .cardLink:hover {
+ transform: translateY(-2px);
+ }
+
+ .cardLink:hover .cardOverlay {
+ border-color: var(--openui-border-accent);
+ box-shadow: var(--openui-shadow-l);
+ }
}
diff --git a/docs/app/(home)/sections/PossibilitiesSection/PossibilitiesSection.tsx b/docs/app/(home)/sections/PossibilitiesSection/PossibilitiesSection.tsx
index 53b7e2254..50cf63e55 100644
--- a/docs/app/(home)/sections/PossibilitiesSection/PossibilitiesSection.tsx
+++ b/docs/app/(home)/sections/PossibilitiesSection/PossibilitiesSection.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, type ReactNode } from "react";
import styles from "./PossibilitiesSection.module.css";
const bottomTraysLightImg = "/homepage/tray-light.png";
@@ -45,29 +45,34 @@ const CARDS: readonly CardImageSet[] = [
},
];
-const MOBILE_CAROUSEL_CARDS = Array.from({ length: MOBILE_CAROUSEL_COPIES }, (_, copyIndex) =>
- CARDS.map((card) => ({
- ...card,
- key: `${card.title}-${copyIndex}`,
- })),
-).flat();
-
// ---------------------------------------------------------------------------
// Sub-components
// ---------------------------------------------------------------------------
-function Card({ title, image, href }: { title: string; image: string; href?: string }) {
+type CardProps = { title: string; titlePrefix?: string; image?: string; href?: string };
+
+function Card({ title, titlePrefix, image, href }: CardProps) {
const content = (
<>
-

+ {image ? (
+

+ ) : (
+
+ )}
-
{title}
+
+ {titlePrefix && {titlePrefix}}
+ {title}
+
@@ -95,9 +100,30 @@ function Card({ title, image, href }: { title: string; image: string; href?: str
// Main component
// ---------------------------------------------------------------------------
-export function PossibilitiesSection() {
+const DEFAULT_CARDS: CardProps[] = CARDS.map((card) => ({
+ title: card.title,
+ image: card.lightImage,
+ href: card.href,
+}));
+
+export function PossibilitiesSection({
+ title = "Endless possibilities. Built in realtime.",
+ tagline,
+ cards = DEFAULT_CARDS,
+}: {
+ title?: ReactNode;
+ tagline?: ReactNode;
+ cards?: CardProps[];
+} = {}) {
const mobileTrackRef = useRef
(null);
+ const mobileCards = Array.from({ length: MOBILE_CAROUSEL_COPIES }, (_, copyIndex) =>
+ cards.map((card, cardIndex) => ({
+ ...card,
+ key: `${card.title}-${cardIndex}-${copyIndex}`,
+ })),
+ ).flat();
+
useEffect(() => {
const track = mobileTrackRef.current;
if (!track) return;
@@ -148,22 +174,35 @@ export function PossibilitiesSection() {
-
Endless possibilities. Built in realtime.
+
{title}
+ {tagline &&
{tagline}
}
- {MOBILE_CAROUSEL_CARDS.map((card) => (
-
+ {mobileCards.map((card) => (
+
))}
- {CARDS.map((card) => (
-
+ {cards.map((card, index) => (
+
))}
diff --git a/docs/app/(home)/sections/StuckInChatSection/StuckInChatSection.module.css b/docs/app/(home)/sections/StuckInChatSection/StuckInChatSection.module.css
new file mode 100644
index 000000000..214c78b33
--- /dev/null
+++ b/docs/app/(home)/sections/StuckInChatSection/StuckInChatSection.module.css
@@ -0,0 +1,158 @@
+.section {
+ width: 100%;
+ padding-inline: 1.25rem;
+}
+
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ max-width: 75rem;
+ margin-inline: auto;
+}
+
+.heading {
+ margin: 0 0 0.75rem;
+ text-align: center;
+ font-family: "Inter Display", sans-serif;
+ font-size: 2rem;
+ font-weight: 600;
+ line-height: 1.2;
+ letter-spacing: -1px;
+ color: var(--openui-text-neutral-primary);
+ text-wrap: balance;
+}
+
+.description {
+ margin: 0 0 2.5rem;
+ max-width: 42rem;
+ text-align: center;
+ font-family: "Inter", sans-serif;
+ font-size: 0.9375rem;
+ line-height: 1.5;
+ color: var(--openui-text-neutral-secondary);
+ text-wrap: balance;
+}
+
+.cards {
+ display: grid;
+ width: 100%;
+ grid-template-columns: 1fr;
+ gap: 0.75rem;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.card {
+ display: flex;
+ flex-direction: column;
+ gap: 0.625rem;
+ padding: 1.25rem;
+ border: 1px solid var(--openui-border-default);
+ border-radius: 0.875rem;
+ background: var(--openui-foreground);
+ box-shadow: var(--openui-shadow-m);
+}
+
+.cardIcon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ border-radius: var(--openui-radius-full);
+ background: var(--openui-text-neutral-primary);
+ color: var(--openui-foreground);
+}
+
+.cardTitle {
+ margin: 0;
+ font-family: "Inter Display", sans-serif;
+ font-size: 1rem;
+ font-weight: 600;
+ line-height: 1.2;
+ color: var(--openui-text-neutral-primary);
+}
+
+.cardDescription {
+ margin: 0;
+ font-family: "Inter", sans-serif;
+ font-size: 0.875rem;
+ line-height: 1.45;
+ color: var(--openui-text-neutral-secondary);
+ text-wrap: balance;
+}
+
+.cta {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+ margin-top: 2.5rem;
+}
+
+.installButton {
+ width: 100%;
+ max-width: 320px;
+ justify-content: space-between;
+}
+
+.ctaSub {
+ margin: 0;
+ text-align: center;
+ font-family: "Inter Display", sans-serif;
+ font-size: 1.125rem;
+ font-weight: 500;
+ color: var(--openui-text-neutral-primary);
+}
+
+
+@media (min-width: 1024px) {
+ .section {
+ padding-inline: 2rem;
+ }
+
+ .heading {
+ font-size: 2.5rem;
+ letter-spacing: -2px;
+ margin-bottom: 1rem;
+ }
+
+ .description {
+ font-size: 1.125rem;
+ margin-bottom: 4rem;
+ }
+
+ .cards {
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1.25rem;
+ }
+
+ .card {
+ padding: 1.5rem;
+ gap: 0.75rem;
+ }
+
+ .cardTitle {
+ font-size: 1.0625rem;
+ }
+
+ .cardDescription {
+ font-size: 0.9375rem;
+ }
+
+ .cta {
+ margin-top: 4rem;
+ }
+
+ .ctaSub {
+ font-size: 1.25rem;
+ }
+
+ .installButton {
+ width: auto;
+ max-width: none;
+ justify-content: center;
+ }
+}
diff --git a/docs/app/(home)/sections/StuckInChatSection/StuckInChatSection.tsx b/docs/app/(home)/sections/StuckInChatSection/StuckInChatSection.tsx
new file mode 100644
index 000000000..e04ee9224
--- /dev/null
+++ b/docs/app/(home)/sections/StuckInChatSection/StuckInChatSection.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import { EyeSlash, LockSimple, Scroll } from "@phosphor-icons/react";
+import { NpmButton } from "../HeroSection/HeroSection";
+import styles from "./StuckInChatSection.module.css";
+
+const ICON_SIZE = 18;
+
+const NEGATIVES = [
+ {
+ title: "No visibility",
+ description: "Agent actions and context are buried in chat. You can't see what it's doing.",
+ icon: ,
+ },
+ {
+ title: "No structure",
+ description: "Everything becomes one long scroll. Work gets scattered and hard to revisit.",
+ icon: ,
+ },
+ {
+ title: "No control",
+ description: "You can't manage tasks, permissions, or execution. Only send messages.",
+ icon: ,
+ },
+];
+
+export function StuckInChatSection({ installCommand }: { installCommand: string }) {
+ return (
+
+
+
Is your agent still stuck in Telegram?
+
+ OpenClaw is powerful, but you're limiting it by keeping it inside a chat thread.
+
+
+
+ {NEGATIVES.map((item) => (
+ -
+
{item.icon}
+ {item.title}
+ {item.description}
+
+ ))}
+
+
+
+
Setup OpenClaw OS in under a minute
+
+
+
+
+ );
+}
diff --git a/docs/app/api/demo/github/stream/route.ts b/docs/app/api/demo/github/stream/route.ts
index fd3648177..753827244 100644
--- a/docs/app/api/demo/github/stream/route.ts
+++ b/docs/app/api/demo/github/stream/route.ts
@@ -1,15 +1,25 @@
import { BASE_URL } from "@/lib/source";
-import { generatePrompt, type PromptSpec } from "@openuidev/lang-core";
+import { generatePrompt, type PromptSpec, type ToolSpec } from "@openuidev/lang-core";
import { readFileSync } from "fs";
import { type NextRequest } from "next/server";
import { join } from "path";
-import { GITHUB_DEMO_MODEL } from "../../../../demo/github/constants";
-import {
- GITHUB_ADDITIONAL_RULES,
- GITHUB_PREAMBLE,
- GITHUB_TOOL_EXAMPLES,
-} from "../../../../demo/github/github/prompt-config";
-import { GITHUB_TOOL_SPECS } from "../../../../demo/github/github/types";
+
+/**
+ * NOTE:
+ * This route used to import its GitHub demo prompt config from `demo/github/*`,
+ * but that folder doesn't exist in this repo. Keep the endpoint buildable by
+ * providing lightweight, local defaults here.
+ */
+const GITHUB_DEMO_MODEL =
+ process.env.GITHUB_DEMO_MODEL ??
+ process.env.OPENROUTER_MODEL ??
+ // A reasonable OpenRouter default; can be overridden via env vars.
+ "openai/gpt-4.1-mini";
+
+const GITHUB_PREAMBLE = `You are a helpful coding assistant.`;
+const GITHUB_ADDITIONAL_RULES: string[] = [];
+const GITHUB_TOOL_EXAMPLES: string[] = [];
+const GITHUB_TOOL_SPECS: Array = [];
// ── Component spec from generated JSON ────────────────────────────────────
diff --git a/docs/app/components/blocks/_components/ClientOnly.tsx b/docs/app/components/blocks/_components/ClientOnly.tsx
index f3e56b07d..617e0693a 100644
--- a/docs/app/components/blocks/_components/ClientOnly.tsx
+++ b/docs/app/components/blocks/_components/ClientOnly.tsx
@@ -4,6 +4,8 @@ import { useEffect, useState, type ReactNode } from "react";
export default function ClientOnly({ children }: { children: ReactNode }) {
const [mounted, setMounted] = useState(false);
- useEffect(() => setMounted(true), []);
+ useEffect(() => {
+ queueMicrotask(() => setMounted(true));
+ }, []);
return mounted ? <>{children}> : null;
}
diff --git a/docs/app/components/components/preview/ColorSwatchesPreviewSection.tsx b/docs/app/components/components/preview/ColorSwatchesPreviewSection.tsx
index 15892fe64..94bababdb 100644
--- a/docs/app/components/components/preview/ColorSwatchesPreviewSection.tsx
+++ b/docs/app/components/components/preview/ColorSwatchesPreviewSection.tsx
@@ -40,7 +40,7 @@ export default function ColorSwatchesPreviewSection({
values[tokenName] = computedStyles.getPropertyValue(tokenName).trim() || "var(...)";
});
});
- setTokenValues(values);
+ queueMicrotask(() => setTokenValues(values));
}, [rows]);
return (
diff --git a/docs/app/components/components/preview/pages/MotionPreviewPage.tsx b/docs/app/components/components/preview/pages/MotionPreviewPage.tsx
index 5b33ee308..c1bcce113 100644
--- a/docs/app/components/components/preview/pages/MotionPreviewPage.tsx
+++ b/docs/app/components/components/preview/pages/MotionPreviewPage.tsx
@@ -24,7 +24,7 @@ export default function MotionPreviewPage() {
computedStyles.getPropertyValue("--motion-ease-standard").trim() || "ease";
map["--motion-ease-emphasized"] =
computedStyles.getPropertyValue("--motion-ease-emphasized").trim() || "ease-in-out";
- setTokenValues(map);
+ queueMicrotask(() => setTokenValues(map));
}, []);
return (
diff --git a/docs/app/components/components/preview/pages/RadiusPreviewPage.tsx b/docs/app/components/components/preview/pages/RadiusPreviewPage.tsx
index ed2713949..539321685 100644
--- a/docs/app/components/components/preview/pages/RadiusPreviewPage.tsx
+++ b/docs/app/components/components/preview/pages/RadiusPreviewPage.tsx
@@ -41,7 +41,7 @@ export default function RadiusPreviewPage() {
RADIUS_TOKENS.forEach((token) => {
map[token] = computedStyles.getPropertyValue(token).trim() || "0";
});
- setTokenValues(map);
+ queueMicrotask(() => setTokenValues(map));
}, []);
const radiusItems = RADIUS_TOKENS.map((token) => ({
diff --git a/docs/app/components/components/preview/pages/ShadowsPreviewPage.tsx b/docs/app/components/components/preview/pages/ShadowsPreviewPage.tsx
index c81e6c755..7fdc6462a 100644
--- a/docs/app/components/components/preview/pages/ShadowsPreviewPage.tsx
+++ b/docs/app/components/components/preview/pages/ShadowsPreviewPage.tsx
@@ -120,7 +120,7 @@ export default function ShadowsPreviewPage() {
const rawValue = computedStyles.getPropertyValue(cssVar).trim() || "none";
map[cssVar] = normalizeShadowValueToOklch(rawValue);
});
- setTokenValues(map);
+ queueMicrotask(() => setTokenValues(map));
}, []);
const shadowItems = SHADOW_TOKENS.map(({ cssVar }) => ({
diff --git a/docs/app/components/components/preview/pages/SpacingPreviewPage.tsx b/docs/app/components/components/preview/pages/SpacingPreviewPage.tsx
index 6d93b2b39..6f2a7e517 100644
--- a/docs/app/components/components/preview/pages/SpacingPreviewPage.tsx
+++ b/docs/app/components/components/preview/pages/SpacingPreviewPage.tsx
@@ -34,7 +34,7 @@ export default function SpacingPreviewPage() {
SPACING_TOKENS.forEach((token) => {
map[token] = computedStyles.getPropertyValue(token).trim() || "0px";
});
- setTokenValues(map);
+ queueMicrotask(() => setTokenValues(map));
}, []);
const spacingItems = SPACING_TOKENS.map((token) => ({
diff --git a/docs/app/demo/github/bookmarks/store.ts b/docs/app/demo/github/bookmarks/store.ts
deleted file mode 100644
index 769fc9dbb..000000000
--- a/docs/app/demo/github/bookmarks/store.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { Bookmark } from "../github/types";
-
-function storageKey(username: string): string {
- return `openui-gh-bookmarks-${username}`;
-}
-
-export function getBookmarks(username: string): Bookmark[] {
- if (typeof window === "undefined") return [];
- try {
- const raw = localStorage.getItem(storageKey(username));
- return raw ? JSON.parse(raw) : [];
- } catch {
- return [];
- }
-}
-
-export function saveBookmark(
- username: string,
- repo: string,
- note?: string,
- tag?: string,
-): Bookmark {
- const bookmarks = getBookmarks(username);
- const existing = bookmarks.findIndex((b) => b.repo === repo);
- const bookmark: Bookmark = {
- repo,
- note: note ?? "",
- tag: tag ?? "important",
- created_at: new Date().toISOString(),
- };
-
- if (existing >= 0) {
- bookmarks[existing] = bookmark;
- } else {
- bookmarks.unshift(bookmark);
- }
-
- localStorage.setItem(storageKey(username), JSON.stringify(bookmarks));
- return bookmark;
-}
-
-export function deleteBookmark(username: string, repo: string): { success: boolean } {
- const bookmarks = getBookmarks(username);
- const filtered = bookmarks.filter((b) => b.repo !== repo);
- localStorage.setItem(storageKey(username), JSON.stringify(filtered));
- return { success: true };
-}
diff --git a/docs/app/demo/github/components/ConversationPanel/ConversationPanel.css b/docs/app/demo/github/components/ConversationPanel/ConversationPanel.css
deleted file mode 100644
index 83b7b01df..000000000
--- a/docs/app/demo/github/components/ConversationPanel/ConversationPanel.css
+++ /dev/null
@@ -1,279 +0,0 @@
-.conv-panel {
- display: flex;
- flex-direction: column;
- width: 360px;
- min-width: 360px;
- border-left: 1px solid var(--openui-border-default);
- background: var(--openui-background);
-}
-
-.conv-header {
- padding: var(--openui-space-m) var(--openui-space-m-l);
- border-bottom: 1px solid var(--openui-border-default);
- font-size: 13px;
- font-weight: 600;
- color: var(--openui-text-neutral-primary);
- font-family: var(--openui-font-body);
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.conv-collapse-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- background: transparent;
- border: none;
- border-radius: var(--openui-radius-s);
- color: var(--openui-text-neutral-tertiary);
- cursor: pointer;
- transition:
- background-color 0.15s,
- color 0.15s;
-}
-
-.conv-collapse-btn:hover {
- background: var(--openui-highlight);
- color: var(--openui-text-neutral-primary);
-}
-
-/* Collapsed state */
-.conv-collapsed {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: var(--openui-space-m) var(--openui-space-xs);
- border-left: 1px solid var(--openui-border-default);
- background: var(--openui-background);
-}
-
-.conv-expand-btn {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: var(--openui-space-s);
- padding: var(--openui-space-s);
- background: transparent;
- border: none;
- border-radius: var(--openui-radius-m);
- color: var(--openui-text-neutral-tertiary);
- cursor: pointer;
- transition:
- background-color 0.15s,
- color 0.15s;
-}
-
-.conv-expand-btn:hover {
- background: var(--openui-highlight);
- color: var(--openui-text-neutral-primary);
-}
-
-.conv-badge {
- font-size: 10px;
- font-weight: 600;
- background: var(--openui-interactive-accent-default);
- color: var(--openui-text-accent-primary);
- border-radius: var(--openui-radius-full);
- min-width: 18px;
- height: 18px;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0 4px;
- font-family: var(--openui-font-body);
-}
-
-.conv-messages {
- flex: 1;
- overflow: auto;
- padding: var(--openui-space-m) var(--openui-space-m-l);
-}
-
-.conv-msg {
- margin-bottom: var(--openui-space-m);
-}
-
-/* User message */
-.conv-user-bubble {
- background: var(--openui-chat-user-response-bg);
- color: var(--openui-chat-user-response-text);
- padding: var(--openui-space-s) var(--openui-space-m);
- border-radius: var(--openui-radius-l);
- font-size: 13px;
- line-height: 1.4;
- margin-left: 40px;
- box-shadow: var(--openui-shadow-s);
- font-family: var(--openui-font-body);
-}
-
-/* Assistant message */
-.conv-assistant {
- margin-right: var(--openui-space-xl);
-}
-
-.conv-assistant-bubble {
- background: var(--openui-chat-assistant-response-bg);
- border: 1px solid var(--openui-border-default);
- padding: var(--openui-space-s) var(--openui-space-m);
- border-radius: var(--openui-radius-l);
- font-size: 13px;
- line-height: 1.5;
- color: var(--openui-chat-assistant-response-text);
- font-family: var(--openui-font-body);
-}
-
-.conv-assistant-bubble .openui-markdown-renderer {
- font-size: 13px;
- line-height: 1.5;
-}
-
-.conv-assistant-bubble .openui-markdown-renderer p {
- margin: 0 0 var(--openui-space-xs) 0;
-}
-
-.conv-assistant-bubble .openui-markdown-renderer p:last-child {
- margin-bottom: 0;
-}
-
-.conv-assistant-bubble .openui-markdown-renderer ul,
-.conv-assistant-bubble .openui-markdown-renderer ol {
- margin: 0 0 var(--openui-space-xs) 0;
- padding-left: var(--openui-space-m-l);
-}
-
-.conv-assistant-bubble .openui-markdown-renderer li {
- margin-bottom: var(--openui-space-3xs);
-}
-
-/* Code badge */
-.conv-code-badge {
- display: inline-flex;
- align-items: center;
- gap: var(--openui-space-2xs);
- padding: var(--openui-space-3xs) var(--openui-space-s);
- border-radius: var(--openui-radius-m);
- font-size: 11px;
- background: var(--openui-success-background);
- color: var(--openui-text-success-primary);
- margin-top: var(--openui-space-2xs);
- font-family: var(--openui-font-body);
-}
-
-.conv-code-updating {
- background: var(--openui-purple-background);
- color: var(--openui-text-purple-primary);
-}
-
-/* Thinking state */
-.conv-thinking {
- background: var(--openui-chat-assistant-response-bg);
- border: 1px solid var(--openui-border-default);
- padding: var(--openui-space-s) var(--openui-space-m);
- border-radius: var(--openui-radius-l);
- font-size: 13px;
- color: var(--openui-text-neutral-secondary);
- font-family: var(--openui-font-body);
-}
-
-.conv-empty {
- font-size: 12px;
- color: var(--openui-text-neutral-tertiary);
- font-style: italic;
-}
-
-/* Input area */
-.conv-input-area {
- padding: var(--openui-space-m) var(--openui-space-m-l);
- border-top: 1px solid var(--openui-border-default);
- background: var(--openui-foreground);
-}
-
-.conv-input-row {
- display: flex;
- gap: var(--openui-space-s);
-}
-
-.conv-input {
- flex: 1;
- padding: var(--openui-space-s) var(--openui-space-m);
- border: 1px solid var(--openui-border-default);
- border-radius: var(--openui-radius-m);
- font-size: 13px;
- outline: none;
- background: var(--openui-sunk);
- color: var(--openui-text-neutral-primary);
- font-family: var(--openui-font-body);
- transition:
- border-color 0.15s,
- box-shadow 0.15s;
-}
-
-.conv-input:focus-visible {
- border-color: var(--openui-border-interactive-emphasis);
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--openui-border-accent-emphasis) 22%, transparent);
-}
-
-.openui-icon-button.conv-send-btn,
-.openui-icon-button.conv-stop-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- border: none;
- border-radius: var(--openui-radius-m);
- cursor: pointer;
- transition:
- background-color 0.15s,
- color 0.15s,
- border-color 0.15s;
-}
-
-.openui-icon-button.conv-send-btn:focus-visible,
-.openui-icon-button.conv-stop-btn:focus-visible {
- outline: 2px solid var(--openui-border-accent);
- outline-offset: 2px;
-}
-
-.openui-icon-button.conv-send-btn {
- background: var(--openui-interactive-accent-default);
- color: var(--openui-text-accent-primary);
-}
-
-.openui-icon-button.conv-send-btn:hover:not(:disabled) {
- background: var(--openui-interactive-accent-hover);
-}
-
-.openui-icon-button.conv-send-btn:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-.openui-icon-button.conv-stop-btn {
- background: var(--openui-highlight-subtle);
- color: var(--openui-text-neutral-secondary);
- border: 1px solid var(--openui-border-default);
-}
-
-.openui-icon-button.conv-stop-btn:hover:not(:disabled) {
- background: var(--openui-highlight);
- color: var(--openui-text-neutral-primary);
- border-color: var(--openui-border-interactive);
-}
-
-@media (max-width: 768px) {
- .conv-panel {
- width: 100%;
- min-width: 0;
- border-left: none;
- border-top: 1px solid var(--openui-border-default);
- max-height: 300px;
- }
-
- .conv-collapsed {
- display: none;
- }
-}
diff --git a/docs/app/demo/github/components/ConversationPanel/ConversationPanel.tsx b/docs/app/demo/github/components/ConversationPanel/ConversationPanel.tsx
deleted file mode 100644
index f9aaabbee..000000000
--- a/docs/app/demo/github/components/ConversationPanel/ConversationPanel.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-"use client";
-
-import { IconButton } from "@openuidev/react-ui";
-import { MarkDownRenderer } from "@openuidev/react-ui/MarkDownRenderer";
-import { ChevronRight, MessageSquare, Send, Square } from "lucide-react";
-import { useEffect, useRef, useState } from "react";
-import type { ChatMessage } from "../../constants";
-import "./ConversationPanel.css";
-
-type ConversationPanelProps = {
- messages: ChatMessage[];
- streamingText: string;
- isStreaming: boolean;
- elapsed: number | null;
- onSend: (text: string) => void;
- onStop: () => void;
- hasDashboard: boolean;
- responseHasCode: boolean;
-};
-
-export function ConversationPanel({
- messages,
- streamingText,
- isStreaming,
- elapsed,
- onSend,
- onStop,
- hasDashboard,
- responseHasCode,
-}: ConversationPanelProps) {
- const [input, setInput] = useState("");
- const [collapsed, setCollapsed] = useState(false);
- const inputRef = useRef(null);
- const scrollRef = useRef(null);
-
- const canSend = input.trim().length > 0 && !isStreaming;
-
- useEffect(() => {
- scrollRef.current?.scrollIntoView({ behavior: "smooth" });
- }, [messages, streamingText]);
-
- useEffect(() => {
- if (!isStreaming) inputRef.current?.focus();
- }, [isStreaming]);
-
- const handleSend = () => {
- if (!canSend) return;
- onSend(input.trim());
- setInput("");
- };
-
- if (collapsed) {
- return (
-
-
-
- );
- }
-
- return (
-
-
- Conversation
-
-
-
-
- {messages.map((msg, i) => (
-
- {msg.role === "user" ? (
-
{msg.content}
- ) : (
-
- {/* Text response */}
- {msg.text && (
-
-
-
- )}
-
- {/* Dashboard updated badge */}
- {msg.hasCode &&
✓ dashboard updated}
-
- {/* Empty response */}
- {!msg.text && !msg.hasCode &&
(empty response)
}
-
- )}
-
- ))}
-
- {/* Streaming indicator */}
- {isStreaming && (
-
-
- {/* Streaming text or thinking indicator */}
- {streamingText ? (
-
-
-
- ) : (
-
- {elapsed
- ? `${(elapsed / 1000).toFixed(1)}s — ${responseHasCode ? "writing openui-lang..." : "thinking..."}`
- : "thinking..."}
-
- )}
-
- {/* Dashboard updating indicator */}
- {responseHasCode && (
-
⟳ updating dashboard...
- )}
-
-
- )}
-
-
-
-
- {/* Input */}
-
-
- setInput(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter" && canSend) handleSend();
- }}
- placeholder={hasDashboard ? "Edit or ask..." : "Describe a dashboard..."}
- disabled={isStreaming}
- />
- {isStreaming ? (
- }
- variant="tertiary"
- size="medium"
- onClick={onStop}
- aria-label="Stop generation"
- />
- ) : (
- }
- variant="primary"
- size="medium"
- onClick={handleSend}
- disabled={!canSend}
- aria-label="Send message"
- />
- )}
-
-
-
- );
-}
diff --git a/docs/app/demo/github/components/GitHubConnect/GitHubConnect.css b/docs/app/demo/github/components/GitHubConnect/GitHubConnect.css
deleted file mode 100644
index 635378e7b..000000000
--- a/docs/app/demo/github/components/GitHubConnect/GitHubConnect.css
+++ /dev/null
@@ -1,534 +0,0 @@
-/* ── Layout ── */
-
-.gh-connect {
- width: fit-content;
- margin: 0 auto;
- padding: 120px 16px;
-}
-
-.gh-builder {
- display: flex;
- flex-direction: column;
- gap: var(--openui-space-2xl);
-}
-
-/* ── Brand ── */
-
-.gh-brand {
- display: inline-flex;
- align-items: center;
- gap: var(--openui-space-s);
- color: var(--openui-text-neutral-primary);
- font: var(--openui-text-body-lg-heavy);
- letter-spacing: var(--openui-text-body-lg-heavy-letter-spacing);
-}
-
-.gh-brandIcon {
- display: inline-flex;
- width: 24px;
- height: 24px;
- align-items: center;
- justify-content: center;
-}
-
-/* ── Sentence block ── */
-
-.gh-sentence {
- display: flex;
- flex-direction: column;
- gap: var(--openui-space-s);
-}
-
-.gh-sentenceLine {
- margin: 0;
- color: var(--openui-text-neutral-primary);
- font-family: var(--openui-font-body);
- font-size: 32px;
- font-weight: 500;
- line-height: 1.5;
- letter-spacing: -0.03em;
-}
-
-.gh-sentenceIdentity {
- font-family: var(--openui-font-body);
- font-size: 32px;
- font-weight: 500;
- line-height: 1.5;
- letter-spacing: -0.03em;
-}
-
-.gh-sentenceLine-secondary {
- margin-top: var(--openui-space-m);
- color: var(--openui-text-neutral-secondary);
-}
-
-/* ── Username input (inline) ── */
-
-.gh-inputWrap {
- display: inline-flex;
- align-items: center;
- gap: 0.25em;
- padding-bottom: 0.125em;
- border-bottom: 1.5px solid var(--openui-border-interactive);
- color: var(--openui-text-neutral-primary);
- font: inherit;
- vertical-align: middle;
- transition: border-color 0.2s ease;
-}
-
-.gh-inputWrap:focus-within {
- border-bottom-color: var(--openui-border-interactive-emphasis);
-}
-
-.gh-inputPrefix {
- color: var(--openui-text-neutral-secondary);
- user-select: none;
-}
-
-.gh-inputSizer {
- display: inline-grid;
- min-width: 4ch;
- max-width: min(16ch, 50vw);
-}
-
-.gh-inputGhost,
-.gh-inputField {
- grid-area: 1 / 1;
- font: inherit;
- line-height: inherit;
-}
-
-.gh-inputGhost {
- visibility: hidden;
- white-space: pre;
-}
-
-.gh-inputField {
- width: 100%;
- min-width: 0;
- padding: 0;
- border: none;
- background: transparent;
- color: inherit;
- font: inherit;
- outline: none;
-}
-
-.gh-inputField::placeholder {
- color: var(--openui-text-neutral-tertiary);
-}
-
-.gh-inputClear {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- border: none;
- background: transparent;
- color: var(--openui-text-neutral-secondary);
- cursor: pointer;
- opacity: 0;
- transition:
- opacity 0.15s ease,
- color 0.15s ease;
-}
-
-.gh-inputWrap:hover .gh-inputClear,
-.gh-inputWrap:focus-within .gh-inputClear {
- opacity: 1;
-}
-
-.gh-inputClear:hover {
- color: var(--openui-text-neutral-primary);
-}
-
-/* ── Inline ellipsis hint (before topic is selected) ── */
-
-.gh-selectHintInline {
- color: var(--openui-text-neutral-tertiary);
- letter-spacing: 0.1em;
-}
-
-/* ── Chip (selected identity / focus) ── */
-
-.gh-chip {
- display: inline-flex;
- align-items: center;
- gap: var(--openui-space-s);
- padding: 10px 18px 10px 10px;
- border: 1px solid color-mix(in srgb, var(--gh-tone-fill) 22%, var(--gh-tone-border));
- border-radius: var(--openui-radius-full);
- background: color-mix(in srgb, var(--gh-tone-bg) 52%, var(--openui-foreground));
- color: var(--openui-text-neutral-primary);
- box-shadow: var(--openui-shadow-s);
- font-family: var(--openui-font-body);
- font-size: 32px;
- font-weight: 500;
- line-height: 1;
- letter-spacing: -0.03em;
- white-space: nowrap;
- vertical-align: middle;
-}
-
-.gh-chip-neutral {
- --gh-tone-fill: var(--openui-text-neutral-secondary);
- --gh-tone-bg: var(--openui-highlight-subtle);
- --gh-tone-border: var(--openui-border-default);
-}
-
-.gh-chipLeading {
- display: inline-flex;
- flex-shrink: 0;
- align-items: center;
- justify-content: center;
-}
-
-.gh-chipLabel {
- white-space: nowrap;
-}
-
-.gh-chipClear {
- display: inline-flex;
- flex-shrink: 0;
- align-items: center;
- justify-content: center;
- padding: 0;
- border: none;
- background: transparent;
- color: var(--openui-text-neutral-secondary);
- cursor: pointer;
- transition: color 0.15s ease;
-}
-
-.gh-chipClear:hover {
- color: var(--openui-text-neutral-primary);
-}
-
-.gh-chipAvatar,
-.gh-chipMonogram {
- width: 1.25em;
- height: 1.25em;
- border-radius: var(--openui-radius-full);
-}
-
-.gh-chipAvatar {
- flex-shrink: 0;
-}
-
-.gh-chipMonogram {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- background: var(--openui-highlight);
- color: var(--openui-text-neutral-secondary);
- font-size: 0.65em;
- font-weight: 600;
-}
-
-/* ── Developer picker ── */
-
-.gh-devPicker {
- display: flex;
- flex-direction: column;
- gap: var(--openui-space-m);
-}
-
-.gh-devPickerCaption {
- margin: 0;
- color: var(--openui-text-neutral-tertiary);
- font: var(--openui-text-label-sm-heavy);
-}
-
-/* ── Option list (shared) ── */
-
-.gh-choiceList {
- display: flex;
- flex-direction: column;
- gap: 0.625rem;
-}
-
-.gh-choiceList-developer {
- max-width: 18rem;
-}
-
-.gh-choiceList-focus {
- max-width: 20rem;
-}
-
-.gh-choiceOption {
- display: flex;
- width: 100%;
- align-items: center;
- justify-content: space-between;
- gap: var(--openui-space-m);
- padding: 0.75rem 1rem;
- border: 1px solid color-mix(in srgb, var(--gh-tone-fill) 18%, var(--gh-tone-border));
- border-radius: calc(var(--openui-radius-3xl) + 4px);
- background: color-mix(in srgb, var(--gh-tone-bg) 34%, var(--openui-foreground));
- color: var(--openui-text-neutral-primary);
- box-shadow: var(--openui-shadow-s);
- cursor: pointer;
- text-align: left;
- transition:
- transform 0.2s ease,
- border-color 0.2s ease,
- background-color 0.2s ease;
-}
-
-.gh-choiceOption:hover,
-.gh-choiceOption-selected {
- transform: translateY(-1px);
- border-color: color-mix(
- in srgb,
- var(--gh-tone-fill) 36%,
- var(--openui-border-interactive-emphasis)
- );
- background: color-mix(in srgb, var(--gh-tone-bg) 54%, var(--openui-foreground));
-}
-
-.gh-choiceOptionPrimary {
- display: inline-flex;
- min-width: 0;
- align-items: center;
- gap: var(--openui-space-s);
-}
-
-.gh-choiceOptionLeading {
- display: inline-flex;
- flex-shrink: 0;
- align-items: center;
- justify-content: center;
-}
-
-.gh-choiceOptionLabel {
- color: var(--openui-text-neutral-primary);
- font: var(--openui-text-body-default-heavy);
- letter-spacing: var(--openui-text-body-default-heavy-letter-spacing);
-}
-
-.gh-choiceOptionCheck {
- flex-shrink: 0;
- color: var(--gh-tone-fill);
-}
-
-.gh-choiceAvatar {
- width: 1.125rem;
- height: 1.125rem;
- border-radius: var(--openui-radius-full);
- flex-shrink: 0;
-}
-
-.gh-choiceIconBubble {
- display: inline-flex;
- width: 1.75rem;
- height: 1.75rem;
- align-items: center;
- justify-content: center;
- border-radius: var(--openui-radius-full);
- background: color-mix(in srgb, var(--gh-tone-fill) 14%, var(--openui-highlight-subtle));
- color: var(--gh-tone-fill);
-}
-
-.gh-choiceSwatch {
- display: inline-flex;
- width: 0.875rem;
- height: 0.875rem;
- border-radius: 50%;
- background: var(--gh-tone-fill);
-}
-
-/* ── Focus picker ── */
-
-.gh-focusPicker {
- display: flex;
- flex-direction: column;
- gap: var(--openui-space-m);
-}
-
-.gh-focusPickerCaption {
- margin: 0;
- color: var(--openui-text-neutral-tertiary);
- font: var(--openui-text-label-sm-heavy);
-}
-
-/* ── Error ── */
-
-.gh-error {
- color: var(--openui-text-danger-primary);
- font: var(--openui-text-label-default-heavy);
- letter-spacing: var(--openui-text-label-default-heavy-letter-spacing);
-}
-
-/* ── Actions ── */
-
-.gh-actions {
- display: flex;
- align-items: center;
- gap: var(--openui-space-m-l);
-}
-
-.openui-button-base.gh-cta {
- min-height: 3rem;
- padding: 0.75rem 1.25rem;
- border: none;
- border-radius: var(--openui-radius-full);
- background: var(--openui-interactive-accent-default);
- color: var(--openui-text-accent-primary);
- box-shadow: var(--openui-shadow-l);
- font: var(--openui-text-body-large-heavy);
- letter-spacing: var(--openui-text-body-default-heavy-letter-spacing);
-}
-
-.openui-button-base.gh-cta:hover:not(:disabled) {
- background: var(--openui-interactive-accent-hover);
-}
-
-.gh-startOver {
- border: none;
- background: transparent;
- color: var(--openui-text-neutral-secondary);
- min-height: 3rem;
- padding: 0.75rem 0;
- font: var(--openui-text-body-large-heavy);
- letter-spacing: var(--openui-text-body-default-heavy-letter-spacing);
- cursor: pointer;
- transition: color 0.2s ease;
-}
-
-.gh-startOver:hover:not(:disabled) {
- color: var(--openui-text-neutral-primary);
-}
-
-.gh-startOver:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-/* ── Focus ring ── */
-
-.gh-cta:focus-visible,
-.gh-inputClear:focus-visible,
-.gh-chipClear:focus-visible,
-.gh-choiceOption:focus-visible,
-.gh-startOver:focus-visible {
- outline: 2px solid var(--openui-border-accent);
- outline-offset: 3px;
-}
-
-/* ── Tone tokens ── */
-
-.gh-tone-green,
-.gh-tone-mint {
- --gh-tone-fill: var(--openui-text-success-primary);
- --gh-tone-bg: var(--openui-success-background);
- --gh-tone-border: var(--openui-border-success);
-}
-
-.gh-tone-purple,
-.gh-tone-violet {
- --gh-tone-fill: var(--openui-text-purple-primary);
- --gh-tone-bg: var(--openui-purple-background);
- --gh-tone-border: var(--openui-border-accent);
-}
-
-.gh-tone-red,
-.gh-tone-rose {
- --gh-tone-fill: var(--openui-text-danger-primary);
- --gh-tone-bg: var(--openui-danger-background);
- --gh-tone-border: var(--openui-border-danger);
-}
-
-.gh-tone-blue {
- --gh-tone-fill: var(--openui-text-info-primary);
- --gh-tone-bg: var(--openui-info-background);
- --gh-tone-border: var(--openui-border-info);
-}
-
-.gh-tone-amber,
-.gh-tone-peach {
- --gh-tone-fill: var(--openui-text-alert-primary);
- --gh-tone-bg: var(--openui-alert-background);
- --gh-tone-border: var(--openui-border-alert);
-}
-
-.gh-tone-pink {
- --gh-tone-fill: var(--openui-text-pink-primary);
- --gh-tone-bg: var(--openui-pink-background);
- --gh-tone-border: color-mix(
- in srgb,
- var(--openui-text-pink-primary) 24%,
- var(--openui-border-default)
- );
-}
-
-/* ── Visually hidden ── */
-
-.gh-visually-hidden {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
-}
-
-/* ── Animation ── */
-
-@keyframes gh-step-fade-in {
- from {
- opacity: 0;
- transform: translateY(8px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.gh-step-appear {
- animation: gh-step-fade-in 200ms ease;
-}
-
-/* ── Mobile ── */
-
-@media (max-width: 768px) {
- .gh-connect {
- padding: var(--openui-space-3xl) var(--openui-space-m-l) var(--openui-space-2xl);
- }
-
- .gh-builder {
- gap: var(--openui-space-xl);
- }
-
- .gh-sentenceLine {
- font-size: 32px;
- }
-
- .gh-sentenceIdentity {
- font-size: 32px;
- }
-
- .gh-chip {
- font-size: 24px;
- padding: 8px 14px 8px 8px;
- }
-
- .gh-choiceList-developer,
- .gh-choiceList-focus {
- max-width: 100%;
- }
-
- .gh-choiceOption {
- width: 100%;
- }
-
- .gh-actions {
- margin-top: 32px;
- flex-wrap: wrap;
- align-items: flex-start;
- gap: var(--openui-space-l);
- }
-}
diff --git a/docs/app/demo/github/components/GitHubConnect/GitHubConnect.tsx b/docs/app/demo/github/components/GitHubConnect/GitHubConnect.tsx
deleted file mode 100644
index 4b4c20f14..000000000
--- a/docs/app/demo/github/components/GitHubConnect/GitHubConnect.tsx
+++ /dev/null
@@ -1,444 +0,0 @@
-"use client";
-
-import { GitHubIcon } from "@/components/brand-logo";
-import { Button } from "@openuidev/react-ui";
-import {
- Activity,
- Check,
- CircleDot,
- Code2,
- GitPullRequest,
- Hexagon,
- Search,
- X,
- type LucideIcon,
-} from "lucide-react";
-import { useCallback, useEffect, useRef, useState, type FormEvent } from "react";
-import {
- GITHUB_STARTERS,
- type GitHubStarterIconKey,
- type GitHubStarterTone,
-} from "../../constants";
-import "./GitHubConnect.css";
-
-type GitHubConnectProps = {
- onConnectAndPrompt: (username: string, prompt: string) => void;
-};
-
-type DeveloperTone = "peach" | "mint" | "violet" | "rose" | "pink" | "red";
-
-type PickerOption = {
- value: string;
- label: string;
- kind: "developer" | "focus";
- tone: DeveloperTone | GitHubStarterTone;
- icon?: GitHubStarterIconKey;
- avatarUsername?: string;
-};
-
-const STARTER_ICON_MAP: Record = {
- "commit-activity": Activity,
- "pull-requests": GitPullRequest,
- "issue-tracking": CircleDot,
- "code-reviews": Search,
- "language-breakdown": Code2,
- "repository-stats": Hexagon,
-};
-
-const DEMO_USERS = [
- { username: "garrytan", tone: "peach" },
- { username: "bradfitz", tone: "mint" },
- { username: "yyx990803", tone: "violet" },
- { username: "ctate", tone: "red" },
- { username: "torvalds", tone: "pink" },
-] as const satisfies ReadonlyArray<{ username: string; tone: DeveloperTone }>;
-
-const DEVELOPER_OPTIONS: PickerOption[] = DEMO_USERS.map((user) => ({
- value: user.username,
- label: `@${user.username}`,
- kind: "developer",
- tone: user.tone,
- avatarUsername: user.username,
-}));
-
-const FOCUS_AREA_OPTIONS: PickerOption[] = GITHUB_STARTERS.map((starter) => ({
- value: starter.prompt,
- label: starter.label,
- kind: "focus",
- tone: starter.tone,
- icon: starter.icon,
-}));
-
-function GitHubStarterIcon({ icon }: { icon: GitHubStarterIconKey }) {
- const Icon = STARTER_ICON_MAP[icon];
- return ;
-}
-
-function renderOptionLeading(option: PickerOption) {
- if (option.kind === "developer") {
- return option.avatarUsername ? (
-
- ) : (
-
- );
- }
-
- if (option.icon) {
- return (
-
-
-
- );
- }
-
- return null;
-}
-
-type OptionListProps = {
- ariaLabel: string;
- options: PickerOption[];
- value: string | null;
- onChange: (value: string) => void;
- className?: string;
-};
-
-function OptionList({ ariaLabel, options, value, onChange, className = "" }: OptionListProps) {
- return (
-
- {options.map((option) => {
- const isSelected = option.value === value;
- const leading = renderOptionLeading(option);
-
- return (
-
- );
- })}
-
- );
-}
-
-export function GitHubConnect({ onConnectAndPrompt }: GitHubConnectProps) {
- const [username, setUsername] = useState("");
- const [selectedDeveloperUsername, setSelectedDeveloperUsername] = useState(null);
- const [avatarUrl, setAvatarUrl] = useState(null);
- const [error, setError] = useState("");
- const [selectedGithubPrompt, setSelectedGithubPrompt] = useState(null);
- const [validating, setValidating] = useState(false);
- const inputRef = useRef(null);
- const debounceRef = useRef>(undefined);
-
- useEffect(() => {
- inputRef.current?.focus();
- }, []);
-
- useEffect(() => {
- if (debounceRef.current) clearTimeout(debounceRef.current);
- if (selectedDeveloperUsername) return;
- if (!username.trim() || username.trim().length < 2) return;
-
- debounceRef.current = setTimeout(() => {
- const url = `https://github.com/${username.trim()}.png?size=64`;
- const img = new window.Image();
- img.onload = () => setAvatarUrl(url);
- img.onerror = () => setAvatarUrl(null);
- img.src = url;
- }, 250);
-
- return () => {
- if (debounceRef.current) clearTimeout(debounceRef.current);
- };
- }, [selectedDeveloperUsername, username]);
-
- const focusInput = useCallback(() => {
- window.requestAnimationFrame(() => inputRef.current?.focus());
- }, []);
-
- const handleReset = useCallback(() => {
- setUsername("");
- setSelectedDeveloperUsername(null);
- setAvatarUrl(null);
- setError("");
- setSelectedGithubPrompt(null);
- focusInput();
- }, [focusInput]);
-
- const validate = useCallback((name: string): boolean => {
- const trimmed = name.trim();
- if (!trimmed) {
- setError("Enter a GitHub username");
- return false;
- }
- if (!/^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(trimmed)) {
- setError("Invalid username format");
- return false;
- }
- if (trimmed.length > 39) {
- setError("Username too long");
- return false;
- }
- setError("");
- return true;
- }, []);
-
- const handlePopularDeveloperSelect = useCallback((nextUsername: string) => {
- setSelectedDeveloperUsername(nextUsername);
- setUsername("");
- setAvatarUrl(null);
- setError("");
- }, []);
-
- const handleClearIdentity = useCallback(() => handleReset(), [handleReset]);
- const handleClearSelectedFocus = useCallback(() => {
- setSelectedGithubPrompt(null);
- setError("");
- }, []);
- const handleClearTypedUsername = useCallback(() => {
- setUsername("");
- setAvatarUrl(null);
- setError("");
- focusInput();
- }, [focusInput]);
-
- const trimmedUsername = username.trim();
- const effectiveUsername = trimmedUsername || selectedDeveloperUsername || "";
- const selectedDeveloperOption =
- DEVELOPER_OPTIONS.find((o) => o.value === selectedDeveloperUsername) ?? null;
- const selectedFocusOption =
- FOCUS_AREA_OPTIONS.find((o) => o.value === selectedGithubPrompt) ?? null;
- const selectedDeveloperLeading = selectedDeveloperOption
- ? renderOptionLeading(selectedDeveloperOption)
- : null;
- const selectedFocusLeading = selectedFocusOption
- ? renderOptionLeading(selectedFocusOption)
- : null;
- const hasValidUsername =
- effectiveUsername.length > 0 &&
- effectiveUsername.length <= 39 &&
- /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(effectiveUsername);
- const hasIdentitySelection = Boolean(effectiveUsername);
- const isTypedIdentityLocked = Boolean(
- trimmedUsername && selectedGithubPrompt && hasValidUsername,
- );
- const showDeveloperPicker = !selectedDeveloperOption && !trimmedUsername;
- const canStart = Boolean(selectedGithubPrompt) && hasValidUsername;
- const canReset = Boolean(
- trimmedUsername || selectedDeveloperUsername || error || selectedGithubPrompt,
- );
-
- const handleStartGenerating = async () => {
- if (!selectedGithubPrompt) return;
- if (!validate(effectiveUsername)) return;
- setValidating(true);
- try {
- const res = await fetch(`https://api.github.com/users/${effectiveUsername}`);
- if (!res.ok) {
- setError("GitHub user not found");
- setValidating(false);
- return;
- }
- } catch {
- setError("Could not verify username");
- setValidating(false);
- return;
- }
- setValidating(false);
- onConnectAndPrompt(effectiveUsername, selectedGithubPrompt);
- };
-
- const handleSubmit = (event: FormEvent) => {
- event.preventDefault();
- void handleStartGenerating();
- };
-
- const renderIdentityControl = () => {
- if (selectedDeveloperOption) {
- return (
-
- {selectedDeveloperLeading && (
- {selectedDeveloperLeading}
- )}
- {selectedDeveloperOption.label}
-
-
- );
- }
- if (isTypedIdentityLocked) {
- return (
-
-
- {avatarUrl ? (
-
- ) : (
- @
- )}
-
- @{effectiveUsername}
-
-
- );
- }
- return (
-
- {avatarUrl &&
}
- @
-
- {username || "username"}
- {
- setSelectedDeveloperUsername(null);
- setUsername(e.target.value);
- setAvatarUrl(null);
- setError("");
- }}
- placeholder="username"
- autoComplete="off"
- spellCheck={false}
- />
-
- {trimmedUsername && (
-
- )}
-
- );
- };
-
- return (
-
- );
-}
diff --git a/docs/app/demo/github/components/Header/Header.css b/docs/app/demo/github/components/Header/Header.css
deleted file mode 100644
index 0e0497a13..000000000
--- a/docs/app/demo/github/components/Header/Header.css
+++ /dev/null
@@ -1,54 +0,0 @@
-.openui-icon-button.header-btn,
-.header-btn {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 10px;
- background: transparent;
- border: none;
- border-radius: 6px;
- color: var(--openui-text-neutral-secondary);
- font-family: var(--openui-font-body);
- font-size: 13px;
- cursor: pointer;
- text-decoration: none;
- transition:
- background 0.15s,
- color 0.15s;
- white-space: nowrap;
-}
-
-.openui-icon-button.header-btn:hover:not(:disabled),
-.header-btn:hover {
- background: var(--openui-highlight);
- color: var(--openui-text-neutral-primary);
-}
-
-.header-btn svg {
- flex-shrink: 0;
-}
-
-.openui-icon-button.header-icon-btn,
-.header-icon-btn {
- padding: 6px;
- border-radius: 6px;
-}
-
-@media (max-width: 1023px) {
- .header-btn {
- padding: 6px 8px;
- font-size: 12px;
- }
-}
-
-@media (max-width: 767px) {
- .header-btn {
- gap: 0;
- min-width: 32px;
- padding: 6px;
- }
-
- .header-btn span {
- display: none;
- }
-}
diff --git a/docs/app/demo/github/components/Header/Header.tsx b/docs/app/demo/github/components/Header/Header.tsx
deleted file mode 100644
index e189b20da..000000000
--- a/docs/app/demo/github/components/Header/Header.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { SiteMarketingHeader } from "@/components/site-marketing-header";
-import type { Theme } from "../../constants";
-import "./Header.css";
-
-type HeaderProps = {
- theme: Theme;
- onThemeToggle: () => void;
- borderMode?: "always" | "scroll";
-};
-
-export function Header({ theme, onThemeToggle, borderMode = "always" }: HeaderProps) {
- const themeLabel = { system: "System", light: "Light", dark: "Dark" }[theme];
-
- return (
-
- );
-}
diff --git a/docs/app/demo/github/components/Modal/Modal.css b/docs/app/demo/github/components/Modal/Modal.css
deleted file mode 100644
index 89026479b..000000000
--- a/docs/app/demo/github/components/Modal/Modal.css
+++ /dev/null
@@ -1,84 +0,0 @@
-.modal-overlay {
- position: fixed;
- inset: 0;
- z-index: 200;
- background: var(--openui-overlay);
- backdrop-filter: blur(4px);
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 24px;
- animation: modal-overlay-in 0.15s ease;
-}
-
-@keyframes modal-overlay-in {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
-}
-
-.modal-container {
- background: var(--openui-foreground);
- border: 1px solid var(--openui-border-interactive);
- border-radius: 16px;
- box-shadow: var(--openui-shadow-2xl);
- width: min(1400px, 95vw);
- height: 95vh;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- animation: modal-container-in 0.2s ease;
-}
-
-@keyframes modal-container-in {
- from {
- opacity: 0;
- transform: translateY(12px) scale(0.98);
- }
- to {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
-}
-
-.modal-container-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 16px;
- background: var(--openui-highlight-subtle);
- border-bottom: 1px solid var(--openui-border-default);
- flex-shrink: 0;
-}
-
-.modal-container-body {
- flex: 1;
- overflow: auto;
-}
-
-/* ─── Mobile ─────────────────────────────────────────────────────────────────── */
-@media (max-width: 768px) {
- .modal-overlay {
- padding: 0;
- align-items: flex-end;
- }
-
- .modal-container {
- width: 100vw;
- height: 95dvh;
- border-radius: 16px 16px 0 0;
- animation: modal-container-mobile-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);
- }
-}
-
-@keyframes modal-container-mobile-in {
- from {
- transform: translateY(100%);
- }
- to {
- transform: translateY(0);
- }
-}
diff --git a/docs/app/demo/github/components/Modal/Modal.tsx b/docs/app/demo/github/components/Modal/Modal.tsx
deleted file mode 100644
index 47b5d784d..000000000
--- a/docs/app/demo/github/components/Modal/Modal.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { IconButton } from "@openuidev/react-ui";
-import { X } from "lucide-react";
-import type { ReactNode } from "react";
-import { useCallback, useEffect } from "react";
-import "./Modal.css";
-
-type ModalProps = {
- title: string;
- titleAdornment?: ReactNode;
- onClose?: () => void;
- children: ReactNode;
- className?: string;
-};
-
-export function Modal({ title, titleAdornment, onClose, children, className }: ModalProps) {
- const handleKey = useCallback(
- (e: KeyboardEvent) => {
- if (e.key === "Escape") onClose?.();
- },
- [onClose],
- );
-
- useEffect(() => {
- if (!onClose) return;
- document.addEventListener("keydown", handleKey);
- return () => document.removeEventListener("keydown", handleKey);
- }, [onClose, handleKey]);
-
- return (
-
-
e.stopPropagation()}
- >
-
-
- {title}
- {titleAdornment}
-
- {onClose && (
-
}
- variant="tertiary"
- size="extra-small"
- onClick={onClose}
- title="Close"
- aria-label="Close modal"
- />
- )}
-
-
{children}
-
-
- );
-}
diff --git a/docs/app/demo/github/components/PreviewPanel/PreviewPanel.css b/docs/app/demo/github/components/PreviewPanel/PreviewPanel.css
deleted file mode 100644
index 7445d8926..000000000
--- a/docs/app/demo/github/components/PreviewPanel/PreviewPanel.css
+++ /dev/null
@@ -1,32 +0,0 @@
-@keyframes preview-spin {
- to {
- transform: rotate(360deg);
- }
-}
-
-.panel-title-group {
- display: flex;
- align-items: center;
- gap: var(--openui-space-s);
-}
-
-.preview-spinner {
- animation: preview-spin 1s linear infinite;
- color: var(--openui-text-purple-primary);
- flex-shrink: 0;
-}
-
-.preview-body {
- flex: 1;
- overflow: auto;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--openui-highlight-subtle);
-}
-
-.preview-content {
- width: 100%;
- padding: var(--openui-space-m-l);
- align-self: flex-start;
-}
diff --git a/docs/app/demo/github/components/PreviewPanel/PreviewPanel.tsx b/docs/app/demo/github/components/PreviewPanel/PreviewPanel.tsx
deleted file mode 100644
index 78bfbf79c..000000000
--- a/docs/app/demo/github/components/PreviewPanel/PreviewPanel.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import type { ActionEvent, ParseResult } from "@openuidev/react-lang";
-import { Renderer } from "@openuidev/react-lang";
-import { IconButton, openuiLibrary, ThemeProvider } from "@openuidev/react-ui";
-import { Loader2, Maximize2, Monitor } from "lucide-react";
-import { useCallback, useState } from "react";
-import { Modal } from "../Modal/Modal";
-import "./PreviewPanel.css";
-
-type PreviewPanelProps = {
- code: string;
- isStreaming: boolean;
- onParseResult?: (result: ParseResult | null) => void;
- mode: "light" | "dark";
- toolProvider?: Record) => Promise> | null;
- onAction?: (event: ActionEvent) => void;
-};
-
-export function PreviewPanel({
- code,
- isStreaming,
- onParseResult,
- mode,
- toolProvider,
- onAction,
-}: PreviewPanelProps) {
- const [isModalOpen, setIsModalOpen] = useState(false);
-
- const closeModal = useCallback(() => setIsModalOpen(false), []);
-
- const previewContent = code ? (
-
-
-
-
-
- ) : (
-
-
-
-
-
Rendered UI will appear here
-
- );
-
- return (
- <>
-
-
-
- Preview
- {isStreaming && }
-
-
- }
- variant="tertiary"
- size="extra-small"
- onClick={() => setIsModalOpen(true)}
- title="Open fullscreen preview"
- aria-label="Open fullscreen preview"
- />
-
-
-
{previewContent}
-
-
- {isModalOpen && (
- : null}
- onClose={closeModal}
- >
- {previewContent}
-
- )}
- >
- );
-}
diff --git a/docs/app/demo/github/constants.ts b/docs/app/demo/github/constants.ts
deleted file mode 100644
index f9f6faab9..000000000
--- a/docs/app/demo/github/constants.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-export type Theme = "system" | "light" | "dark";
-export type Status = "idle" | "streaming" | "done" | "error";
-
-export const GITHUB_DEMO_MODEL = "anthropic/claude-sonnet-4-6";
-export const GITHUB_DEMO_MODEL_LABEL = "Claude Sonnet 4.6";
-
-export type GitHubStarterIconKey =
- | "commit-activity"
- | "pull-requests"
- | "issue-tracking"
- | "code-reviews"
- | "language-breakdown"
- | "repository-stats";
-
-export type GitHubStarterTone = "green" | "purple" | "red" | "blue" | "amber" | "pink";
-
-export type GitHubStarter = {
- label: string;
- prompt: string;
- icon: GitHubStarterIconKey;
- tone: GitHubStarterTone;
-};
-
-export const GITHUB_STARTERS: GitHubStarter[] = [
- {
- label: "Commit Activity",
- prompt:
- "Build a dashboard focused on commit activity, contribution streaks, busiest repositories, and recent work patterns over time.",
- icon: "commit-activity",
- tone: "green",
- },
- {
- label: "Pull Requests",
- prompt:
- "Create a pull request dashboard with open versus merged trends, review turnaround, and the repositories with the most PR activity.",
- icon: "pull-requests",
- tone: "purple",
- },
- {
- label: "Issue Tracking",
- prompt:
- "Show issue tracking insights with open versus closed trends, response speed, issue backlog, and the repos with the most active discussions.",
- icon: "issue-tracking",
- tone: "red",
- },
- {
- label: "Code Reviews",
- prompt:
- "Analyze code reviews by showing review volume, average turnaround, participation by repository, and recent review activity.",
- icon: "code-reviews",
- tone: "blue",
- },
- {
- label: "Language Breakdown",
- prompt:
- "Visualize language breakdown across repositories with usage share, top projects per language, and how the stack changes over time.",
- icon: "language-breakdown",
- tone: "amber",
- },
- {
- label: "Repository Stats",
- prompt:
- "Build a repository stats dashboard with stars, forks, watchers, top repositories, and overall portfolio health.",
- icon: "repository-stats",
- tone: "pink",
- },
-];
-
-// ── Chat message types (shared between page + ConversationPanel) ──────────
-
-export type ToolCallEntry = {
- tool: string;
- status: "pending" | "done" | "error";
-};
-
-export interface ChatMessage {
- role: "user" | "assistant";
- content: string;
- text?: string;
- hasCode: boolean;
-}
diff --git a/docs/app/demo/github/github/prompt-config.ts b/docs/app/demo/github/github/prompt-config.ts
deleted file mode 100644
index 6e0811e3d..000000000
--- a/docs/app/demo/github/github/prompt-config.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-// Server-safe — no React, no browser APIs.
-// Used by the stream route to build the GitHub-specific system prompt.
-
-export const GITHUB_PREAMBLE = `You are an AI assistant that builds GitHub profile dashboards using openui-lang, a declarative UI language.
-
-The user has connected their GitHub profile. All data comes from real GitHub API tools via Query().
-The username is already set — do NOT include username in Query args.
-
-Build rich, visually impressive dashboards with:
-- Multiple KPI cards showing live computed stats
-- Charts (PieChart for languages, BarChart for comparisons, LineChart for trends)
-- Tables with sorting and filtering via $variables
-- Interactive controls (Select dropdowns, search Input) wired to $variables
-
-After the openui-lang code block, write a brief friendly message describing what you built and suggesting what the user could iterate on next. Keep it conversational, like you're talking to the user. For example: "Here's your dashboard with KPIs and language breakdown. You could try adding a search filter to the repos table, or ask me to add an activity timeline chart."`;
-
-export const GITHUB_TOOL_EXAMPLES: string[] = [
- `Example — GitHub Profile Dashboard (PREFERRED pattern):
-root = Stack([header, globalControls, kpiRow, chartsRow, repoSection])
-header = CardHeader("GitHub Dashboard", "Your developer profile at a glance")
-$sortBy = "stars"
-$language = "all"
-profile = Query("get_profile", {}, {login: "", name: "", bio: "", avatar_url: "", public_repos: 0, followers: 0, following: 0, created_at: ""})
-repos = Query("get_repos", {sort: $sortBy, language: $language}, {rows: []})
-languages = Query("get_languages", {}, {rows: []})
-events = Query("get_events", {}, {rows: [], summary: {total: 0, push: 0, pr: 0, issues: 0, reviews: 0}})
-globalControls = Stack([langFilter, refreshBtn], "row", "m", "end")
-langFilter = FormControl("Language", Select("language", [SelectItem("all", "All"), SelectItem("TypeScript", "TypeScript"), SelectItem("Python", "Python"), SelectItem("JavaScript", "JavaScript"), SelectItem("C", "C"), SelectItem("Go", "Go"), SelectItem("Rust", "Rust")], null, null, $language))
-refreshBtn = Button("Refresh", Action([@Run(repos), @Run(languages), @Run(events)]), "secondary")
-kpiRow = Stack([reposKpi, starsKpi, followersKpi, activityKpi], "row", "m", "stretch", "start", true)
-reposKpi = Card([TextContent("Repositories", "small"), TextContent("" + profile.public_repos, "large-heavy")])
-starsKpi = Card([TextContent("Total Stars", "small"), TextContent("" + @Sum(repos.rows.stars), "large-heavy")])
-followersKpi = Card([TextContent("Followers", "small"), TextContent("" + profile.followers, "large-heavy")])
-activityKpi = Card([TextContent("Recent Events", "small"), TextContent("" + events.summary.total, "large-heavy")])
-chartsRow = Stack([langCard, activityCard], "row", "m")
-langCard = Card([CardHeader("Languages", "By bytes of code"), PieChart(languages.rows.language, languages.rows.bytes, "donut")])
-activityCard = Card([CardHeader("Activity Mix"), BarChart(["Push", "PR", "Issues", "Reviews"], [Series("Events", [events.summary.push, events.summary.pr, events.summary.issues, events.summary.reviews])])])
-repoSection = Card([CardHeader("Repositories"), sortControl, repoTable])
-sortControl = Stack([sortFilter], "row", "m", "end")
-sortFilter = FormControl("Sort", Select("sortBy", [SelectItem("stars", "Stars"), SelectItem("forks", "Forks"), SelectItem("updated", "Recent")], null, null, $sortBy))
-sorted = @Sort(repos.rows, $sortBy, "desc")
-repoTable = Table([Col("Name", sorted.name), Col("Language", @Each(sorted, "r", Tag(r.language, null, "sm", r.language == "TypeScript" ? "info" : r.language == "Python" ? "success" : "neutral"))), Col("Stars", sorted.stars, "number"), Col("Forks", sorted.forks, "number")])`,
-
- `Example — Repo Organizer with Bookmarks (CRUD pattern):
-root = Stack([header, kpiRow, tabs, bookmarkModal])
-header = CardHeader("Repo Organizer", "Bookmark and tag your repositories")
-$showBookmark = false
-$bookmarkRepo = ""
-$bookmarkNote = ""
-$bookmarkTag = "important"
-$search = ""
-repos = Query("get_repos", {sort: "stars"}, {rows: []})
-bookmarks = Query("get_bookmarks", {}, {rows: []})
-languages = Query("get_languages", {}, {rows: []})
-saveResult = Mutation("save_bookmark", {repo: $bookmarkRepo, note: $bookmarkNote, tag: $bookmarkTag})
-deleteResult = Mutation("delete_bookmark", {repo: $bookmarkRepo})
-kpiRow = Stack([Card([TextContent("Repos", "small"), TextContent("" + @Count(repos.rows), "large-heavy")]), Card([TextContent("Bookmarked", "small"), TextContent("" + @Count(bookmarks.rows), "large-heavy")]), Card([TextContent("Stars", "small"), TextContent("" + @Sum(repos.rows.stars), "large-heavy")])], "row", "m", "stretch", "start", true)
-tabs = Tabs([reposTab, bookmarksTab, insightsTab])
-reposTab = TabItem("repos", "Repos", [searchField, repoTable])
-searchField = FormControl("Search", Input("search", "Filter repos...", "text", null, $search))
-filtered = @Filter(repos.rows, "name", "contains", $search)
-repoTable = Table([Col("Repo", filtered.name), Col("Stars", filtered.stars, "number"), Col("Language", @Each(filtered, "r", Tag(r.language, null, "sm"))), Col("Actions", @Each(filtered, "r", Button("Bookmark", Action([@Set($bookmarkRepo, r.name), @Set($showBookmark, true)]), "secondary", "normal", "small")))])
-bookmarksTab = TabItem("bookmarks", "Bookmarks", [bookmarkTable])
-bookmarkTable = @Count(bookmarks.rows) > 0 ? Table([Col("Repo", bookmarks.rows.repo), Col("Tag", @Each(bookmarks.rows, "b", Tag(b.tag, null, "sm", b.tag == "important" ? "danger" : b.tag == "favorite" ? "success" : "info"))), Col("Note", bookmarks.rows.note), Col("Remove", @Each(bookmarks.rows, "b", Button("Delete", Action([@Set($bookmarkRepo, b.repo), @Run(deleteResult), @Run(bookmarks)]), "secondary", "destructive", "small")))]) : TextContent("No bookmarks yet. Go to Repos tab and bookmark some!")
-insightsTab = TabItem("insights", "Insights", [Card([CardHeader("Language Breakdown"), PieChart(languages.rows.language, languages.rows.bytes, "donut")])])
-bmBtns = Buttons([Button("Save", Action([@Run(saveResult), @Run(bookmarks), @Set($showBookmark, false), @Reset($bookmarkNote, $bookmarkTag)]), "primary"), Button("Cancel", Action([@Set($showBookmark, false)]), "secondary")])
-bmForm = Form("bookmark", bmBtns, [FormControl("Repo", Input("bm_repo", "", "text", null, $bookmarkRepo)), FormControl("Tag", Select("bm_tag", [SelectItem("important", "Important"), SelectItem("review", "To Review"), SelectItem("learning", "Learning"), SelectItem("favorite", "Favorite")], null, null, $bookmarkTag)), FormControl("Note", TextArea("bm_note", "Why bookmark this?", 3, null, $bookmarkNote))])
-bookmarkModal = Modal("Bookmark Repository", $showBookmark, [bmForm])`,
-];
-
-export const GITHUB_ADDITIONAL_RULES: string[] = [
- "The user's GitHub username is already connected — do NOT ask for it or include username in Query() args",
- "get_repos returns: rows[].{name, full_name, description, language, stars, forks, size, open_issues, updated_at, html_url}",
- "get_profile returns: {login, name, bio, avatar_url, public_repos, followers, following, created_at}",
- "get_languages returns: rows[].{language, bytes, repos_count} sorted by bytes descending",
- "get_events returns: {rows[].{type, repo, date}, summary.{total, push, pr, issues, reviews}}. Event types: Push, PullRequest, Issues, Create, Watch, Fork",
- 'get_commit_activity({repo}) returns: rows[].{week, total} — 52 weeks. Needs repo name, e.g. {repo: "linux"}',
- "get_star_history({repo}) returns: rows[].{starred_at, cumulative} for LineChart. Needs repo name.",
- "get_contributors({repo}) returns: rows[].{login, avatar_url, total_commits}. Needs repo name.",
- "Bookmark tools: save_bookmark({repo, note?, tag?}), get_bookmarks({}), delete_bookmark({repo}). Use Mutation + @Run pattern.",
- "For dashboards, ALWAYS include: 3+ KPI cards with @Sum/@Count, 2+ chart types, 1 data table, 1+ interactive filter ($variable + Select)",
- "Place GLOBAL filters (language, date range, refresh) at the dashboard level — between header and KPI cards — so they visibly affect all widgets below. Place LOCAL filters (sort, search) inside their specific widget Card, directly above the component they control.",
- "Use @Sum(repos.rows.stars), @Count(repos.rows), @Avg, @Round on Query results for KPIs — NEVER hardcode numbers",
- "Use PieChart for language breakdowns (donut variant), BarChart for comparisons, LineChart for trends over time",
- "Rate limit: 60 req/hr per visitor. Prefer get_repos (1 API call) over per-repo endpoints (get_commit_activity, get_star_history, get_contributors)",
- "Per-repo tools (get_commit_activity, get_star_history, get_contributors) are expensive — only use when the user specifically asks about a specific repo",
- "NEVER use Image() or ImageBlock() components — no external image URLs are available. Use TextContent, Tag, or emoji for visual accents instead.",
-];
diff --git a/docs/app/demo/github/github/tools.ts b/docs/app/demo/github/github/tools.ts
deleted file mode 100644
index e22485261..000000000
--- a/docs/app/demo/github/github/tools.ts
+++ /dev/null
@@ -1,465 +0,0 @@
-import { Octokit } from "octokit";
-import { deleteBookmark, getBookmarks, saveBookmark } from "../bookmarks/store";
-import type {
- CommitWeek,
- ContributorRow,
- EventRow,
- LanguageRow,
- RepoRow,
- StarPoint,
-} from "./types";
-
-// ── Cache ──────────────────────────────────────────────────────────────────
-
-const cache = new Map();
-const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
-
-function cached(key: string, fn: () => Promise): Promise {
- const now = Date.now();
- const entry = cache.get(key);
- if (entry && now - entry.ts < CACHE_TTL) return Promise.resolve(entry.data as T);
-
- // Evict expired entries occasionally
- if (cache.size > 50) {
- for (const [k, v] of cache) {
- if (now - v.ts >= CACHE_TTL) cache.delete(k);
- }
- }
-
- return fn().then((data) => {
- cache.set(key, { data, ts: now });
- return data;
- });
-}
-
-/** Clear all cached data (call when switching users) */
-export function clearCache() {
- cache.clear();
-}
-
-/**
- * Prefetch core GitHub data and return a compact summary for LLM context.
- * Also warms the cache so Query() nodes resolve instantly on first render.
- */
-export async function prefetchAndSummarize(
- tools: Record) => Promise>,
-): Promise {
- const [profile, repos, languages, events] = await Promise.all([
- tools.get_profile({}),
- tools.get_repos({ sort: "stars" }),
- tools.get_languages({}),
- tools.get_events({}),
- ]);
-
- const p = profile as Record;
- const repoRows = ((repos as Record).rows ?? []) as Array<
- Record
- >;
- const langRows = ((languages as Record).rows ?? []) as Array<
- Record
- >;
- const ev = ((events as Record).summary ?? {}) as Record;
-
- const topRepos = repoRows
- .slice(0, 8)
- .map((r) => `${r.name} (${r.stars}★, ${r.language || "unknown"})`)
- .join(", ");
- const topLangs = langRows
- .slice(0, 6)
- .map((l) => l.language)
- .join(", ");
-
- return `
-Profile: ${p.name || p.login}, ${p.public_repos} public repos, ${p.followers} followers, joined ${p.created_at}
-Top repos by stars: ${topRepos || "none found"}
-Languages: ${topLangs || "none detected"}
-Recent activity (last 90 days): ${ev.total || 0} events (${ev.push || 0} pushes, ${ev.pr || 0} PRs, ${ev.issues || 0} issues, ${ev.reviews || 0} reviews)
-`;
-}
-
-// ── Rate limit tracking ────────────────────────────────────────────────────
-
-let rateLimitRemaining = 60;
-let rateLimitReset = 0;
-
-export function getRateLimit() {
- return { remaining: rateLimitRemaining, reset: rateLimitReset };
-}
-
-function checkRateLimit(): string | null {
- if (rateLimitRemaining <= 0 && Date.now() / 1000 < rateLimitReset) {
- const mins = Math.ceil((rateLimitReset - Date.now() / 1000) / 60);
- return `GitHub API rate limit exceeded. Try again in ${mins} minute${mins > 1 ? "s" : ""}.`;
- }
- return null;
-}
-
-function updateRateLimit(response: any) {
- const h = response?.headers;
- if (!h) return;
- const remaining = h["x-ratelimit-remaining"];
- const reset = h["x-ratelimit-reset"];
- if (remaining != null) {
- const n = parseInt(String(remaining), 10);
- if (!isNaN(n)) rateLimitRemaining = n;
- }
- if (reset != null) {
- const n = parseInt(String(reset), 10);
- if (!isNaN(n)) rateLimitReset = n;
- }
-}
-
-// ── Retry for 202 (computing) ──────────────────────────────────────────────
-
-async function fetchWithRetry(
- octokit: Octokit,
- route: string,
- params: Record,
- retries = 3,
-): Promise {
- for (let i = 0; i < retries; i++) {
- const res = await octokit.request(route, params);
- updateRateLimit(res);
- if (res.status !== 202) return res.data;
- // GitHub is computing stats — wait and retry
- await new Promise((r) => setTimeout(r, 1500));
- }
- return { error: "GitHub is still computing stats. Try again in a moment." };
-}
-
-// ── Tool implementations ───────────────────────────────────────────────────
-
-export function createGitHubToolProvider(
- username: string,
-): Record) => Promise> {
- const octokit = new Octokit();
-
- // Shared repo data — fetched once, used by get_repos + get_languages
- let repoCache: RepoRow[] | null = null;
-
- async function fetchRepos(): Promise {
- if (repoCache) return repoCache;
-
- const limitErr = checkRateLimit();
- if (limitErr) throw new Error(limitErr);
-
- const res = await octokit.rest.repos.listForUser({
- username,
- per_page: 100,
- sort: "pushed",
- });
- updateRateLimit(res);
-
- repoCache = res.data.map((r) => ({
- name: r.name,
- full_name: r.full_name,
- description: r.description ?? "",
- language: r.language ?? "",
- stars: r.stargazers_count ?? 0,
- forks: r.forks_count ?? 0,
- size: r.size ?? 0,
- open_issues: r.open_issues_count ?? 0,
- updated_at: r.updated_at?.slice(0, 10) ?? "",
- html_url: r.html_url ?? "",
- }));
- return repoCache;
- }
-
- return {
- // ── Profile ──────────────────────────────────────────────────────────
-
- get_profile: () =>
- cached(`profile:${username}`, async () => {
- const empty = {
- login: "",
- name: "",
- bio: "",
- avatar_url: "",
- public_repos: 0,
- followers: 0,
- following: 0,
- created_at: "",
- };
- const limitErr = checkRateLimit();
- if (limitErr) return { ...empty, error: limitErr };
-
- try {
- const res = await octokit.rest.users.getByUsername({ username });
- updateRateLimit(res);
- const u = res.data;
- return {
- login: u.login,
- name: u.name ?? u.login,
- bio: u.bio ?? "",
- avatar_url: u.avatar_url,
- public_repos: u.public_repos,
- followers: u.followers,
- following: u.following,
- created_at: u.created_at?.slice(0, 10) ?? "",
- };
- } catch (err: any) {
- if (err.status === 404) return { ...empty, error: `User "${username}" not found` };
- return { ...empty, error: err.message ?? "Failed to fetch profile" };
- }
- }),
-
- // ── Repos ────────────────────────────────────────────────────────────
-
- get_repos: (args) =>
- cached(`repos:${username}:${JSON.stringify(args)}`, async () => {
- try {
- let rows = await fetchRepos();
-
- // Client-side language filter
- const lang = args.language as string | undefined;
- if (lang && lang !== "all" && lang !== "") {
- rows = rows.filter((r) => r.language.toLowerCase() === lang.toLowerCase());
- }
-
- // Client-side sort
- const sort = (args.sort as string) ?? "stars";
- const sortKey =
- sort === "forks"
- ? "forks"
- : sort === "updated"
- ? "updated_at"
- : sort === "created"
- ? "updated_at"
- : "stars";
- rows = [...rows].sort((a, b) => {
- const av = (a as any)[sortKey];
- const bv = (b as any)[sortKey];
- if (typeof av === "string") return bv.localeCompare(av);
- return bv - av;
- });
-
- return { rows };
- } catch (err: any) {
- return { error: err.message, rows: [] };
- }
- }),
-
- // ── Languages (aggregated) ───────────────────────────────────────────
-
- get_languages: () =>
- cached(`languages:${username}`, async () => {
- try {
- const repos = await fetchRepos();
-
- // Quick aggregation from repo language field
- const langMap = new Map();
- for (const r of repos) {
- if (!r.language) continue;
- const entry = langMap.get(r.language) ?? { bytes: 0, count: 0 };
- entry.bytes += r.size * 1024; // size is in KB, convert to bytes approx
- entry.count += 1;
- langMap.set(r.language, entry);
- }
-
- // Fetch detailed language bytes for top 5 repos (by stars)
- const topRepos = [...repos].sort((a, b) => b.stars - a.stars).slice(0, 5);
-
- const limitErr = checkRateLimit();
- if (!limitErr) {
- for (const repo of topRepos) {
- try {
- const res = await octokit.rest.repos.listLanguages({
- owner: username,
- repo: repo.name,
- });
- updateRateLimit(res);
- for (const [lang, bytes] of Object.entries(res.data)) {
- const entry = langMap.get(lang) ?? { bytes: 0, count: 0 };
- entry.bytes = Math.max(entry.bytes, bytes as number);
- if (!langMap.has(lang)) entry.count = 1;
- langMap.set(lang, entry);
- }
- } catch {
- // Skip individual repo failures
- }
- }
- }
-
- const rows: LanguageRow[] = [...langMap.entries()]
- .map(([language, { bytes, count }]) => ({
- language,
- bytes,
- repos_count: count,
- }))
- .sort((a, b) => b.bytes - a.bytes);
-
- return { rows };
- } catch (err: any) {
- return { error: err.message, rows: [] };
- }
- }),
-
- // ── Events ───────────────────────────────────────────────────────────
-
- get_events: () =>
- cached(`events:${username}`, async () => {
- const limitErr = checkRateLimit();
- if (limitErr)
- return {
- error: limitErr,
- rows: [],
- summary: { total: 0, push: 0, pr: 0, issues: 0, reviews: 0 },
- };
-
- try {
- const res = await octokit.rest.activity.listPublicEventsForUser({
- username,
- per_page: 100,
- });
- updateRateLimit(res);
-
- const rows: EventRow[] = res.data.map((e) => ({
- type: e.type?.replace("Event", "") ?? "Unknown",
- repo: (e.repo as any)?.name?.split("/")[1] ?? e.repo?.name ?? "",
- date: e.created_at?.slice(0, 10) ?? "",
- count: 1,
- }));
-
- // Build summary
- const summary = { total: rows.length, push: 0, pr: 0, issues: 0, reviews: 0 };
- for (const r of rows) {
- if (r.type === "Push") summary.push++;
- else if (r.type === "PullRequest") summary.pr++;
- else if (r.type === "Issues") summary.issues++;
- else if (r.type === "PullRequestReview") summary.reviews++;
- }
-
- return { rows, summary };
- } catch (err: any) {
- return {
- error: err.message,
- rows: [],
- summary: { total: 0, push: 0, pr: 0, issues: 0, reviews: 0 },
- };
- }
- }),
-
- // ── Commit Activity (per repo) ───────────────────────────────────────
-
- get_commit_activity: (args) =>
- cached(`commits:${username}:${args.repo}`, async () => {
- const repo = args.repo as string;
- if (!repo) return { error: "repo argument is required", rows: [] };
-
- const limitErr = checkRateLimit();
- if (limitErr) return { error: limitErr, rows: [] };
-
- try {
- const data = await fetchWithRetry(
- octokit,
- "GET /repos/{owner}/{repo}/stats/commit_activity",
- { owner: username, repo },
- );
-
- if (!Array.isArray(data)) return data; // error object
-
- const rows: CommitWeek[] = data.map((w: { week: number; total: number }) => ({
- week: new Date(w.week * 1000).toISOString().slice(0, 10),
- total: w.total,
- }));
-
- return { rows };
- } catch (err: any) {
- return { error: err.message, rows: [] };
- }
- }),
-
- // ── Star History (per repo) ──────────────────────────────────────────
-
- get_star_history: (args) =>
- cached(`stars:${username}:${args.repo}`, async () => {
- const repo = args.repo as string;
- if (!repo) return { error: "repo argument is required", rows: [] };
-
- const limitErr = checkRateLimit();
- if (limitErr) return { error: limitErr, rows: [] };
-
- try {
- const res = await octokit.request("GET /repos/{owner}/{repo}/stargazers", {
- owner: username,
- repo,
- per_page: 100,
- headers: { accept: "application/vnd.github.star+json" },
- });
- updateRateLimit(res);
-
- const stargazers = res.data as Array<{
- starred_at: string;
- user: { login: string };
- }>;
-
- const rows: StarPoint[] = stargazers.map((s, i) => ({
- starred_at: s.starred_at.slice(0, 10),
- cumulative: i + 1,
- }));
-
- return { rows };
- } catch (err: any) {
- return { error: err.message, rows: [] };
- }
- }),
-
- // ── Contributors (per repo) ──────────────────────────────────────────
-
- get_contributors: (args) =>
- cached(`contrib:${username}:${args.repo}`, async () => {
- const repo = args.repo as string;
- if (!repo) return { error: "repo argument is required", rows: [] };
-
- const limitErr = checkRateLimit();
- if (limitErr) return { error: limitErr, rows: [] };
-
- try {
- const data = await fetchWithRetry(
- octokit,
- "GET /repos/{owner}/{repo}/stats/contributors",
- { owner: username, repo },
- );
-
- if (!Array.isArray(data)) return data;
-
- const rows: ContributorRow[] = data
- .map((c: any) => ({
- login: c.author?.login ?? "unknown",
- avatar_url: c.author?.avatar_url ?? "",
- total_commits: c.total ?? 0,
- }))
- .sort((a: ContributorRow, b: ContributorRow) => b.total_commits - a.total_commits)
- .slice(0, 20);
-
- return { rows };
- } catch (err: any) {
- return { error: err.message, rows: [] };
- }
- }),
-
- // ── Bookmarks (localStorage) ─────────────────────────────────────────
-
- save_bookmark: async (args) => {
- const repo = args.repo as string;
- if (!repo) return { error: "repo argument is required" };
- const bookmark = saveBookmark(
- username,
- repo,
- args.note as string | undefined,
- args.tag as string | undefined,
- );
- return { success: true, bookmark };
- },
-
- get_bookmarks: async () => {
- return { rows: getBookmarks(username) };
- },
-
- delete_bookmark: async (args) => {
- const repo = args.repo as string;
- if (!repo) return { error: "repo argument is required" };
- return deleteBookmark(username, repo);
- },
- };
-}
diff --git a/docs/app/demo/github/github/types.ts b/docs/app/demo/github/github/types.ts
deleted file mode 100644
index 46153e335..000000000
--- a/docs/app/demo/github/github/types.ts
+++ /dev/null
@@ -1,347 +0,0 @@
-import type { ToolSpec } from "@openuidev/lang-core";
-
-// ── Transformed GitHub data interfaces ────────────────────────────────────
-
-export interface GitHubProfile {
- login: string;
- name: string;
- bio: string;
- avatar_url: string;
- public_repos: number;
- followers: number;
- following: number;
- created_at: string;
-}
-
-export interface RepoRow {
- name: string;
- full_name: string;
- description: string;
- language: string;
- stars: number;
- forks: number;
- size: number;
- open_issues: number;
- updated_at: string;
- html_url: string;
-}
-
-export interface LanguageRow {
- language: string;
- bytes: number;
- repos_count: number;
-}
-
-export interface EventRow {
- type: string;
- repo: string;
- date: string;
- count: number;
-}
-
-export interface CommitWeek {
- week: string;
- total: number;
-}
-
-export interface StarPoint {
- starred_at: string;
- cumulative: number;
-}
-
-export interface ContributorRow {
- login: string;
- avatar_url: string;
- total_commits: number;
-}
-
-export interface Bookmark {
- repo: string;
- note: string;
- tag: string;
- created_at: string;
-}
-
-// ── ToolSpec definitions for system prompt ──────────────────────────────
-
-export const GITHUB_TOOL_SPECS: ToolSpec[] = [
- {
- name: "get_profile",
- description:
- "Get the connected GitHub user's profile — name, bio, avatar, follower/following counts, public repo count, join date",
- inputSchema: { type: "object", properties: {} },
- outputSchema: {
- type: "object",
- properties: {
- login: { type: "string" },
- name: { type: "string" },
- bio: { type: "string" },
- avatar_url: { type: "string" },
- public_repos: { type: "integer" },
- followers: { type: "integer" },
- following: { type: "integer" },
- created_at: { type: "string" },
- },
- },
- annotations: { readOnlyHint: true },
- },
- {
- name: "get_repos",
- description:
- "List the user's public repositories with stars, forks, language, size, open issues. Supports sorting and language filtering.",
- inputSchema: {
- type: "object",
- properties: {
- sort: {
- type: "string",
- enum: ["stars", "forks", "updated", "created"],
- description: "Sort field (default: stars)",
- },
- language: {
- type: "string",
- description: "Filter by programming language (e.g. 'TypeScript'). Omit or empty for all.",
- },
- },
- },
- outputSchema: {
- type: "object",
- properties: {
- rows: {
- type: "array",
- items: {
- type: "object",
- properties: {
- name: { type: "string" },
- full_name: { type: "string" },
- description: { type: "string" },
- language: { type: "string" },
- stars: { type: "integer" },
- forks: { type: "integer" },
- size: { type: "integer" },
- open_issues: { type: "integer" },
- updated_at: { type: "string" },
- html_url: { type: "string" },
- },
- },
- },
- },
- },
- annotations: { readOnlyHint: true },
- },
- {
- name: "get_languages",
- description:
- "Get aggregated language breakdown across all repos — language name, bytes of code, number of repos using it. Sorted by bytes descending.",
- inputSchema: { type: "object", properties: {} },
- outputSchema: {
- type: "object",
- properties: {
- rows: {
- type: "array",
- items: {
- type: "object",
- properties: {
- language: { type: "string" },
- bytes: { type: "integer" },
- repos_count: { type: "integer" },
- },
- },
- },
- },
- },
- annotations: { readOnlyHint: true },
- },
- {
- name: "get_events",
- description:
- "Get the user's recent public events (pushes, PRs, issues, reviews, forks, stars) from the last 30 days. Returns event rows + summary counts.",
- inputSchema: { type: "object", properties: {} },
- outputSchema: {
- type: "object",
- properties: {
- rows: {
- type: "array",
- items: {
- type: "object",
- properties: {
- type: { type: "string" },
- repo: { type: "string" },
- date: { type: "string" },
- count: { type: "integer" },
- },
- },
- },
- summary: {
- type: "object",
- properties: {
- total: { type: "integer" },
- push: { type: "integer" },
- pr: { type: "integer" },
- issues: { type: "integer" },
- reviews: { type: "integer" },
- },
- },
- },
- },
- annotations: { readOnlyHint: true },
- },
- {
- name: "get_commit_activity",
- description: "Get weekly commit counts for a specific repo over the past year (52 weeks).",
- inputSchema: {
- type: "object",
- properties: {
- repo: {
- type: "string",
- description:
- 'Repository name (just the name, not full path — e.g. "linux" not "torvalds/linux")',
- },
- },
- required: ["repo"],
- },
- outputSchema: {
- type: "object",
- properties: {
- rows: {
- type: "array",
- items: {
- type: "object",
- properties: {
- week: { type: "string" },
- total: { type: "integer" },
- },
- },
- },
- },
- },
- annotations: { readOnlyHint: true },
- },
- {
- name: "get_star_history",
- description:
- "Get star history for a repo — who starred it and when, with cumulative count for charting.",
- inputSchema: {
- type: "object",
- properties: {
- repo: {
- type: "string",
- description: "Repository name (e.g. 'linux')",
- },
- },
- required: ["repo"],
- },
- outputSchema: {
- type: "object",
- properties: {
- rows: {
- type: "array",
- items: {
- type: "object",
- properties: {
- starred_at: { type: "string" },
- cumulative: { type: "integer" },
- },
- },
- },
- },
- },
- annotations: { readOnlyHint: true },
- },
- {
- name: "get_contributors",
- description: "Get contributor stats for a repo — login, avatar, total commits.",
- inputSchema: {
- type: "object",
- properties: {
- repo: {
- type: "string",
- description: "Repository name (e.g. 'linux')",
- },
- },
- required: ["repo"],
- },
- outputSchema: {
- type: "object",
- properties: {
- rows: {
- type: "array",
- items: {
- type: "object",
- properties: {
- login: { type: "string" },
- avatar_url: { type: "string" },
- total_commits: { type: "integer" },
- },
- },
- },
- },
- },
- annotations: { readOnlyHint: true },
- },
- {
- name: "save_bookmark",
- description:
- "Save a repository bookmark with a note and tag. Upserts — saving the same repo updates it.",
- inputSchema: {
- type: "object",
- properties: {
- repo: { type: "string", description: "Repository name to bookmark" },
- note: { type: "string", description: "User note about the repo" },
- tag: {
- type: "string",
- enum: ["important", "review", "learning", "favorite"],
- description: "Tag category",
- },
- },
- required: ["repo"],
- },
- outputSchema: {
- type: "object",
- properties: {
- success: { type: "boolean" },
- },
- },
- annotations: { readOnlyHint: false },
- },
- {
- name: "get_bookmarks",
- description: "Get all saved bookmarks for the current user.",
- inputSchema: { type: "object", properties: {} },
- outputSchema: {
- type: "object",
- properties: {
- rows: {
- type: "array",
- items: {
- type: "object",
- properties: {
- repo: { type: "string" },
- note: { type: "string" },
- tag: { type: "string" },
- created_at: { type: "string" },
- },
- },
- },
- },
- },
- annotations: { readOnlyHint: true },
- },
- {
- name: "delete_bookmark",
- description: "Delete a bookmark by repo name.",
- inputSchema: {
- type: "object",
- properties: {
- repo: { type: "string", description: "Repository name to remove" },
- },
- required: ["repo"],
- },
- outputSchema: {
- type: "object",
- properties: {
- success: { type: "boolean" },
- },
- },
- annotations: { readOnlyHint: false, destructiveHint: true },
- },
-];
diff --git a/docs/app/demo/github/layout.css b/docs/app/demo/github/layout.css
deleted file mode 100644
index 07362f805..000000000
--- a/docs/app/demo/github/layout.css
+++ /dev/null
@@ -1,814 +0,0 @@
-/* Scope playground resets so they do not leak into other routes after navigation.
- Use `:where()` to keep specificity at zero and avoid overriding react-ui classes. */
-:where(.app, .app *, .app *::before, .app *::after) {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
-}
-
-/* ─── Global ────────────────────────────────────────────────────────────────── */
-.app {
- font-family: var(--openui-font-body);
- background-color: var(--openui-foreground);
- color: var(--openui-text-neutral-primary);
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- transition:
- background-color 0.2s ease,
- color 0.2s ease;
-}
-
-/* ─── Scrollbar ─────────────────────────────────────────────────────────────── */
-.app::-webkit-scrollbar,
-.app ::-webkit-scrollbar {
- width: 6px;
- height: 6px;
-}
-.app::-webkit-scrollbar-track,
-.app ::-webkit-scrollbar-track {
- background: transparent;
-}
-.app::-webkit-scrollbar-thumb,
-.app ::-webkit-scrollbar-thumb {
- background: var(--openui-text-neutral-tertiary);
- border-radius: 3px;
-}
-
-/* ─── App Layout ────────────────────────────────────────────────────────────── */
-.app {
- --playground-focus-ring: color-mix(
- in srgb,
- var(--openui-border-accent-emphasis) 24%,
- transparent
- );
- --playground-code-background: var(--openui-inverted-background);
- --playground-code-foreground: var(--openui-text-accent-primary);
- display: flex;
- flex-direction: column;
- height: 100vh;
- background: var(--openui-foreground);
- position: relative;
- isolation: isolate;
-}
-
-.app-body {
- display: flex;
- flex: 1;
- min-height: 0;
- overflow: hidden;
-}
-
-.app-body-home {
- align-items: stretch;
- overflow-y: auto;
-}
-
-.content-wrapper {
- flex: 1;
- display: flex;
- flex-direction: column;
- padding: var(--openui-space-2xl) var(--openui-space-xl);
- overflow: hidden;
- min-width: 0;
-}
-
-.content-wrapper-home {
- position: relative;
- align-items: stretch;
- justify-content: flex-start;
- padding: 0;
- overflow: visible;
-}
-
-.content-wrapper-home::before {
- content: "";
- position: absolute;
- inset: 0;
- background: var(--openui-foreground);
- pointer-events: none;
-}
-
-.content-wrapper-home > * {
- position: relative;
- z-index: 1;
-}
-
-/* ─── Split Screen ──────────────────────────────────────────────────────────── */
-.split-screen {
- display: grid;
- grid-template-columns: 2fr 3fr;
- gap: 16px;
- flex: 1;
- min-height: 0;
- overflow: hidden;
-}
-
-/* ─── Shared Panel ──────────────────────────────────────────────────────────── */
-.panel {
- display: flex;
- flex-direction: column;
- border: 1px solid var(--openui-border-default);
- border-radius: var(--openui-radius-xl);
- overflow: hidden;
- background: var(--openui-foreground);
- min-height: 0;
-}
-
-.panel-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: var(--openui-space-s-m) var(--openui-space-m-l);
- background: var(--openui-highlight-subtle);
- border-bottom: 1px solid var(--openui-border-default);
- flex-shrink: 0;
-}
-
-.panel-title {
- font-size: 12px;
- font-weight: 600;
- color: var(--openui-text-neutral-secondary);
- letter-spacing: 0.04em;
- text-transform: uppercase;
-}
-
-.panel-actions {
- display: flex;
- align-items: center;
- gap: var(--openui-space-2xs);
-}
-
-.openui-icon-button.panel-icon-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- background: transparent;
- border: none;
- border-radius: var(--openui-radius-s);
- color: var(--openui-text-neutral-tertiary);
- cursor: pointer;
- transition:
- background-color 0.15s,
- color 0.15s;
-}
-
-.openui-icon-button.panel-icon-btn:hover:not(:disabled) {
- background-color: var(--openui-highlight);
- color: var(--openui-text-neutral-primary);
-}
-
-.openui-icon-button.panel-icon-btn:focus-visible,
-.openui-button-base.send-btn:focus-visible,
-.openui-button-base.stop-btn:focus-visible,
-.openui-button-base.chip:focus-visible,
-.openui-button-base.dashboard-source-toggle:focus-visible,
-.openui-button-base.gh-connected-change:focus-visible {
- outline: 2px solid var(--openui-border-accent);
- outline-offset: 2px;
-}
-
-/* ─── Shared Empty State ────────────────────────────────────────────────────── */
-.empty-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: var(--openui-space-s);
- padding: var(--openui-space-2xl) var(--openui-space-xl);
- text-align: center;
- color: var(--openui-text-neutral-tertiary);
- height: 100%;
-}
-
-.empty-state-icon {
- opacity: 0.3;
- margin-bottom: 4px;
-}
-
-.empty-state-text {
- font-size: 13px;
- line-height: 1.5;
-}
-
-/* ─── Prompt Section ────────────────────────────────────────────────────────── */
-.prompt-section {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: var(--openui-space-m-l);
- padding: var(--openui-space-3xl) 0 var(--openui-space-2xl);
-}
-
-.prompt-heading {
- font-size: 28px;
- font-weight: 600;
- letter-spacing: -0.03em;
- color: var(--openui-text-neutral-primary);
- text-align: center;
-}
-
-.prompt-container {
- width: 100%;
- max-width: 760px;
- background: var(--openui-foreground);
- border: 1px solid var(--openui-border-interactive);
- border-radius: var(--openui-radius-2xl);
- display: flex;
- flex-direction: column;
- transition: border-color 0.15s;
- box-shadow: var(--openui-shadow-m);
-}
-
-.prompt-container:focus-within {
- border-color: var(--openui-border-accent-emphasis);
- box-shadow: 0 0 0 3px var(--playground-focus-ring);
-}
-
-.prompt-textarea {
- width: 100%;
- padding: var(--openui-space-m-l) var(--openui-space-m-l) 0;
- background: transparent;
- border: none;
- outline: none;
- resize: none;
- font-family: var(--openui-font-body);
- font-size: 15px;
- line-height: 1.5;
- color: var(--openui-text-neutral-primary);
- min-height: 64px;
- max-height: 240px;
- overflow-y: auto;
-}
-
-.prompt-textarea::placeholder {
- color: var(--openui-text-neutral-tertiary);
-}
-
-.prompt-textarea:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-.prompt-actions {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: var(--openui-space-s-m) var(--openui-space-m);
- gap: var(--openui-space-s);
-}
-
-.model-select {
- background: var(--openui-highlight-subtle);
- border: 1px solid var(--openui-border-default);
- border-radius: var(--openui-radius-m);
- color: var(--openui-text-neutral-secondary);
- font-family: var(--openui-font-body);
- font-size: 12px;
- font-weight: 500;
- padding: 5px 8px;
- cursor: pointer;
- outline: none;
- transition: border-color 0.15s;
- appearance: none;
- -webkit-appearance: none;
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: right 8px center;
- padding-right: 24px;
-}
-
-.model-select:focus {
- border-color: var(--openui-border-interactive);
-}
-
-.model-select:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-.openui-button-base.send-btn {
- display: flex;
- align-items: center;
- gap: var(--openui-space-xs);
- padding: var(--openui-space-xs) var(--openui-space-m);
- background: var(--openui-interactive-accent-default);
- color: var(--openui-text-accent-primary);
- border: none;
- border-radius: var(--openui-radius-m);
- font-family: var(--openui-font-body);
- font-size: 13px;
- font-weight: 500;
- cursor: pointer;
- transition:
- background 0.15s,
- opacity 0.15s;
- flex-shrink: 0;
-}
-
-.openui-button-base.send-btn:hover:not(:disabled) {
- background: var(--openui-interactive-accent-hover);
-}
-
-.openui-button-base.send-btn:disabled {
- opacity: 0.35;
- cursor: not-allowed;
-}
-
-.openui-button-base.stop-btn {
- display: flex;
- align-items: center;
- gap: var(--openui-space-xs);
- padding: var(--openui-space-xs) var(--openui-space-m);
- background: transparent;
- color: var(--openui-text-neutral-secondary);
- border: 1px solid var(--openui-border-interactive);
- border-radius: var(--openui-radius-m);
- font-family: var(--openui-font-body);
- font-size: 13px;
- font-weight: 500;
- cursor: pointer;
- transition:
- background 0.15s,
- color 0.15s,
- border-color 0.15s;
- flex-shrink: 0;
-}
-
-.openui-button-base.stop-btn:hover:not(:disabled) {
- background-color: var(--openui-highlight);
- color: var(--openui-text-neutral-primary);
- border-color: var(--openui-border-interactive-emphasis);
-}
-
-/* ─── Chips ─────────────────────────────────────────────────────────────────── */
-.chips-row {
- display: flex;
- flex-wrap: wrap;
- gap: var(--openui-space-s);
- justify-content: center;
- max-width: 760px;
- width: 100%;
-}
-
-.openui-button-base.chip {
- padding: var(--openui-space-s-m) var(--openui-space-m-l);
- background: var(--openui-highlight-subtle);
- border: 1px solid var(--openui-border-interactive);
- border-radius: var(--openui-radius-full);
- color: var(--openui-text-neutral-primary);
- font-size: 13px;
- cursor: pointer;
- transition:
- background 0.15s,
- color 0.15s,
- border-color 0.15s;
- white-space: nowrap;
- flex-shrink: 0;
-}
-
-.openui-button-base.chip:hover:not(:disabled),
-.openui-button-base.chip:active:not(:disabled) {
- background-color: var(--openui-highlight);
- color: var(--openui-text-neutral-primary);
- border-color: var(--openui-border-interactive);
-}
-
-.openui-button-base.chip-selected,
-.openui-button-base.chip-selected:hover:not(:disabled),
-.openui-button-base.chip-selected:active:not(:disabled) {
- border-color: var(--openui-border-interactive-selected);
-}
-
-.openui-button-base.chip:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-/* ─── Error Banner ──────────────────────────────────────────────────────────── */
-.error-banner {
- margin: 0 0 var(--openui-space-m-l);
- padding: var(--openui-space-m) var(--openui-space-m-l);
- background: var(--openui-danger-background);
- border: 1px solid var(--openui-border-danger);
- border-radius: var(--openui-radius-m);
- font-size: 13px;
- color: var(--openui-text-danger-primary);
- line-height: 1.5;
-}
-
-/* ─── Artifact Layout (GitHub connected mode) ──────────────────────────────── */
-.artifact-layout {
- display: flex;
- flex: 1;
- min-height: 0;
- overflow: hidden;
-}
-
-.dashboard-area {
- flex: 1;
- overflow: auto;
- padding: var(--openui-space-m-l) var(--openui-space-xl);
- min-width: 0;
-}
-
-.dashboard-renderer {
- min-height: 200px;
-}
-
-.dashboard-renderer .panel {
- border-radius: var(--openui-radius-xl);
- box-shadow: var(--openui-shadow-s);
-}
-
-.dashboard-meta {
- display: flex;
- gap: var(--openui-space-m);
- margin-bottom: var(--openui-space-s);
- font-size: 12px;
- color: var(--openui-text-neutral-tertiary);
- align-items: center;
-}
-
-.dashboard-model-label {
- padding: 2px var(--openui-space-s);
- border-radius: var(--openui-radius-full);
- background: var(--openui-highlight-subtle);
- border: 1px solid var(--openui-border-default);
- font-size: 11px;
- font-weight: 500;
- font-family: var(--openui-font-code);
- color: var(--openui-text-neutral-secondary);
-}
-
-.dashboard-elapsed {
- font-family: var(--openui-font-code);
-}
-
-.openui-button-base.dashboard-source-toggle {
- display: flex;
- align-items: center;
- gap: var(--openui-space-2xs);
- padding: var(--openui-space-2xs) var(--openui-space-s);
- background: var(--openui-highlight-subtle);
- border: 1px solid var(--openui-border-default);
- border-radius: var(--openui-radius-full);
- cursor: pointer;
- color: var(--openui-text-neutral-secondary);
- font-size: 12px;
- font-family: var(--openui-font-body);
- transition:
- background-color 0.15s,
- border-color 0.15s,
- color 0.15s;
-}
-
-.openui-button-base.dashboard-source-toggle:hover:not(:disabled) {
- background: var(--openui-highlight);
- border-color: var(--openui-border-interactive);
- color: var(--openui-text-neutral-primary);
-}
-
-/* Token saving badge */
-.dashboard-token-saving {
- display: inline-flex;
- align-items: center;
- gap: 3px;
- padding: 2px var(--openui-space-s);
- border-radius: var(--openui-radius-full);
- background: var(--openui-success-background);
- color: var(--openui-text-success-primary);
- font-size: 11px;
- font-weight: 600;
- font-family: var(--openui-font-body);
-}
-
-/* Source panel with tabs */
-.dashboard-source-panel {
- margin-bottom: var(--openui-space-m);
- border: 1px solid var(--openui-border-default);
- border-radius: var(--openui-radius-m);
- overflow: hidden;
-}
-
-.dashboard-source-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- background: var(--openui-highlight-subtle);
- border-bottom: 1px solid var(--openui-border-default);
- padding-right: var(--openui-space-s);
-}
-
-.dashboard-source-tabs {
- display: flex;
- gap: 0;
-}
-
-.dashboard-source-tab {
- display: flex;
- align-items: center;
- gap: var(--openui-space-xs);
- padding: var(--openui-space-s) var(--openui-space-m);
- background: transparent;
- border: none;
- border-bottom: 2px solid transparent;
- color: var(--openui-text-neutral-tertiary);
- font-size: 11px;
- font-weight: 600;
- font-family: var(--openui-font-body);
- cursor: pointer;
- transition:
- color 0.15s,
- border-color 0.15s;
-}
-
-.dashboard-source-tab:hover {
- color: var(--openui-text-neutral-secondary);
-}
-
-.dashboard-source-tab-active {
- color: var(--openui-text-neutral-primary);
- border-bottom-color: var(--openui-border-accent-emphasis);
-}
-
-.dashboard-source-token-count {
- padding: 1px 6px;
- border-radius: var(--openui-radius-full);
- background: var(--openui-highlight);
- color: var(--openui-text-neutral-secondary);
- font-size: 10px;
- font-weight: 600;
- font-family: var(--openui-font-code);
-}
-
-.dashboard-source-copy {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- background: transparent;
- border: none;
- border-radius: var(--openui-radius-s);
- color: var(--openui-text-neutral-tertiary);
- cursor: pointer;
- transition:
- background-color 0.15s,
- color 0.15s;
- flex-shrink: 0;
-}
-
-.dashboard-source-copy:hover {
- background: var(--openui-highlight);
- color: var(--openui-text-neutral-primary);
-}
-
-.dashboard-source-panel .dashboard-source {
- border: none;
- border-radius: 0;
- margin-bottom: 0;
-}
-
-.dashboard-source {
- background: var(--openui-sunk);
- padding: var(--openui-space-m);
- font-size: 11px;
- font-family: var(--openui-font-code);
- overflow: auto;
- white-space: pre-wrap;
- max-height: 250px;
- line-height: 1.6;
- margin-bottom: var(--openui-space-m);
-}
-
-/* Live data indicator strip */
-.dashboard-data-strip {
- display: flex;
- align-items: center;
- gap: var(--openui-space-s);
- padding: var(--openui-space-xs) var(--openui-space-m);
- margin-bottom: var(--openui-space-s);
- border: 1px solid var(--openui-border-success);
- border-radius: var(--openui-radius-m);
- background: var(--openui-success-background);
- font-size: 11px;
- font-family: var(--openui-font-body);
- transition:
- border-color 0.2s,
- background-color 0.2s;
-}
-
-.dashboard-data-strip-loading {
- border-color: var(--openui-border-alert);
- background: var(--openui-alert-background);
-}
-
-.dashboard-data-label {
- color: var(--openui-text-success-primary);
- font-weight: 600;
- white-space: nowrap;
-}
-
-.dashboard-data-strip-loading .dashboard-data-label {
- color: var(--openui-text-alert-primary);
-}
-
-.dashboard-data-chips {
- display: flex;
- flex-wrap: wrap;
- gap: 3px;
-}
-
-.dashboard-data-chip {
- padding: 1px var(--openui-space-xs);
- border-radius: var(--openui-radius-xs);
- font-size: 10px;
- font-family: var(--openui-font-code);
-}
-
-.dashboard-data-chip-done {
- background: var(--openui-success-background);
- color: var(--openui-text-success-primary);
-}
-
-.dashboard-data-chip-pending {
- background: var(--openui-alert-background);
- color: var(--openui-text-alert-primary);
-}
-
-.dashboard-data-chip-error {
- background: var(--openui-danger-background);
- color: var(--openui-text-danger-primary);
-}
-
-.dashboard-loading {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 60px var(--openui-space-xl);
- color: var(--openui-text-neutral-tertiary);
- gap: var(--openui-space-xs);
-}
-
-.dashboard-loading-text {
- font-size: 14px;
-}
-
-.dashboard-loading-timer {
- font-size: 12px;
- font-family: var(--openui-font-code);
-}
-
-/* Connected user bar */
-.gh-connected-bar {
- display: flex;
- align-items: center;
- gap: var(--openui-space-s);
- padding: var(--openui-space-s) var(--openui-space-m);
- margin-bottom: var(--openui-space-m);
- border: 1px solid var(--openui-border-default);
- border-radius: var(--openui-radius-m);
- background: var(--openui-highlight-subtle);
- font-size: 13px;
- color: var(--openui-text-neutral-secondary);
- font-family: var(--openui-font-body);
-}
-
-.gh-connected-avatar {
- width: 20px;
- height: 20px;
- border-radius: 50%;
-}
-
-.openui-button-base.gh-connected-change {
- margin-left: auto;
- background: transparent;
- border: none;
- color: var(--openui-text-neutral-tertiary);
- font-size: 12px;
- cursor: pointer;
- padding: var(--openui-space-2xs) var(--openui-space-xs);
- border-radius: var(--openui-radius-s);
- font-family: var(--openui-font-body);
- box-shadow: none;
- transition:
- color 0.15s,
- background-color 0.15s;
-}
-
-.openui-button-base.gh-connected-change:hover:not(:disabled) {
- color: var(--openui-text-neutral-primary);
- background: var(--openui-highlight);
-}
-
-/* GitHub starters welcome (inside artifact layout) */
-.gh-starters-welcome {
- max-width: 600px;
- margin: var(--openui-space-3xl) auto;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: var(--openui-space-xl);
-}
-
-.gh-welcome-text {
- display: flex;
- align-items: center;
- gap: var(--openui-space-s);
- font-size: 15px;
- color: var(--openui-text-neutral-secondary);
- font-family: var(--openui-font-body);
- text-align: center;
- flex-wrap: wrap;
- justify-content: center;
-}
-
-.gh-welcome-avatar {
- display: flex;
- align-items: center;
-}
-
-.gh-welcome-avatar-image {
- width: 24px;
- height: 24px;
- border-radius: var(--openui-radius-full);
-}
-
-.gh-starters-grid-compact {
- width: min(100%, 600px);
-}
-
-.gh-starter-icon {
- display: inline-flex;
- width: 24px;
- height: 24px;
- align-items: center;
- justify-content: center;
- border-radius: var(--openui-radius-full);
-}
-
-/* ─── Responsive ────────────────────────────────────────────────────────────── */
-@media (max-width: 768px) {
- .app {
- height: 100dvh;
- overflow: hidden;
- }
-
- .app-body {
- flex-direction: column;
- overflow-y: auto;
- overflow-x: hidden;
- }
-
- .content-wrapper {
- flex: none;
- overflow: visible;
- padding: 20px var(--openui-space-m-l) var(--openui-space-2xl);
- }
-
- .content-wrapper-home {
- padding: 0;
- }
-
- .split-screen {
- display: flex;
- flex-direction: column;
- gap: 12px;
- overflow: visible;
- min-height: 0;
- }
-
- .panel {
- height: 380px;
- flex-shrink: 0;
- }
-
- .prompt-section {
- padding: 24px 0 20px;
- gap: 12px;
- }
-
- .prompt-heading {
- font-size: 20px;
- }
-
- .chips-row {
- gap: 6px;
- }
-
- .chip {
- font-size: 12px;
- padding: 5px 12px;
- }
-
- .artifact-layout {
- flex-direction: column;
- }
-
- .dashboard-area {
- padding: var(--openui-space-m) var(--openui-space-m-l);
- }
-}
diff --git a/docs/app/demo/github/layout.tsx b/docs/app/demo/github/layout.tsx
deleted file mode 100644
index 39f12f65d..000000000
--- a/docs/app/demo/github/layout.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import { WebsiteThemeProvider } from "@/components/website-theme-provider";
-import type { ReactNode } from "react";
-import "./layout.css";
-
-export default function DemoGitHubLayout({ children }: { children: ReactNode }) {
- return {children};
-}
diff --git a/docs/app/demo/github/page.tsx b/docs/app/demo/github/page.tsx
deleted file mode 100644
index 5afec7f00..000000000
--- a/docs/app/demo/github/page.tsx
+++ /dev/null
@@ -1,712 +0,0 @@
-"use client";
-
-import { mergeStatements } from "@openuidev/react-lang";
-import { Button } from "@openuidev/react-ui";
-import { encode } from "gpt-tokenizer";
-import {
- Activity,
- Check,
- CircleDot,
- Code2,
- Copy,
- GitPullRequest,
- Hexagon,
- Search,
- Zap,
- type LucideIcon,
-} from "lucide-react";
-import { useTheme } from "next-themes";
-import { Highlight, themes } from "prism-react-renderer";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { ConversationPanel } from "./components/ConversationPanel/ConversationPanel";
-import { GitHubConnect } from "./components/GitHubConnect/GitHubConnect";
-import { Header } from "./components/Header/Header";
-import { PreviewPanel } from "./components/PreviewPanel/PreviewPanel";
-import {
- GITHUB_DEMO_MODEL_LABEL,
- GITHUB_STARTERS,
- type ChatMessage,
- type GitHubStarterIconKey,
- type Status,
- type Theme,
- type ToolCallEntry,
-} from "./constants";
-import { clearCache, createGitHubToolProvider, prefetchAndSummarize } from "./github/tools";
-
-// ── Helpers ─────────────────────────────────────────────────────────────────
-
-function extractCodeOnly(response: string): string | null {
- const fenceRegex = /```[\w-]*\n([\s\S]*?)```/g;
- const blocks: string[] = [];
- let match;
- while ((match = fenceRegex.exec(response)) !== null) {
- blocks.push(match[1].trim());
- }
- if (blocks.length > 0) return blocks.join("\n");
-
- const unclosedMatch = response.match(/```[\w-]*\n([\s\S]*)$/);
- if (unclosedMatch) return unclosedMatch[1].trim() || null;
-
- if (isPureCode(response)) return response;
- return null;
-}
-
-function extractText(response: string): string {
- const withoutFences = response.replace(/```[\w-]*\n[\s\S]*?```/g, "").trim();
- const withoutUnclosed = withoutFences.replace(/```[\w-]*\n[\s\S]*$/g, "").trim();
- if (withoutUnclosed && isPureCode(withoutUnclosed)) return "";
- return withoutUnclosed;
-}
-
-function responseHasCode(response: string): boolean {
- if (/```[\w-]*\n/.test(response)) return true;
- const trimmed = response.trim();
- if (/^[a-zA-Z_$][\w$]*\s*=\s*/.test(trimmed)) return true;
- return false;
-}
-
-function isPureCode(response: string): boolean {
- const trimmed = response.trim();
- if (/```/.test(trimmed)) return false;
- const lines = trimmed.split("\n").filter((l) => l.trim());
- if (lines.length === 0) return false;
- const stmtPattern = /^[a-zA-Z_$][\w$]*\s*=/;
- const stmtCount = lines.filter((l) => stmtPattern.test(l.trim())).length;
- return stmtCount / lines.length > 0.7;
-}
-
-const STARTER_ICON_MAP: Record = {
- "commit-activity": Activity,
- "pull-requests": GitPullRequest,
- "issue-tracking": CircleDot,
- "code-reviews": Search,
- "language-breakdown": Code2,
- "repository-stats": Hexagon,
-};
-
-function renderStarterIcon(icon: GitHubStarterIconKey) {
- const Icon = STARTER_ICON_MAP[icon];
- return ;
-}
-
-// ── Tool call tracking wrapper ───────────────────────────────────────────
-
-type ToolCallListener = (calls: ToolCallEntry[]) => void;
-
-function wrapToolProvider(
- inner: Record) => Promise>,
- listener: ToolCallListener,
-): {
- tools: Record) => Promise>;
- resetCalls: () => void;
-} {
- let activeCalls: ToolCallEntry[] = [];
- const wrapped: Record) => Promise> = {};
-
- for (const [name, fn] of Object.entries(inner)) {
- wrapped[name] = async (args) => {
- const entry: ToolCallEntry = { tool: name, status: "pending" };
- activeCalls.push(entry);
- listener([...activeCalls]);
- try {
- const data = await fn(args);
- entry.status = "done";
- listener([...activeCalls]);
- return data;
- } catch {
- entry.status = "error";
- listener([...activeCalls]);
- return null;
- }
- };
- }
-
- return {
- tools: wrapped,
- resetCalls: () => {
- activeCalls = [];
- },
- };
-}
-
-// ── SSE streaming ────────────────────────────────────────────────────────
-
-async function streamChat(
- body: Record,
- onChunk: (text: string) => void,
- onDone: () => void,
- signal?: AbortSignal,
- onFirstChunk?: () => void,
-) {
- const res = await fetch("/api/demo/github/stream", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body),
- signal,
- });
-
- if (!res.ok) {
- const err = await res.json().catch(() => ({}));
- onChunk(
- `Error: ${(err as { error?: { message?: string } }).error?.message ?? `Server error ${res.status}`}`,
- );
- onDone();
- return;
- }
-
- const reader = res.body!.getReader();
- const decoder = new TextDecoder();
- let firstChunkFired = false;
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- const raw = decoder.decode(value, { stream: true });
- for (const line of raw.split("\n")) {
- const trimmed = line.trim();
- if (!trimmed.startsWith("data:")) continue;
- const data = trimmed.slice(5).trim();
- if (data === "[DONE]") {
- onDone();
- return;
- }
- try {
- const parsed = JSON.parse(data) as {
- choices: Array<{ delta: { content?: string } }>;
- };
- const content = parsed.choices[0]?.delta?.content;
- if (content) {
- if (!firstChunkFired) {
- firstChunkFired = true;
- onFirstChunk?.();
- }
- onChunk(content);
- }
- } catch {
- // skip malformed chunks
- }
- }
- }
- onDone();
-}
-
-// ── Main Page ────────────────────────────────────────────────────────────
-
-export default function GitHubDemoPage() {
- // Theme
- const { theme, setTheme, resolvedTheme } = useTheme();
-
- // GitHub connection
- const [githubUsername, setGithubUsername] = useState(null);
- const [toolProvider, setToolProvider] = useState) => Promise
- > | null>(null);
-
- // Dashboard state
- const [dashboardCode, setDashboardCode] = useState(null);
- const [showSource, setShowSource] = useState(false);
- const [sourceTab, setSourceTab] = useState<"raw" | "json">("raw");
- const [parsedJson, setParsedJson] = useState(null);
- const [codeCopied, setCodeCopied] = useState(false);
-
- // Conversation
- const [conversation, setConversation] = useState([]);
- const [streamingText, setStreamingText] = useState("");
- const [toolCalls, setToolCalls] = useState([]);
- const [streamResponseHasCode, setStreamResponseHasCode] = useState(false);
-
- // Streaming
- const [status, setStatus] = useState("idle");
- const [startTime, setStartTime] = useState(null);
- const [elapsed, setElapsed] = useState(null);
- const [errorMsg, setErrorMsg] = useState("");
-
- const abortRef = useRef(null);
- const responseRef = useRef("");
- const pendingPromptRef = useRef(null);
-
- const isStreaming = status === "streaming";
- const hasDashboard = dashboardCode !== null;
- const isGitHub = githubUsername !== null;
- const isHomeState = !isGitHub && !hasDashboard && conversation.length === 0;
-
- // Token comparison
- const rawTokens = useMemo(
- () => (dashboardCode ? encode(dashboardCode).length : 0),
- [dashboardCode],
- );
- const jsonTokens = useMemo(() => (parsedJson ? encode(parsedJson).length : 0), [parsedJson]);
- const tokenSavingPct =
- status === "done" && rawTokens > 0 && jsonTokens > 0 && rawTokens < jsonTokens
- ? Math.round(((jsonTokens - rawTokens) / jsonTokens) * 100)
- : null;
-
- // Theme
- const currentTheme = useMemo(() => {
- if (theme === "light" || theme === "dark" || theme === "system") {
- return theme;
- }
- return "system";
- }, [theme]);
-
- const resolvedMode = resolvedTheme === "dark" ? "dark" : "light";
-
- const cycleTheme = () =>
- setTheme(currentTheme === "system" ? "light" : currentTheme === "light" ? "dark" : "system");
-
- // Timer
- useEffect(() => {
- if (!isStreaming || !startTime) return;
- const iv = setInterval(() => setElapsed(Date.now() - startTime), 100);
- return () => clearInterval(iv);
- }, [isStreaming, startTime]);
-
- // ── GitHub connect ───────────────────────────────────────────────────
-
- const rawToolsRef = useRef) => Promise
- > | null>(null);
- const resetToolCallsRef = useRef<(() => void) | null>(null);
-
- const handleConnect = useCallback((username: string) => {
- const rawTools = createGitHubToolProvider(username);
- rawToolsRef.current = rawTools;
- const { tools: wrapped, resetCalls } = wrapToolProvider(rawTools, (calls) => {
- setToolCalls([...calls]);
- });
- resetToolCallsRef.current = resetCalls;
- setGithubUsername(username);
- setToolProvider(wrapped);
- }, []);
-
- const handleDisconnect = () => {
- abortRef.current?.abort();
- abortRef.current = null;
- clearCache();
- setGithubUsername(null);
- setToolProvider(null);
- setDashboardCode(null);
- setConversation([]);
- setStatus("idle");
- setStreamingText("");
- setToolCalls([]);
- setStreamResponseHasCode(false);
- setElapsed(null);
- setShowSource(false);
- setParsedJson(null);
- setErrorMsg("");
- };
-
- // ── Send message ─────────────────────────────────────────────────────
-
- const send = useCallback(
- async (text: string) => {
- if (!text.trim() || isStreaming) return;
- const trimmed = text.trim();
-
- setStatus("streaming");
- setStartTime(null);
- setElapsed(null);
- setErrorMsg("");
- responseRef.current = "";
- setStreamingText("");
- setToolCalls([]);
- resetToolCallsRef.current?.();
- setStreamResponseHasCode(false);
- let streamStartTime: number | null = null;
-
- const userMsg: ChatMessage = {
- role: "user",
- content: trimmed,
- hasCode: false,
- };
- const updated = [...conversation, userMsg];
- setConversation(updated);
- const existingCode = dashboardCode;
-
- // Build API messages
- const apiMessages = updated.map((m, i) => {
- if (m.role === "user" && i === updated.length - 1 && existingCode) {
- return {
- role: m.role,
- content: `${m.content}\n\n\n${existingCode}\n`,
- };
- }
- return { role: m.role, content: m.content };
- });
-
- // Prefetch GitHub data on first message to warm cache + give LLM context
- // Use raw (unwrapped) tools to avoid triggering tool-call tracking side effects
- let githubContext = "";
- if (conversation.length === 0 && rawToolsRef.current) {
- try {
- githubContext = await prefetchAndSummarize(rawToolsRef.current);
- } catch {
- // Non-critical — LLM just won't have data context
- }
- }
-
- const controller = new AbortController();
- abortRef.current = controller;
-
- try {
- await streamChat(
- {
- prompt: githubContext ? `${githubContext}\n\n${trimmed}` : trimmed,
- messages: apiMessages.slice(0, -1),
- },
- (chunk) => {
- responseRef.current += chunk;
- const raw = responseRef.current;
- setStreamingText(extractText(raw) || "");
- setStreamResponseHasCode(responseHasCode(raw));
- if (existingCode) {
- setDashboardCode(existingCode + "\n" + raw);
- } else {
- setDashboardCode(raw);
- }
- },
- () => {
- setStatus("done");
- abortRef.current = null;
- setStreamingText("");
- setStreamResponseHasCode(false);
- if (streamStartTime) setElapsed(Date.now() - streamStartTime);
-
- const raw = responseRef.current;
- const hasCode = responseHasCode(raw);
- const pureCode = isPureCode(raw);
- const text = pureCode ? undefined : extractText(raw) || undefined;
-
- const assistantMsg: ChatMessage = {
- role: "assistant",
- content: raw,
- text,
- hasCode,
- };
- setConversation((prev) => [...prev, assistantMsg]);
-
- if (hasCode) {
- const newCode = pureCode ? raw : extractCodeOnly(raw);
- if (newCode) {
- const merged = existingCode ? mergeStatements(existingCode, newCode) : newCode;
- setDashboardCode(merged);
- }
- }
- },
- controller.signal,
- () => {
- streamStartTime = Date.now();
- setStartTime(streamStartTime);
- },
- );
- } catch (err: unknown) {
- if (err instanceof Error && err.name === "AbortError") {
- setStreamResponseHasCode(false);
- setStatus("idle");
- return;
- }
- setErrorMsg(err instanceof Error ? err.message : "Unknown error");
- setStreamResponseHasCode(false);
- setStatus("error");
- }
- },
- [isStreaming, conversation, dashboardCode],
- );
-
- // Process pending prompt after GitHub connect
- useEffect(() => {
- if (githubUsername && toolProvider && pendingPromptRef.current) {
- const p = pendingPromptRef.current;
- pendingPromptRef.current = null;
- send(p);
- }
- }, [githubUsername, toolProvider, send]);
-
- const handleStop = () => {
- abortRef.current?.abort();
- setStreamResponseHasCode(false);
- setStatus("idle");
- };
-
- const handleConnectAndPrompt = useCallback(
- (username: string, promptText: string) => {
- pendingPromptRef.current = promptText;
- handleConnect(username);
- },
- [handleConnect],
- );
-
- // ── Render ─────────────────────────────────────────────────────────────
-
- const showConversation = conversation.length > 0 || isStreaming;
-
- return (
-
-
-
-
- {/* Phase 1: Connect Screen */}
- {isHomeState && (
-
-
-
- )}
-
- {/* Phase 2: Artifact Layout */}
- {(isGitHub || hasDashboard || conversation.length > 0) && (
-
- {/* Left: Dashboard */}
-
- {/* GitHub starters (before first generation) */}
- {isGitHub && !hasDashboard && conversation.length === 0 && (
-
-
-
-
-
- Connected as
@{githubUsername}. What do you want to build?
-
-
- {GITHUB_STARTERS.map((s) => (
-
- ))}
-
-
- )}
-
- {/* Meta + source toggle + token comparison */}
- {hasDashboard && !isStreaming && (
-
- {GITHUB_DEMO_MODEL_LABEL}
- {elapsed && (
- {(elapsed / 1000).toFixed(1)}s
- )}
- {tokenSavingPct !== null && (
-
-
- {tokenSavingPct}% fewer tokens
-
- )}
-
-
- )}
-
- {/* Source code view with tabs */}
- {hasDashboard &&
- showSource &&
- (() => {
- const activeSource =
- sourceTab === "raw"
- ? (dashboardCode ?? "")
- : (parsedJson ?? "Waiting for parse...");
- const handleCopy = async () => {
- await navigator.clipboard.writeText(activeSource);
- setCodeCopied(true);
- setTimeout(() => setCodeCopied(false), 2000);
- };
- return (
-
-
-
-
-
-
-
-
-
-
- {({ className, style, tokens, getLineProps, getTokenProps }) => (
-
-
- {tokens.map((line, i) => (
-
- {line.map((token, j) => (
-
- ))}
-
- ))}
-
-
- )}
-
-
-
- );
- })()}
-
- {/* Connected user bar */}
- {isGitHub && (hasDashboard || conversation.length > 0) && (
-
-

-
@{githubUsername}
-
-
- )}
-
- {/* Live data indicator */}
- {hasDashboard && toolCalls.length > 0 && (
-
t.status === "pending") ? "dashboard-data-strip-loading" : ""}`}
- >
-
- {toolCalls.some((t) => t.status === "pending")
- ? "Fetching live data..."
- : "Live data from GitHub"}
-
-
- {toolCalls.map((tc, i) => (
-
- {tc.status === "done" ? "✓" : tc.status === "error" ? "✗" : "⏳"}{" "}
- {tc.tool.replace("get_", "")}
-
- ))}
-
-
- )}
-
- {/* Dashboard renderer */}
- {hasDashboard && (
-
-
setParsedJson(r ? JSON.stringify(r, null, 2) : null)}
- mode={resolvedMode}
- toolProvider={toolProvider}
- onAction={(event) => {
- if (event.type === "continue_conversation") {
- const text =
- (typeof event.params?.context === "string" ? event.params.context : "") ||
- event.humanFriendlyMessage ||
- "";
- if (text) send(text);
- } else if (event.type === "open_url") {
- const url = event.params?.["url"] as string | undefined;
- if (url) window.open(url, "_blank");
- }
- }}
- />
-
- )}
-
- {/* Streaming placeholder */}
- {isStreaming && !hasDashboard && (
-
-
Generating dashboard...
- {elapsed && (
-
{(elapsed / 1000).toFixed(1)}s
- )}
-
- )}
-
-
- {/* Right: Conversation Panel */}
- {showConversation && (
-
- )}
-
- )}
-
-
- {/* Error banner */}
- {status === "error" && errorMsg &&
{errorMsg}
}
-
- );
-}
diff --git a/docs/app/playground/components/PreviewPanel/PreviewPanel.tsx b/docs/app/playground/components/PreviewPanel/PreviewPanel.tsx
index 022f95fca..7510b9cb6 100644
--- a/docs/app/playground/components/PreviewPanel/PreviewPanel.tsx
+++ b/docs/app/playground/components/PreviewPanel/PreviewPanel.tsx
@@ -21,7 +21,7 @@ export function PreviewPanel({ code, isStreaming, onParseResult, theme }: Previe
useEffect(() => {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
- setSystemDark(mq.matches);
+ queueMicrotask(() => setSystemDark(mq.matches));
const handler = (e: MediaQueryListEvent) => setSystemDark(e.matches);
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
diff --git a/docs/components/site-marketing-header.module.css b/docs/components/site-marketing-header.module.css
index 67caff41f..53969c7fd 100644
--- a/docs/components/site-marketing-header.module.css
+++ b/docs/components/site-marketing-header.module.css
@@ -85,7 +85,7 @@
width: 100%;
height: 3.5rem;
align-items: center;
- gap: 0.25rem;
+ gap: 0.5rem;
padding-inline: 0.75rem;
border-radius: 0.375rem;
color: var(--openui-text-neutral-primary);
@@ -96,6 +96,18 @@
letter-spacing: var(--openui-text-body-lg-letter-spacing);
}
+.mobileTrayBadge {
+ padding: 0.125rem 0.375rem;
+ border-radius: 4px;
+ background: rgba(124, 58, 237, 0.12);
+ color: #7c3aed;
+ font-size: 10px;
+ font-weight: 600;
+ line-height: 1;
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+}
+
.mobileGithubButtonWrap {
display: flex;
justify-content: center;
diff --git a/docs/components/site-marketing-header.tsx b/docs/components/site-marketing-header.tsx
index e873b338a..e6ad45532 100644
--- a/docs/components/site-marketing-header.tsx
+++ b/docs/components/site-marketing-header.tsx
@@ -82,14 +82,18 @@ function MobileMenu({ starCount, onClose }: { starCount: number | null; onClose:
>
- {PRIMARY_SITE_NAV_ITEMS.map((item, index) => (
-
- {index > 0 &&
}
-
- {item.title}
-
-
- ))}
+ {PRIMARY_SITE_NAV_ITEMS.map((item, index) => {
+ const badge = "badge" in item ? item.badge : undefined;
+ return (
+
+ {index > 0 &&
}
+
+ {item.title}
+ {badge &&
{badge}}
+
+
+ );
+ })}
@@ -127,7 +131,7 @@ export function SiteMarketingHeader({
useEffect(() => {
if (borderMode === "always") {
- setIsScrolled(true);
+ queueMicrotask(() => setIsScrolled(true));
return;
}
diff --git a/docs/components/site-primary-nav.module.css b/docs/components/site-primary-nav.module.css
index fe7e02ad8..81b4eb7b7 100644
--- a/docs/components/site-primary-nav.module.css
+++ b/docs/components/site-primary-nav.module.css
@@ -29,6 +29,19 @@
background: var(--openui-highlight-strong);
}
+.badge {
+ margin-left: 0.375rem;
+ padding: 0.125rem 0.375rem;
+ border-radius: 4px;
+ background: rgba(124, 58, 237, 0.12);
+ color: #7c3aed;
+ font-size: 10px;
+ font-weight: 600;
+ line-height: 1;
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+}
+
@media (min-width: 1024px) {
.nav {
display: flex;
diff --git a/docs/components/site-primary-nav.tsx b/docs/components/site-primary-nav.tsx
index 0d95a6ebc..bda8515a5 100644
--- a/docs/components/site-primary-nav.tsx
+++ b/docs/components/site-primary-nav.tsx
@@ -9,6 +9,7 @@ export const PRIMARY_SITE_NAV_ITEMS = [
{ title: "Playground", href: "/playground", newTab: false },
{ title: "Demo", href: "/demo/github", newTab: true },
{ title: "Blogs", href: "/blog", newTab: false },
+ { title: "OpenClaw OS", href: "/openclaw-os", newTab: false, badge: "New" },
] as const;
export function SitePrimaryNav() {
@@ -18,6 +19,7 @@ export function SitePrimaryNav() {