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) => (
+ - {renderInlineLines(item, `${key}-i-${idx}`)}
+ ))}
+
,
+ );
+ } 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 */}