Merge branch 'main' into dev

Resolve 38 conflicts preserving both sides' work:
- Server: DB-based registries (dev) + WorkspaceGitService/DaemonConfigStore (main)
- App: script proxy/voice MCP (dev) + pin/hide tabs/bootstrap boundary (main)
- Shared: WorkspaceScriptPayload (dev) + git/GitHub runtime schemas (main)
- Deleted workspace-registry-bootstrap.ts (dev's DB approach supersedes)
This commit is contained in:
Mohamed Boudra
2026-04-14 03:05:45 +07:00
333 changed files with 13490 additions and 6317 deletions
@@ -59,14 +59,10 @@ export function CommandDialog({
>
<div className="space-y-2">
<p className="text-base font-medium text-white">{title}</p>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</div>
<CodeBlock>{command}</CodeBlock>
{footnote && (
<p className="text-xs text-white/30">{footnote}</p>
)}
{footnote && <p className="text-xs text-white/30">{footnote}</p>}
</motion.div>
</>
)}
+342 -109
View File
@@ -58,7 +58,12 @@ const CHAT: ChatItem[] = [
{ type: "text", text: "I'll break this down into planning and implementation.", bold: true },
{ type: "tool", label: "Run plan-technical", summary: "codex · plan-technical", status: "done" },
{ type: "tool", label: "Run plan-design", summary: "claude · plan-design", status: "done" },
{ type: "tool", label: "Wait for agents", summary: "plan-technical plan-design", status: "done" },
{
type: "tool",
label: "Wait for agents",
summary: "plan-technical plan-design",
status: "done",
},
{ type: "text", text: "Got the plans. Spinning up Codex for implementation.", bold: true },
{ type: "tool", label: "Run implement", summary: "codex · 12 files changed", status: "done" },
{ type: "text", text: "Implementation done. Requesting review from Claude." },
@@ -88,8 +93,19 @@ const SIDEBAR_PROJECTS: SidebarProject[] = [
initial: "A",
name: "acme/returns-app",
workspaces: [
{ name: "main", kind: "checkout", status: "syncing", selected: true, diffStat: { additions: 247, deletions: 15 } },
{ name: "feat/dashboard", kind: "worktree", status: "done", pr: { number: 142, state: "open" } },
{
name: "main",
kind: "checkout",
status: "syncing",
selected: true,
diffStat: { additions: 247, deletions: 15 },
},
{
name: "feat/dashboard",
kind: "worktree",
status: "done",
pr: { number: 142, state: "open" },
},
],
},
{
@@ -97,7 +113,13 @@ const SIDEBAR_PROJECTS: SidebarProject[] = [
name: "acme/payments",
workspaces: [
{ name: "main", kind: "checkout", status: "idle" },
{ name: "fix/stripe-webhook", kind: "worktree", status: "done", diffStat: { additions: 38, deletions: 4 }, pr: { number: 89, state: "merged" } },
{
name: "fix/stripe-webhook",
kind: "worktree",
status: "done",
diffStat: { additions: 38, deletions: 4 },
pr: { number: 89, state: "merged" },
},
],
},
{
@@ -105,15 +127,18 @@ const SIDEBAR_PROJECTS: SidebarProject[] = [
name: "acme/infra",
workspaces: [
{ name: "main", kind: "checkout", status: "idle" },
{ name: "feat/k8s-autoscale", kind: "worktree", status: "syncing", diffStat: { additions: 91, deletions: 3 } },
{
name: "feat/k8s-autoscale",
kind: "worktree",
status: "syncing",
diffStat: { additions: 91, deletions: 3 },
},
],
},
{
initial: "D",
name: "acme/design-system",
workspaces: [
{ name: "main", kind: "checkout", status: "idle" },
],
workspaces: [{ name: "main", kind: "checkout", status: "idle" }],
},
];
@@ -143,20 +168,145 @@ type DiffLine = {
};
const DIFF_LINES: DiffLine[] = [
{ type: "add", ln: "1", tokens: [{ text: "import", cls: "text-syn-keyword" }, { text: " { ", cls: "text-syn-punctuation" }, { text: "useState", cls: "text-syn-variable" }, { text: " } ", cls: "text-syn-punctuation" }, { text: "from", cls: "text-syn-keyword" }, { text: ' "react"', cls: "text-syn-string" }] },
{ type: "add", ln: "2", tokens: [{ text: "import", cls: "text-syn-keyword" }, { text: " { ", cls: "text-syn-punctuation" }, { text: "ReturnTable", cls: "text-syn-variable" }, { text: " } ", cls: "text-syn-punctuation" }, { text: "from", cls: "text-syn-keyword" }, { text: ' "./components"', cls: "text-syn-string" }] },
{
type: "add",
ln: "1",
tokens: [
{ text: "import", cls: "text-syn-keyword" },
{ text: " { ", cls: "text-syn-punctuation" },
{ text: "useState", cls: "text-syn-variable" },
{ text: " } ", cls: "text-syn-punctuation" },
{ text: "from", cls: "text-syn-keyword" },
{ text: ' "react"', cls: "text-syn-string" },
],
},
{
type: "add",
ln: "2",
tokens: [
{ text: "import", cls: "text-syn-keyword" },
{ text: " { ", cls: "text-syn-punctuation" },
{ text: "ReturnTable", cls: "text-syn-variable" },
{ text: " } ", cls: "text-syn-punctuation" },
{ text: "from", cls: "text-syn-keyword" },
{ text: ' "./components"', cls: "text-syn-string" },
],
},
{ type: "add", ln: "3", tokens: [] },
{ type: "add", ln: "4", tokens: [{ text: "export", cls: "text-syn-keyword" }, { text: " function", cls: "text-syn-keyword" }, { text: " Dashboard", cls: "text-syn-function" }, { text: "() {", cls: "text-syn-punctuation" }] },
{ type: "add", ln: "5", tokens: [{ text: " const", cls: "text-syn-keyword" }, { text: " [returns, setReturns]", cls: "text-syn-variable" }, { text: " = ", cls: "text-syn-operator" }, { text: "useState", cls: "text-syn-function" }, { text: "([])", cls: "text-syn-punctuation" }] },
{ type: "add", ln: "6", tokens: [{ text: " const", cls: "text-syn-keyword" }, { text: " [filter, setFilter]", cls: "text-syn-variable" }, { text: " = ", cls: "text-syn-operator" }, { text: "useState", cls: "text-syn-function" }, { text: '("all")', cls: "text-syn-string" }] },
{
type: "add",
ln: "4",
tokens: [
{ text: "export", cls: "text-syn-keyword" },
{ text: " function", cls: "text-syn-keyword" },
{ text: " Dashboard", cls: "text-syn-function" },
{ text: "() {", cls: "text-syn-punctuation" },
],
},
{
type: "add",
ln: "5",
tokens: [
{ text: " const", cls: "text-syn-keyword" },
{ text: " [returns, setReturns]", cls: "text-syn-variable" },
{ text: " = ", cls: "text-syn-operator" },
{ text: "useState", cls: "text-syn-function" },
{ text: "([])", cls: "text-syn-punctuation" },
],
},
{
type: "add",
ln: "6",
tokens: [
{ text: " const", cls: "text-syn-keyword" },
{ text: " [filter, setFilter]", cls: "text-syn-variable" },
{ text: " = ", cls: "text-syn-operator" },
{ text: "useState", cls: "text-syn-function" },
{ text: '("all")', cls: "text-syn-string" },
],
},
{ type: "add", ln: "7", tokens: [] },
{ type: "add", ln: "8", tokens: [{ text: " ", cls: "text-syn-punctuation" }, { text: "return", cls: "text-syn-keyword" }, { text: " (", cls: "text-syn-punctuation" }] },
{ type: "add", ln: "9", tokens: [{ text: " <", cls: "text-syn-punctuation" }, { text: "main", cls: "text-syn-tag" }, { text: " className", cls: "text-syn-property" }, { text: '="', cls: "text-syn-punctuation" }, { text: "min-h-screen p-8", cls: "text-syn-string" }, { text: '">', cls: "text-syn-punctuation" }] },
{ type: "add", ln: "10", tokens: [{ text: " <", cls: "text-syn-punctuation" }, { text: "h1", cls: "text-syn-tag" }, { text: ">Customer Returns</", cls: "text-syn-variable" }, { text: "h1", cls: "text-syn-tag" }, { text: ">", cls: "text-syn-punctuation" }] },
{ type: "add", ln: "11", tokens: [{ text: " <", cls: "text-syn-punctuation" }, { text: "FilterBar", cls: "text-syn-tag" }, { text: " value", cls: "text-syn-property" }, { text: "={", cls: "text-syn-punctuation" }, { text: "filter", cls: "text-syn-variable" }, { text: "}", cls: "text-syn-punctuation" }, { text: " onChange", cls: "text-syn-property" }, { text: "={", cls: "text-syn-punctuation" }, { text: "setFilter", cls: "text-syn-variable" }, { text: "} />", cls: "text-syn-punctuation" }] },
{ type: "add", ln: "12", tokens: [{ text: " <", cls: "text-syn-punctuation" }, { text: "ReturnTable", cls: "text-syn-tag" }, { text: " data", cls: "text-syn-property" }, { text: "={", cls: "text-syn-punctuation" }, { text: "returns", cls: "text-syn-variable" }, { text: "} />", cls: "text-syn-punctuation" }] },
{ type: "add", ln: "13", tokens: [{ text: " <", cls: "text-syn-punctuation" }, { text: "StatusChart", cls: "text-syn-tag" }, { text: " data", cls: "text-syn-property" }, { text: "={", cls: "text-syn-punctuation" }, { text: "returns", cls: "text-syn-variable" }, { text: "} />", cls: "text-syn-punctuation" }] },
{ type: "add", ln: "14", tokens: [{ text: " </", cls: "text-syn-punctuation" }, { text: "main", cls: "text-syn-tag" }, { text: ">", cls: "text-syn-punctuation" }] },
{
type: "add",
ln: "8",
tokens: [
{ text: " ", cls: "text-syn-punctuation" },
{ text: "return", cls: "text-syn-keyword" },
{ text: " (", cls: "text-syn-punctuation" },
],
},
{
type: "add",
ln: "9",
tokens: [
{ text: " <", cls: "text-syn-punctuation" },
{ text: "main", cls: "text-syn-tag" },
{ text: " className", cls: "text-syn-property" },
{ text: '="', cls: "text-syn-punctuation" },
{ text: "min-h-screen p-8", cls: "text-syn-string" },
{ text: '">', cls: "text-syn-punctuation" },
],
},
{
type: "add",
ln: "10",
tokens: [
{ text: " <", cls: "text-syn-punctuation" },
{ text: "h1", cls: "text-syn-tag" },
{ text: ">Customer Returns</", cls: "text-syn-variable" },
{ text: "h1", cls: "text-syn-tag" },
{ text: ">", cls: "text-syn-punctuation" },
],
},
{
type: "add",
ln: "11",
tokens: [
{ text: " <", cls: "text-syn-punctuation" },
{ text: "FilterBar", cls: "text-syn-tag" },
{ text: " value", cls: "text-syn-property" },
{ text: "={", cls: "text-syn-punctuation" },
{ text: "filter", cls: "text-syn-variable" },
{ text: "}", cls: "text-syn-punctuation" },
{ text: " onChange", cls: "text-syn-property" },
{ text: "={", cls: "text-syn-punctuation" },
{ text: "setFilter", cls: "text-syn-variable" },
{ text: "} />", cls: "text-syn-punctuation" },
],
},
{
type: "add",
ln: "12",
tokens: [
{ text: " <", cls: "text-syn-punctuation" },
{ text: "ReturnTable", cls: "text-syn-tag" },
{ text: " data", cls: "text-syn-property" },
{ text: "={", cls: "text-syn-punctuation" },
{ text: "returns", cls: "text-syn-variable" },
{ text: "} />", cls: "text-syn-punctuation" },
],
},
{
type: "add",
ln: "13",
tokens: [
{ text: " <", cls: "text-syn-punctuation" },
{ text: "StatusChart", cls: "text-syn-tag" },
{ text: " data", cls: "text-syn-property" },
{ text: "={", cls: "text-syn-punctuation" },
{ text: "returns", cls: "text-syn-variable" },
{ text: "} />", cls: "text-syn-punctuation" },
],
},
{
type: "add",
ln: "14",
tokens: [
{ text: " </", cls: "text-syn-punctuation" },
{ text: "main", cls: "text-syn-tag" },
{ text: ">", cls: "text-syn-punctuation" },
],
},
{ type: "add", ln: "15", tokens: [{ text: " )", cls: "text-syn-punctuation" }] },
{ type: "add", ln: "16", tokens: [{ text: "}", cls: "text-syn-punctuation" }] },
];
@@ -207,15 +357,27 @@ function TrafficLights() {
);
}
function ProviderIcon({ provider, muted = false }: { provider: "claude" | "codex" | "terminal"; muted?: boolean }) {
function ProviderIcon({
provider,
muted = false,
}: {
provider: "claude" | "codex" | "terminal";
muted?: boolean;
}) {
const cls = muted ? "text-mock-fg-muted" : "text-mock-fg";
if (provider === "terminal") return <SquareTerminal size={13} className={cls} />;
return provider === "claude"
? <ClaudeIcon size={13} className={cls} />
: <CodexIcon size={13} className={cls} />;
return provider === "claude" ? (
<ClaudeIcon size={13} className={cls} />
) : (
<CodexIcon size={13} className={cls} />
);
}
function TabBarAction({ icon: Icon }: { icon: React.ComponentType<{ size?: number; className?: string; strokeWidth?: number }> }) {
function TabBarAction({
icon: Icon,
}: {
icon: React.ComponentType<{ size?: number; className?: string; strokeWidth?: number }>;
}) {
return (
<div className="w-4 h-5 flex items-center justify-center flex-shrink-0">
<Icon size={12} strokeWidth={1.5} className="text-mock-fg-muted" />
@@ -227,16 +389,23 @@ function PaneTabBar({ tabs, focused = false }: { tabs: TabDef[]; focused?: boole
return (
<div className="flex items-stretch h-7 bg-mock-surface0 border-b border-mock-border flex-shrink-0">
{tabs.map((tab) => (
<div key={tab.name} className="flex items-center gap-1.5 px-2 border-r border-mock-border relative min-w-0">
<div
key={tab.name}
className="flex items-center gap-1.5 px-2 border-r border-mock-border relative min-w-0"
>
{tab.active && (
<div className={`absolute top-0 left-0 right-0 h-0.5 ${focused ? "bg-mock-accent" : "bg-mock-border-accent"}`} />
<div
className={`absolute top-0 left-0 right-0 h-0.5 ${focused ? "bg-mock-accent" : "bg-mock-border-accent"}`}
/>
)}
<div className="relative flex-shrink-0">
<ProviderIcon provider={tab.provider} muted={!tab.active} />
</div>
<span className={`text-[11px] truncate ${tab.active ? "text-mock-fg" : "text-mock-fg-muted"}`}>
<span
className={`text-[11px] truncate ${tab.active ? "text-mock-fg" : "text-mock-fg-muted"}`}
>
{tab.name}
</span>
@@ -253,7 +422,15 @@ function PaneTabBar({ tabs, focused = false }: { tabs: TabDef[]; focused?: boole
);
}
function Composer({ provider, model, focused = false }: { provider: "claude" | "codex"; model: string; focused?: boolean }) {
function Composer({
provider,
model,
focused = false,
}: {
provider: "claude" | "codex";
model: string;
focused?: boolean;
}) {
const Icon = provider === "claude" ? ClaudeIcon : CodexIcon;
return (
<div className="px-3 pb-3 flex-shrink-0">
@@ -417,7 +594,9 @@ function ExplorerSidebar() {
<span
className="text-[10px] text-mock-green-400 px-2 py-[2px] rounded-md flex-shrink-0"
style={{ backgroundColor: "rgba(46, 160, 67, 0.2)" }}
>New</span>
>
New
</span>
</div>
<div className="flex items-center gap-1 flex-shrink-0 ml-1">
<span className="text-[11px] text-mock-green-400">+16</span>
@@ -430,17 +609,29 @@ function ExplorerSidebar() {
{DIFF_LINES.map((line, i) => {
const isAdd = line.type === "add";
const isRemove = line.type === "remove";
const lineBg = isAdd ? "bg-mock-diff-add" : isRemove ? "bg-mock-diff-remove" : "bg-mock-surface1";
const lineNumCls = isAdd ? "text-mock-green-400" : isRemove ? "text-mock-red" : "text-mock-fg-muted";
const lineBg = isAdd
? "bg-mock-diff-add"
: isRemove
? "bg-mock-diff-remove"
: "bg-mock-surface1";
const lineNumCls = isAdd
? "text-mock-green-400"
: isRemove
? "text-mock-red"
: "text-mock-fg-muted";
return (
<div key={i} className={`flex items-stretch ${lineBg}`}>
<div className="w-8 border-r border-mock-border flex-shrink-0 flex items-center justify-end">
<code className={`text-[10px] font-mono ${lineNumCls} select-none pr-2 py-[1px]`}>{line.ln ?? ""}</code>
<code className={`text-[10px] font-mono ${lineNumCls} select-none pr-2 py-[1px]`}>
{line.ln ?? ""}
</code>
</div>
<code className="text-[10px] font-mono text-mock-fg pl-3 pr-3 py-[1px] whitespace-pre flex-1 min-w-0">
{line.tokens?.map((tok, j) => (
<span key={j} className={tok.cls}>{tok.text}</span>
<span key={j} className={tok.cls}>
{tok.text}
</span>
))}
</code>
</div>
@@ -456,7 +647,10 @@ function ExplorerSidebar() {
{ name: "returns.ts", dir: "src/api", added: 12, removed: 5 },
{ name: "index.tsx", dir: "src/pages", added: 6, removed: 2 },
].map((file) => (
<div key={file.name} className="flex items-center justify-between pl-2 pr-2 py-1.5 border-b border-mock-border">
<div
key={file.name}
className="flex items-center justify-between pl-2 pr-2 py-1.5 border-b border-mock-border"
>
<div className="flex items-center gap-1 flex-1 min-w-0 overflow-hidden">
<ChevronRight size={10} className="text-mock-fg-muted flex-shrink-0" />
<span className="text-[11px] text-mock-fg flex-shrink-0">{file.name}</span>
@@ -465,7 +659,9 @@ function ExplorerSidebar() {
<span
className="text-[10px] text-mock-green-400 px-2 py-[2px] rounded-md flex-shrink-0"
style={{ backgroundColor: "rgba(46, 160, 67, 0.2)" }}
>New</span>
>
New
</span>
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0 ml-1">
@@ -494,7 +690,16 @@ function SyncedLoader({ size = 11 }: { size?: number }) {
const gridH = dotSize * 3 + gap * 2;
return (
<div style={{ width: size, height: size, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
<div
style={{
width: size,
height: size,
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<div style={{ position: "relative", width: gridW, height: gridH }}>
{Array.from({ length: DOT_COUNT }).map((_, dotIndex) => {
const col = dotIndex % 2;
@@ -560,7 +765,9 @@ function Sidebar() {
{/* Project row — icon aligns with traffic lights / sessions */}
<div className="flex items-center gap-2 min-h-[32px] py-1.5 pl-3 pr-2">
<div className="w-4 h-4 rounded-sm border border-mock-border flex items-center justify-center flex-shrink-0">
<span className="text-[9px] text-mock-fg-muted leading-none">{project.initial}</span>
<span className="text-[9px] text-mock-fg-muted leading-none">
{project.initial}
</span>
</div>
<span className="text-[13px] text-mock-fg font-normal truncate flex-1 min-w-0 leading-5">
{project.name}
@@ -569,17 +776,21 @@ function Sidebar() {
{/* Workspace rows — indented one level */}
{project.workspaces.map((workspace) => (
<div key={workspace.name} className={`mb-1 mx-1.5 rounded-lg ${workspace.selected ? "bg-mock-surface1" : ""}`}>
<div
key={workspace.name}
className={`mb-1 mx-1.5 rounded-lg ${workspace.selected ? "bg-mock-surface1" : ""}`}
>
<div className="flex items-center gap-2 min-h-[28px] py-1 pl-[22px] pr-1">
<div className="relative w-[14px] h-4 flex-shrink-0 flex items-center justify-center">
{workspace.status === "syncing" ? (
<SyncedLoader size={11} />
) : (
<>
{workspace.kind === "worktree"
? <FolderGit2 size={14} className="text-mock-fg-muted" />
: <Monitor size={14} className="text-mock-fg-muted" />
}
{workspace.kind === "worktree" ? (
<FolderGit2 size={14} className="text-mock-fg-muted" />
) : (
<Monitor size={14} className="text-mock-fg-muted" />
)}
{workspace.status !== "idle" && (
<div className="absolute bottom-0 right-0">
<WorkspaceStatusDot status={workspace.status} />
@@ -593,8 +804,12 @@ function Sidebar() {
</span>
{workspace.diffStat && (
<div className="flex items-center gap-1 flex-shrink-0">
<span className="text-[10px] text-mock-green-400 font-normal leading-none">+{workspace.diffStat.additions}</span>
<span className="text-[10px] text-mock-red font-normal leading-none">-{workspace.diffStat.deletions}</span>
<span className="text-[10px] text-mock-green-400 font-normal leading-none">
+{workspace.diffStat.additions}
</span>
<span className="text-[10px] text-mock-red font-normal leading-none">
-{workspace.diffStat.deletions}
</span>
</div>
)}
</div>
@@ -602,7 +817,12 @@ function Sidebar() {
<div className="flex items-center gap-1 pl-[42px] pr-2 pb-0.5">
<GitPullRequest size={11} className="text-mock-fg-muted" />
<span className="text-[10px] text-mock-fg-muted leading-none truncate">
#{workspace.pr.number} · {workspace.pr.state === "open" ? "Open" : workspace.pr.state === "merged" ? "Merged" : "Closed"}
#{workspace.pr.number} ·{" "}
{workspace.pr.state === "open"
? "Open"
: workspace.pr.state === "merged"
? "Merged"
: "Closed"}
</span>
</div>
)}
@@ -650,81 +870,94 @@ function DesktopMockup() {
}, []);
return (
<div ref={containerRef} className="w-full overflow-hidden" style={{ height: `${1200 * (9 / 16) * scale}px` }}>
<div
ref={containerRef}
className="w-full overflow-hidden"
style={{ height: `${1200 * (9 / 16) * scale}px` }}
>
<div
className="mx-auto rounded-xl overflow-hidden border border-mock-border bg-mock-surface0 shadow-[6px_6px_0_rgba(0,0,0,0.4)] origin-top-left"
style={{ width: 1200, transform: `scale(${scale})` }}
>
{/* Top-level: left sidebar | center column | explorer sidebar — all full height */}
<div className="flex aspect-video">
{/* Left sidebar — full height */}
<motion.div {...fade(D.sidebar)} className="contents">
<Sidebar />
</motion.div>
{/* Center column: title bar + split panes */}
<div className="flex flex-col flex-1 min-w-0 min-h-0">
{/* Title bar — belongs to center column only */}
<motion.div {...fade(D.titleBar)} className="flex items-center h-10 px-2 bg-mock-surface0 border-b border-mock-border flex-shrink-0">
<div className="flex items-center gap-2 flex-1 min-w-0">
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
<PanelLeft size={14} className="text-mock-fg-muted" />
</div>
<div className="flex items-center gap-2 min-w-0 flex-1">
<span className="text-[13px] font-light text-mock-fg truncate flex-shrink-0">main</span>
<span className="text-[13px] text-mock-fg-muted truncate flex-shrink min-w-0">acme/returns-app</span>
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
<Ellipsis size={14} className="text-mock-fg-muted" />
</div>
</div>
</div>
<div className="flex items-center gap-2 ml-auto flex-shrink-0">
<div className="flex items-stretch rounded-md border border-mock-border-accent overflow-hidden">
<div className="flex items-center gap-2 px-3 py-1">
<GitCommitHorizontal size={12} className="text-mock-fg-muted flex-shrink-0" />
<span className="text-[11px] text-mock-fg font-normal">Commit</span>
</div>
<div className="flex items-center justify-center w-7 border-l border-mock-border-accent">
<ChevronDown size={12} className="text-mock-fg-muted" />
</div>
</div>
<div className="flex items-center gap-2 px-3 py-1 rounded-md">
<SourceControlIcon size={14} className="text-mock-fg-muted" />
<span className="text-[11px] font-normal text-mock-green-400">+247</span>
<span className="text-[11px] font-normal text-mock-red">-15</span>
</div>
</div>
{/* Left sidebar — full height */}
<motion.div {...fade(D.sidebar)} className="contents">
<Sidebar />
</motion.div>
{/* Split panes */}
<div className="flex flex-1 min-h-0">
{/* Left pane: all agent tabs + chat */}
<div className="flex flex-col flex-1 min-w-0 min-h-0">
<motion.div {...fade(D.tabs)}>
<PaneTabBar tabs={TABS} focused />
</motion.div>
<motion.div {...fade(D.chat)} className="flex-1 flex min-h-0">
<ChatArea />
{/* Center column: title bar + split panes */}
<div className="flex flex-col flex-1 min-w-0 min-h-0">
{/* Title bar — belongs to center column only */}
<motion.div
{...fade(D.titleBar)}
className="flex items-center h-10 px-2 bg-mock-surface0 border-b border-mock-border flex-shrink-0"
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
<PanelLeft size={14} className="text-mock-fg-muted" />
</div>
<div className="flex items-center gap-2 min-w-0 flex-1">
<span className="text-[13px] font-light text-mock-fg truncate flex-shrink-0">
main
</span>
<span className="text-[13px] text-mock-fg-muted truncate flex-shrink min-w-0">
acme/returns-app
</span>
<div className="px-2 py-1 rounded-lg flex items-center justify-center flex-shrink-0">
<Ellipsis size={14} className="text-mock-fg-muted" />
</div>
</div>
</div>
<div className="flex items-center gap-2 ml-auto flex-shrink-0">
<div className="flex items-stretch rounded-md border border-mock-border-accent overflow-hidden">
<div className="flex items-center gap-2 px-3 py-1">
<GitCommitHorizontal size={12} className="text-mock-fg-muted flex-shrink-0" />
<span className="text-[11px] text-mock-fg font-normal">Commit</span>
</div>
<div className="flex items-center justify-center w-7 border-l border-mock-border-accent">
<ChevronDown size={12} className="text-mock-fg-muted" />
</div>
</div>
<div className="flex items-center gap-2 px-3 py-1 rounded-md">
<SourceControlIcon size={14} className="text-mock-fg-muted" />
<span className="text-[11px] font-normal text-mock-green-400">+247</span>
<span className="text-[11px] font-normal text-mock-red">-15</span>
</div>
</div>
</motion.div>
{/* Split panes */}
<div className="flex flex-1 min-h-0">
{/* Left pane: all agent tabs + chat */}
<div className="flex flex-col flex-1 min-w-0 min-h-0">
<motion.div {...fade(D.tabs)}>
<PaneTabBar tabs={TABS} focused />
</motion.div>
<motion.div {...fade(D.chat)} className="flex-1 flex min-h-0">
<ChatArea />
</motion.div>
</div>
{/* Resize handle */}
<div className="w-px bg-mock-border flex-shrink-0" />
{/* Right pane: terminal only */}
<motion.div {...fade(D.chat)} className="flex flex-col flex-1 min-w-0 min-h-0">
<PaneTabBar
tabs={[{ name: "npm run dev", provider: "terminal", done: false, active: true }]}
/>
<TerminalPane />
</motion.div>
</div>
{/* Resize handle */}
<div className="w-px bg-mock-border flex-shrink-0" />
{/* Right pane: terminal only */}
<motion.div {...fade(D.chat)} className="flex flex-col flex-1 min-w-0 min-h-0">
<PaneTabBar tabs={[{ name: "npm run dev", provider: "terminal", done: false, active: true }]} />
<TerminalPane />
</motion.div>
</div>
</div>
{/* Explorer sidebar — full height, pushes center column */}
<motion.div {...fade(D.diffPanel)} className="contents">
<ExplorerSidebar />
</motion.div>
</div>
{/* Explorer sidebar — full height, pushes center column */}
<motion.div {...fade(D.diffPanel)} className="contents">
<ExplorerSidebar />
</motion.div>
</div>
</div>
</div>
);
+279 -114
View File
@@ -1,5 +1,12 @@
import * as React from "react";
import { motion, AnimatePresence, useInView, useScroll, useTransform, useMotionValueEvent } from "framer-motion";
import {
motion,
AnimatePresence,
useInView,
useScroll,
useTransform,
useMotionValueEvent,
} from "framer-motion";
import { CursorFieldProvider } from "~/components/butterfly";
import { CommandDialog } from "~/components/command-dialog";
import {
@@ -30,7 +37,6 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
<CursorFieldProvider>
{/* Hero section with background image */}
<div className="relative bg-cover bg-center bg-no-repeat">
<div className="relative p-6 pb-10 md:px-32 md:pt-20 md:pb-12 max-w-7xl mx-auto">
<Nav />
<Hero title={title} subtitle={subtitle} />
@@ -48,7 +54,6 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
<HeroMockup />
</div>
</motion.div>
</div>
{/* Phone showcase */}
@@ -335,17 +340,88 @@ function MultiProviderSection() {
function SelfHostedDiagram() {
const clients = [
{ name: "Desktop", icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8M12 17v4" /></svg> },
{ name: "Web", icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg> },
{ name: "Mobile", icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" /><path d="M12 18h.01" /></svg> },
{ name: "CLI", icon: <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 17 10 11 4 5" /><line x1="12" y1="19" x2="20" y2="19" /></svg> },
{
name: "Desktop",
icon: (
<svg
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="2" y="3" width="20" height="14" rx="2" />
<path d="M8 21h8M12 17v4" />
</svg>
),
},
{
name: "Web",
icon: (
<svg
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</svg>
),
},
{
name: "Mobile",
icon: (
<svg
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="5" y="2" width="14" height="20" rx="2" />
<path d="M12 18h.01" />
</svg>
),
},
{
name: "CLI",
icon: (
<svg
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="4 17 10 11 4 5" />
<line x1="12" y1="19" x2="20" y2="19" />
</svg>
),
},
];
const hosts = ["MacBook Pro", "Hetzner VM", "Dev server"];
const containerRef = React.useRef<HTMLDivElement>(null);
const clientRefs = React.useRef<(HTMLDivElement | null)[]>([]);
const hostRefs = React.useRef<(HTMLDivElement | null)[]>([]);
const centerRef = React.useRef<HTMLDivElement>(null);
const [paths, setPaths] = React.useState<{ left: string[]; right: string[] }>({ left: [], right: [] });
const [paths, setPaths] = React.useState<{ left: string[]; right: string[] }>({
left: [],
right: [],
});
React.useEffect(() => {
function computePaths() {
@@ -387,93 +463,141 @@ function SelfHostedDiagram() {
return (
<>
{/* Mobile: vertical stack */}
<div className="md:hidden flex flex-col items-center gap-4 py-4">
<div className="space-y-2 w-full">
{clients.map((c) => (
<div key={c.name} className="flex items-center justify-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4">
<span className="text-white/80">{c.icon}</span>
<span className="font-medium">{c.name}</span>
</div>
))}
</div>
<div className="w-px h-6 border-l border-dashed border-white/25" />
<div className="rounded-xl border border-white/10 bg-white/[0.03] px-6 py-5 text-center space-y-1">
<p className="text-xs font-medium text-white/50">E2E Encrypted Relay</p>
<p className="text-[10px] text-white/25">or</p>
<p className="text-xs font-medium text-white/50">Direct Connection</p>
</div>
<div className="w-px h-6 border-l border-dashed border-white/25" />
<div className="space-y-2 w-full">
{hosts.map((h) => (
<div key={h} className="flex items-center justify-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4">
<span className="text-white/80"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" />
<rect x="2" y="14" width="20" height="8" rx="2" />
<circle cx="6" cy="6" r="1" />
<circle cx="6" cy="18" r="1" />
</svg></span>
<span className="font-medium">{h}</span>
</div>
))}
</div>
</div>
{/* Desktop: horizontal with bezier curves */}
<div ref={containerRef} className="relative hidden md:flex items-center py-4 gap-0">
{/* SVG curves */}
<svg className="absolute inset-0 w-full h-full pointer-events-none" style={{ overflow: "visible" }}>
{[...paths.left, ...paths.right].map((d, i) => (
d && <path key={i} d={d} fill="none" stroke="rgba(255,255,255,0.25)" strokeWidth="1" strokeDasharray="4 4" />
))}
</svg>
{/* Clients */}
<div className="space-y-3 flex-shrink-0 relative z-10">
{clients.map((c, i) => (
<div
key={c.name}
ref={(el) => { clientRefs.current[i] = el; }}
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm"
>
<span className="text-white/80">{c.icon}</span>
<span className="font-medium">{c.name}</span>
</div>
))}
{/* Mobile: vertical stack */}
<div className="md:hidden flex flex-col items-center gap-4 py-4">
<div className="space-y-2 w-full">
{clients.map((c) => (
<div
key={c.name}
className="flex items-center justify-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4"
>
<span className="text-white/80">{c.icon}</span>
<span className="font-medium">{c.name}</span>
</div>
))}
</div>
<div className="w-px h-6 border-l border-dashed border-white/25" />
<div className="rounded-xl border border-white/10 bg-white/[0.03] px-6 py-5 text-center space-y-1">
<p className="text-xs font-medium text-white/50">E2E Encrypted Relay</p>
<p className="text-[10px] text-white/25">or</p>
<p className="text-xs font-medium text-white/50">Direct Connection</p>
</div>
<div className="w-px h-6 border-l border-dashed border-white/25" />
<div className="space-y-2 w-full">
{hosts.map((h) => (
<div
key={h}
className="flex items-center justify-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4"
>
<span className="text-white/80">
<svg
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="2" y="2" width="20" height="8" rx="2" />
<rect x="2" y="14" width="20" height="8" rx="2" />
<circle cx="6" cy="6" r="1" />
<circle cx="6" cy="18" r="1" />
</svg>
</span>
<span className="font-medium">{h}</span>
</div>
))}
</div>
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Desktop: horizontal with bezier curves */}
<div ref={containerRef} className="relative hidden md:flex items-center py-4 gap-0">
{/* SVG curves */}
<svg
className="absolute inset-0 w-full h-full pointer-events-none"
style={{ overflow: "visible" }}
>
{[...paths.left, ...paths.right].map(
(d, i) =>
d && (
<path
key={i}
d={d}
fill="none"
stroke="rgba(255,255,255,0.25)"
strokeWidth="1"
strokeDasharray="4 4"
/>
),
)}
</svg>
{/* Center label */}
<div ref={centerRef} className="flex-shrink-0 rounded-xl border border-white/10 bg-white/[0.03] px-8 py-6 text-center space-y-1.5 relative z-10 backdrop-blur-sm">
<p className="text-sm font-medium text-white/50">E2E Encrypted Relay</p>
<p className="text-xs text-white/25">or</p>
<p className="text-sm font-medium text-white/50">Direct Connection</p>
{/* Clients */}
<div className="space-y-3 flex-shrink-0 relative z-10">
{clients.map((c, i) => (
<div
key={c.name}
ref={(el) => {
clientRefs.current[i] = el;
}}
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm"
>
<span className="text-white/80">{c.icon}</span>
<span className="font-medium">{c.name}</span>
</div>
))}
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Center label */}
<div
ref={centerRef}
className="flex-shrink-0 rounded-xl border border-white/10 bg-white/[0.03] px-8 py-6 text-center space-y-1.5 relative z-10 backdrop-blur-sm"
>
<p className="text-sm font-medium text-white/50">E2E Encrypted Relay</p>
<p className="text-xs text-white/25">or</p>
<p className="text-sm font-medium text-white/50">Direct Connection</p>
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Hosts */}
<div className="space-y-3 flex-shrink-0 relative z-10">
{hosts.map((h, i) => (
<div
key={h}
ref={(el) => {
hostRefs.current[i] = el;
}}
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm"
>
<span className="text-white/80">
<svg
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="2" y="2" width="20" height="8" rx="2" />
<rect x="2" y="14" width="20" height="8" rx="2" />
<circle cx="6" cy="6" r="1" />
<circle cx="6" cy="18" r="1" />
</svg>
</span>
<span className="font-medium">{h}</span>
</div>
))}
</div>
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Hosts */}
<div className="space-y-3 flex-shrink-0 relative z-10">
{hosts.map((h, i) => (
<div
key={h}
ref={(el) => { hostRefs.current[i] = el; }}
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4 backdrop-blur-sm"
>
<span className="text-white/80"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" />
<rect x="2" y="14" width="20" height="8" rx="2" />
<circle cx="6" cy="6" r="1" />
<circle cx="6" cy="18" r="1" />
</svg></span>
<span className="font-medium">{h}</span>
</div>
))}
</div>
</div>
</>
);
}
@@ -489,7 +613,6 @@ function SelfHostedSection() {
);
}
function ServiceProxySection() {
const workspaces = [
{ name: "fix-auth", url: "fix-auth.my-app.localhost" },
@@ -611,8 +734,12 @@ function VoiceWaveform() {
);
}
const USER_WORDS = "Refactor the auth middleware to use the new session store, then run the test suite".split(" ");
const RESPONSE_WORDS = "I'll update the auth middleware to use SessionStore instead of the legacy cookie-based approach. Let me refactor the middleware and update the tests.".split(" ");
const USER_WORDS =
"Refactor the auth middleware to use the new session store, then run the test suite".split(" ");
const RESPONSE_WORDS =
"I'll update the auth middleware to use SessionStore instead of the legacy cookie-based approach. Let me refactor the middleware and update the tests.".split(
" ",
);
const DICTATION_LAG = 2;
const RESPONSE_LAG = 3;
const WORD_APPEAR_MS = 150;
@@ -620,7 +747,13 @@ const RESPONSE_WORD_MS = 60;
const PHASE_GAP_MS = 800;
const LOOP_PAUSE_MS = 3000;
type VoicePhase = "dictation" | "dictation-flush" | "pause" | "response" | "response-flush" | "done";
type VoicePhase =
| "dictation"
| "dictation-flush"
| "pause"
| "response"
| "response-flush"
| "done";
function useVoiceConversation() {
const [phase, setPhase] = React.useState<VoicePhase>("dictation");
@@ -641,11 +774,16 @@ function useVoiceConversation() {
const t = setTimeout(() => setWordIndex((w) => w + 1), WORD_APPEAR_MS);
return () => clearTimeout(t);
}
const t = setTimeout(() => { setPhase("pause"); }, PHASE_GAP_MS);
const t = setTimeout(() => {
setPhase("pause");
}, PHASE_GAP_MS);
return () => clearTimeout(t);
}
if (phase === "pause") {
const t = setTimeout(() => { setPhase("response"); setWordIndex(0); }, PHASE_GAP_MS);
const t = setTimeout(() => {
setPhase("response");
setWordIndex(0);
}, PHASE_GAP_MS);
return () => clearTimeout(t);
}
if (phase === "response") {
@@ -662,11 +800,16 @@ function useVoiceConversation() {
const t = setTimeout(() => setWordIndex((w) => w + 1), RESPONSE_WORD_MS);
return () => clearTimeout(t);
}
const t = setTimeout(() => { setPhase("done"); }, LOOP_PAUSE_MS);
const t = setTimeout(() => {
setPhase("done");
}, LOOP_PAUSE_MS);
return () => clearTimeout(t);
}
if (phase === "done") {
const t = setTimeout(() => { setPhase("dictation"); setWordIndex(0); }, 0);
const t = setTimeout(() => {
setPhase("dictation");
setWordIndex(0);
}, 0);
return () => clearTimeout(t);
}
}, [phase, wordIndex]);
@@ -697,7 +840,15 @@ function useVoiceConversation() {
return { dictationWordIndex, responseWordIndex, showResponse };
}
function StreamingWords({ words, wordIndex, confirmLag = 2 }: { words: string[]; wordIndex: number; confirmLag?: number }) {
function StreamingWords({
words,
wordIndex,
confirmLag = 2,
}: {
words: string[];
wordIndex: number;
confirmLag?: number;
}) {
return (
<div className="relative">
{/* Invisible full text to reserve height at any viewport width */}
@@ -813,10 +964,7 @@ function GetStarted() {
<ServerInstallButton />
</div>
<div className="pt-3">
<a
href="/download"
className="text-xs text-white/40 hover:text-white/70 transition-colors"
>
<a href="/download" className="text-xs text-white/40 hover:text-white/70 transition-colors">
All download options
</a>
</div>
@@ -866,8 +1014,8 @@ function ServerInstallButton() {
command="npm install -g @getpaseo/cli && paseo"
footnote={
<>
Requires Node.js 18+. Run <span className="font-mono text-white/40">paseo</span> to
start the daemon.
Requires Node.js 18+. Run <span className="font-mono text-white/40">paseo</span> to start
the daemon.
</>
}
/>
@@ -1014,7 +1162,6 @@ function Step({ number, children }: { number: number; children: React.ReactNode
);
}
const bashKeywords = new Set([
"while",
"do",
@@ -1304,11 +1451,22 @@ function PhoneShowcase() {
transition={{ duration: 0.5 }}
className="flex flex-col items-center gap-1.5 px-6"
>
<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" viewBox="0 0 24 24" className="text-white/20">
<svg
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
viewBox="0 0 24 24"
className="text-white/20"
>
<path d="M12 5v14M5 12l7 7 7-7" />
</svg>
<p className="text-lg text-white/80 text-center">
When you want to step away from your desk,<br className="md:hidden" /> you can.
When you want to step away from your desk,
<br className="md:hidden" /> you can.
</p>
<p className="text-sm text-white/50 text-center">
The native mobile app has full feature parity with desktop.
@@ -1316,7 +1474,10 @@ function PhoneShowcase() {
</motion.div>
{/* Phone trio — side phones are absolute, start behind center, slide outward with perspective rotation */}
<div className="relative flex items-center justify-center overflow-x-clip w-full" style={{ minHeight: 480, perspective: 1200 }}>
<div
className="relative flex items-center justify-center overflow-x-clip w-full"
style={{ minHeight: 480, perspective: 1200 }}
>
{/* Left phone — rotated to face inward */}
<motion.div
style={{ opacity: sideOpacity, x: leftX, rotateY: -15, scale: 0.97 }}
@@ -1441,9 +1602,11 @@ function FAQ() {
</FAQItem>
<FAQItem question="Do I need the desktop app?">
No. You can run the daemon headless with{" "}
<code className="font-mono text-muted-foreground">npm install -g @getpaseo/cli && paseo</code> and
use the CLI, web app, or mobile app to connect. The desktop app just bundles the daemon
with a UI.
<code className="font-mono text-muted-foreground">
npm install -g @getpaseo/cli && paseo
</code>{" "}
and use the CLI, web app, or mobile app to connect. The desktop app just bundles the
daemon with a UI.
</FAQItem>
<FAQItem question="How does voice work?">
Voice runs locally on your device by default. You talk, the app transcribes and sends it
@@ -1502,7 +1665,9 @@ function SponsorCTA() {
>
<div className="text-sm text-muted-foreground leading-relaxed space-y-3">
<p>
I built Paseo because I wanted better tools for coding agents on my own setup. It's an independent open source project, built around freedom of choice and real workflows. If you like what I'm building, consider becoming a supporter.
I built Paseo because I wanted better tools for coding agents on my own setup. It's an
independent open source project, built around freedom of choice and real workflows. If you
like what I'm building, consider becoming a supporter.
</p>
<p>- Mo</p>
</div>
@@ -2,7 +2,14 @@
export function ClaudeIcon({ size = 13, className }: { size?: number; className?: string }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" fillRule="evenodd" className={className}>
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
fillRule="evenodd"
className={className}
>
<path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" />
</svg>
);
@@ -10,7 +17,14 @@ export function ClaudeIcon({ size = 13, className }: { size?: number; className?
export function CodexIcon({ size = 13, className }: { size?: number; className?: string }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" fillRule="evenodd" className={className}>
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
fillRule="evenodd"
className={className}
>
<path d="M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z" />
</svg>
);
@@ -18,7 +32,17 @@ export function CodexIcon({ size = 13, className }: { size?: number; className?:
export function SourceControlIcon({ size = 14, className }: { size?: number; className?: string }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className}>
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<rect x={3} y={3} width={18} height={18} rx={2} />
<line x1={9} y1={9.5} x2={15} y2={9.5} />
<line x1={12} y1={6.5} x2={12} y2={12.5} />
@@ -1,6 +1,8 @@
import "~/styles.css";
import { useStars } from "~/routes/__root";
export function SiteHeader() {
const { stars } = useStars();
return (
<header className="flex flex-col items-center gap-4 md:flex-row md:justify-between">
<a href="/" className="flex items-center gap-3">
@@ -55,7 +57,7 @@ export function SiteHeader() {
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-1.5"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -66,6 +68,7 @@ export function SiteHeader() {
>
<path d="M12 0C5.37 0 0 5.484 0 12.252c0 5.418 3.438 10.013 8.205 11.637.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.738-4.042-1.61-4.042-1.61-.546-1.403-1.333-1.776-1.333-1.776-1.089-.756.084-.741.084-.741 1.205.087 1.838 1.262 1.838 1.262 1.07 1.87 2.809 1.33 3.495 1.017.108-.79.417-1.33.76-1.636-2.665-.31-5.467-1.35-5.467-6.005 0-1.327.465-2.413 1.235-3.262-.124-.31-.535-1.556.117-3.243 0 0 1.008-.33 3.3 1.248a11.2 11.2 0 0 1 3.003-.404c1.02.005 2.045.138 3.003.404 2.29-1.578 3.297-1.248 3.297-1.248.653 1.687.242 2.933.118 3.243.77.85 1.233 1.935 1.233 3.262 0 4.667-2.807 5.692-5.48 5.995.43.38.823 1.133.823 2.285 0 1.65-.015 2.98-.015 3.386 0 .315.218.694.825.576C20.565 22.26 24 17.667 24 12.252 24 5.484 18.627 0 12 0z" />
</svg>
{stars && <span className="text-sm">{stars}</span>}
</a>
</div>
</header>