diff --git a/docs/images/inspector.png b/docs/images/inspector.png index 1c16ed2..7966848 100644 Binary files a/docs/images/inspector.png and b/docs/images/inspector.png differ diff --git a/frontend/packages/inspector/index.html b/frontend/packages/inspector/index.html index b9ebbdb..29e4970 100644 --- a/frontend/packages/inspector/index.html +++ b/frontend/packages/inspector/index.html @@ -1970,6 +1970,84 @@ white-space: pre-wrap; } + .markdown-body p { + margin: 0; + } + + .markdown-body p + p { + margin-top: 8px; + } + + .markdown-body h1, + .markdown-body h2, + .markdown-body h3, + .markdown-body h4, + .markdown-body h5, + .markdown-body h6 { + margin: 0 0 8px; + line-height: 1.35; + font-weight: 700; + } + + .markdown-body h1 { font-size: 1.35em; } + .markdown-body h2 { font-size: 1.22em; } + .markdown-body h3 { font-size: 1.12em; } + .markdown-body h4, + .markdown-body h5, + .markdown-body h6 { font-size: 1em; } + + .markdown-body ul, + .markdown-body ol { + margin: 8px 0 0 18px; + padding: 0; + } + + .markdown-body li + li { + margin-top: 4px; + } + + .markdown-body blockquote { + margin: 8px 0 0; + padding: 6px 10px; + border-left: 3px solid var(--border-2); + background: var(--surface-2); + color: var(--muted); + border-radius: 4px; + } + + .markdown-body code { + font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, monospace; + font-size: 0.9em; + background: rgba(255, 255, 255, 0.08); + border: 1px solid var(--border-2); + border-radius: 4px; + padding: 1px 5px; + } + + .message.user .markdown-body code { + background: rgba(0, 0, 0, 0.08); + border-color: rgba(0, 0, 0, 0.12); + } + + .markdown-body .md-pre { + margin: 8px 0 0; + padding: 8px 10px; + border-radius: 6px; + border: 1px solid var(--border); + background: var(--surface-2); + overflow-x: auto; + } + + .markdown-body .md-pre code { + background: transparent; + border: none; + border-radius: 0; + padding: 0; + font-size: 11px; + line-height: 1.5; + display: block; + } + .message-error { background: rgba(255, 59, 48, 0.1); border: 1px solid rgba(255, 59, 48, 0.3); diff --git a/frontend/packages/inspector/src/components/chat/ChatMessages.tsx b/frontend/packages/inspector/src/components/chat/ChatMessages.tsx index b35943b..69bb76b 100644 --- a/frontend/packages/inspector/src/components/chat/ChatMessages.tsx +++ b/frontend/packages/inspector/src/components/chat/ChatMessages.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { getMessageClass } from "./messageUtils"; import type { TimelineEntry } from "./types"; import { AlertTriangle, ChevronRight, ChevronDown, Wrench, Brain, Info, ExternalLink, PlayCircle } from "lucide-react"; +import MarkdownText from "./MarkdownText"; const ToolItem = ({ entry, @@ -253,7 +254,7 @@ const ChatMessages = ({
{entry.text ? ( -
{entry.text}
+ ) : ( diff --git a/frontend/packages/inspector/src/components/chat/MarkdownText.tsx b/frontend/packages/inspector/src/components/chat/MarkdownText.tsx new file mode 100644 index 0000000..d850fe2 --- /dev/null +++ b/frontend/packages/inspector/src/components/chat/MarkdownText.tsx @@ -0,0 +1,206 @@ +import type { ReactNode } from "react"; + +const SAFE_URL_RE = /^(https?:\/\/|mailto:)/i; + +const isSafeUrl = (url: string): boolean => SAFE_URL_RE.test(url.trim()); + +const inlineTokenRe = /(`[^`\n]+`|\[[^\]\n]+\]\(([^)\s]+)(?:\s+"[^"]*")?\)|\*\*[^*\n]+\*\*|__[^_\n]+__|\*[^*\n]+\*|_[^_\n]+_|~~[^~\n]+~~)/g; + +const parseInline = (text: string, keyPrefix: string): ReactNode[] => { + const out: ReactNode[] = []; + let lastIndex = 0; + let tokenIndex = 0; + + for (const match of text.matchAll(inlineTokenRe)) { + const token = match[0]; + const idx = match.index ?? 0; + + if (idx > lastIndex) { + out.push(text.slice(lastIndex, idx)); + } + + const key = `${keyPrefix}-t-${tokenIndex++}`; + + if (token.startsWith("`") && token.endsWith("`")) { + out.push({token.slice(1, -1)}); + } else if (token.startsWith("**") && token.endsWith("**")) { + out.push({token.slice(2, -2)}); + } else if (token.startsWith("__") && token.endsWith("__")) { + out.push({token.slice(2, -2)}); + } else if (token.startsWith("*") && token.endsWith("*")) { + out.push({token.slice(1, -1)}); + } else if (token.startsWith("_") && token.endsWith("_")) { + out.push({token.slice(1, -1)}); + } else if (token.startsWith("~~") && token.endsWith("~~")) { + out.push({token.slice(2, -2)}); + } else if (token.startsWith("[") && token.includes("](") && token.endsWith(")")) { + const linkMatch = token.match(/^\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/); + if (!linkMatch) { + out.push(token); + } else { + const label = linkMatch[1]; + const href = linkMatch[2]; + if (isSafeUrl(href)) { + out.push( + + {label} + , + ); + } else { + out.push(label); + } + } + } else { + out.push(token); + } + + lastIndex = idx + token.length; + } + + if (lastIndex < text.length) { + out.push(text.slice(lastIndex)); + } + + return out; +}; + +const renderInlineLines = (text: string, keyPrefix: string): ReactNode[] => { + const lines = text.split("\n"); + const out: ReactNode[] = []; + lines.forEach((line, idx) => { + if (idx > 0) out.push(
); + out.push(...parseInline(line, `${keyPrefix}-l-${idx}`)); + }); + return out; +}; + +const isUnorderedListItem = (line: string): boolean => /^\s*[-*+]\s+/.test(line); +const isOrderedListItem = (line: string): boolean => /^\s*\d+\.\s+/.test(line); + +const MarkdownText = ({ text }: { text: string }) => { + const source = text.replace(/\r\n?/g, "\n"); + const lines = source.split("\n"); + const nodes: ReactNode[] = []; + + let i = 0; + while (i < lines.length) { + const line = lines[i]; + const trimmed = line.trim(); + + if (!trimmed) { + i += 1; + continue; + } + + if (trimmed.startsWith("```")) { + const lang = trimmed.slice(3).trim(); + const codeLines: string[] = []; + i += 1; + while (i < lines.length && !lines[i].trim().startsWith("```")) { + codeLines.push(lines[i]); + i += 1; + } + if (i < lines.length && lines[i].trim().startsWith("```")) i += 1; + nodes.push( +
+          {codeLines.join("\n")}
+        
, + ); + continue; + } + + const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/); + if (headingMatch) { + const level = headingMatch[1].length; + const content = headingMatch[2]; + const key = `h-${nodes.length}`; + if (level === 1) nodes.push(

{renderInlineLines(content, key)}

); + else if (level === 2) nodes.push(

{renderInlineLines(content, key)}

); + else if (level === 3) nodes.push(

{renderInlineLines(content, key)}

); + else if (level === 4) nodes.push(

{renderInlineLines(content, key)}

); + else if (level === 5) nodes.push(
{renderInlineLines(content, key)}
); + else nodes.push(
{renderInlineLines(content, key)}
); + i += 1; + continue; + } + + if (trimmed.startsWith(">")) { + const quoteLines: string[] = []; + while (i < lines.length && lines[i].trim().startsWith(">")) { + quoteLines.push(lines[i].trim().replace(/^>\s?/, "")); + i += 1; + } + const content = quoteLines.join("\n"); + const key = `q-${nodes.length}`; + nodes.push(
{renderInlineLines(content, key)}
); + continue; + } + + if (isUnorderedListItem(line) || isOrderedListItem(line)) { + const ordered = isOrderedListItem(line); + const items: string[] = []; + while (i < lines.length) { + const candidate = lines[i]; + if (ordered && isOrderedListItem(candidate)) { + items.push(candidate.replace(/^\s*\d+\.\s+/, "")); + i += 1; + continue; + } + if (!ordered && isUnorderedListItem(candidate)) { + items.push(candidate.replace(/^\s*[-*+]\s+/, "")); + i += 1; + continue; + } + if (!candidate.trim()) { + i += 1; + break; + } + break; + } + const key = `list-${nodes.length}`; + if (ordered) { + nodes.push( +
    + {items.map((item, idx) => ( +
  1. {renderInlineLines(item, `${key}-i-${idx}`)}
  2. + ))} +
, + ); + } else { + nodes.push( +
    + {items.map((item, idx) => ( +
  • {renderInlineLines(item, `${key}-i-${idx}`)}
  • + ))} +
, + ); + } + continue; + } + + const paragraphLines: string[] = []; + while (i < lines.length) { + const current = lines[i]; + const currentTrimmed = current.trim(); + if (!currentTrimmed) break; + if ( + currentTrimmed.startsWith("```") || + currentTrimmed.startsWith(">") || + /^(#{1,6})\s+/.test(currentTrimmed) || + isUnorderedListItem(current) || + isOrderedListItem(current) + ) { + break; + } + paragraphLines.push(current); + i += 1; + } + const content = paragraphLines.join("\n"); + const key = `p-${nodes.length}`; + nodes.push(

{renderInlineLines(content, key)}

); + } + + return
{nodes}
; +}; + +export default MarkdownText; diff --git a/frontend/packages/website/public/images/inspector.png b/frontend/packages/website/public/images/inspector.png index 1c16ed2..7966848 100644 Binary files a/frontend/packages/website/public/images/inspector.png and b/frontend/packages/website/public/images/inspector.png differ diff --git a/frontend/packages/website/src/components/Hero.tsx b/frontend/packages/website/src/components/Hero.tsx index 9f12fd0..e453381 100644 --- a/frontend/packages/website/src/components/Hero.tsx +++ b/frontend/packages/website/src/components/Hero.tsx @@ -154,6 +154,7 @@ function UniversalAPIDiagram() { const CopyInstallButton = () => { const [copied, setCopied] = useState(false); const installCommand = 'npx skills add rivet-dev/skills -s sandbox-agent'; + const shortCommand = 'npx skills add rivet-dev/skills'; const handleCopy = async () => { try { @@ -171,8 +172,9 @@ const CopyInstallButton = () => { onClick={handleCopy} className="w-full sm:w-auto inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-white/10 px-4 py-2 text-sm text-zinc-300 transition-colors hover:border-white/20 hover:text-white font-mono" > - {copied ? : } - {installCommand} + {copied ? : } + {installCommand} + {shortCommand}
Give this to your coding agent @@ -183,32 +185,49 @@ const CopyInstallButton = () => { export function Hero() { const [scrollOpacity, setScrollOpacity] = useState(1); + const [isMobile, setIsMobile] = useState(false); useEffect(() => { + const updateViewportMode = () => { + const mobile = window.innerWidth < 1024; + setIsMobile(mobile); + if (mobile) { + setScrollOpacity(1); + } + }; + const handleScroll = () => { + if (window.innerWidth < 1024) { + setScrollOpacity(1); + return; + } const scrollY = window.scrollY; const windowHeight = window.innerHeight; - const isMobile = window.innerWidth < 1024; - - const fadeStart = windowHeight * (isMobile ? 0.3 : 0.15); - const fadeEnd = windowHeight * (isMobile ? 0.7 : 0.5); + const fadeStart = windowHeight * 0.15; + const fadeEnd = windowHeight * 0.5; const opacity = 1 - Math.min(1, Math.max(0, (scrollY - fadeStart) / (fadeEnd - fadeStart))); setScrollOpacity(opacity); }; + updateViewportMode(); + handleScroll(); + window.addEventListener('resize', updateViewportMode); window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('resize', updateViewportMode); + window.removeEventListener('scroll', handleScroll); + }; }, []); return ( -
+
{/* Background gradient */}
{/* Main content */}