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:
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -28,8 +28,7 @@ function versionFromTag(tag: string): string {
|
||||
return tag.replace(/^v/, "");
|
||||
}
|
||||
|
||||
const GITHUB_RELEASES_URL =
|
||||
"https://api.github.com/repos/getpaseo/paseo/releases?per_page=10";
|
||||
const GITHUB_RELEASES_URL = "https://api.github.com/repos/getpaseo/paseo/releases?per_page=10";
|
||||
|
||||
async function fetchLatestReadyRelease(): Promise<string> {
|
||||
const fallback = websitePackage.version.replace(/-.*$/, "");
|
||||
@@ -52,18 +51,14 @@ async function fetchLatestReadyRelease(): Promise<string> {
|
||||
if (!res.ok) return fallback;
|
||||
|
||||
const releases = (await res.json()) as GitHubRelease[];
|
||||
const ready = releases.find(
|
||||
(r) => !r.prerelease && !r.draft && hasRequiredAssets(r),
|
||||
);
|
||||
const ready = releases.find((r) => !r.prerelease && !r.draft && hasRequiredAssets(r));
|
||||
return ready ? versionFromTag(ready.tag_name) : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export const getLatestRelease = createServerFn({ method: "GET" }).handler(
|
||||
async () => {
|
||||
const version = await fetchLatestReadyRelease();
|
||||
return { version };
|
||||
},
|
||||
);
|
||||
export const getLatestRelease = createServerFn({ method: "GET" }).handler(async () => {
|
||||
const version = await fetchLatestReadyRelease();
|
||||
return { version };
|
||||
});
|
||||
|
||||
@@ -8,440 +8,440 @@
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as PrivacyRouteImport } from './routes/privacy'
|
||||
import { Route as OpencodeRouteImport } from './routes/opencode'
|
||||
import { Route as DownloadRouteImport } from './routes/download'
|
||||
import { Route as DocsRouteImport } from './routes/docs'
|
||||
import { Route as CodexRouteImport } from './routes/codex'
|
||||
import { Route as ClaudeCodeRouteImport } from './routes/claude-code'
|
||||
import { Route as ChangelogRouteImport } from './routes/changelog'
|
||||
import { Route as BlogRouteImport } from './routes/blog'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as DocsIndexRouteImport } from './routes/docs/index'
|
||||
import { Route as BlogIndexRouteImport } from './routes/blog/index'
|
||||
import { Route as DocsWorktreesRouteImport } from './routes/docs/worktrees'
|
||||
import { Route as DocsVoiceRouteImport } from './routes/docs/voice'
|
||||
import { Route as DocsUpdatesRouteImport } from './routes/docs/updates'
|
||||
import { Route as DocsSkillsRouteImport } from './routes/docs/skills'
|
||||
import { Route as DocsSecurityRouteImport } from './routes/docs/security'
|
||||
import { Route as DocsConfigurationRouteImport } from './routes/docs/configuration'
|
||||
import { Route as DocsCliRouteImport } from './routes/docs/cli'
|
||||
import { Route as DocsBestPracticesRouteImport } from './routes/docs/best-practices'
|
||||
import { Route as BlogSplatRouteImport } from './routes/blog/$'
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as PrivacyRouteImport } from "./routes/privacy";
|
||||
import { Route as OpencodeRouteImport } from "./routes/opencode";
|
||||
import { Route as DownloadRouteImport } from "./routes/download";
|
||||
import { Route as DocsRouteImport } from "./routes/docs";
|
||||
import { Route as CodexRouteImport } from "./routes/codex";
|
||||
import { Route as ClaudeCodeRouteImport } from "./routes/claude-code";
|
||||
import { Route as ChangelogRouteImport } from "./routes/changelog";
|
||||
import { Route as BlogRouteImport } from "./routes/blog";
|
||||
import { Route as IndexRouteImport } from "./routes/index";
|
||||
import { Route as DocsIndexRouteImport } from "./routes/docs/index";
|
||||
import { Route as BlogIndexRouteImport } from "./routes/blog/index";
|
||||
import { Route as DocsWorktreesRouteImport } from "./routes/docs/worktrees";
|
||||
import { Route as DocsVoiceRouteImport } from "./routes/docs/voice";
|
||||
import { Route as DocsUpdatesRouteImport } from "./routes/docs/updates";
|
||||
import { Route as DocsSkillsRouteImport } from "./routes/docs/skills";
|
||||
import { Route as DocsSecurityRouteImport } from "./routes/docs/security";
|
||||
import { Route as DocsConfigurationRouteImport } from "./routes/docs/configuration";
|
||||
import { Route as DocsCliRouteImport } from "./routes/docs/cli";
|
||||
import { Route as DocsBestPracticesRouteImport } from "./routes/docs/best-practices";
|
||||
import { Route as BlogSplatRouteImport } from "./routes/blog/$";
|
||||
|
||||
const PrivacyRoute = PrivacyRouteImport.update({
|
||||
id: '/privacy',
|
||||
path: '/privacy',
|
||||
id: "/privacy",
|
||||
path: "/privacy",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const OpencodeRoute = OpencodeRouteImport.update({
|
||||
id: '/opencode',
|
||||
path: '/opencode',
|
||||
id: "/opencode",
|
||||
path: "/opencode",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const DownloadRoute = DownloadRouteImport.update({
|
||||
id: '/download',
|
||||
path: '/download',
|
||||
id: "/download",
|
||||
path: "/download",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const DocsRoute = DocsRouteImport.update({
|
||||
id: '/docs',
|
||||
path: '/docs',
|
||||
id: "/docs",
|
||||
path: "/docs",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const CodexRoute = CodexRouteImport.update({
|
||||
id: '/codex',
|
||||
path: '/codex',
|
||||
id: "/codex",
|
||||
path: "/codex",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const ClaudeCodeRoute = ClaudeCodeRouteImport.update({
|
||||
id: '/claude-code',
|
||||
path: '/claude-code',
|
||||
id: "/claude-code",
|
||||
path: "/claude-code",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const ChangelogRoute = ChangelogRouteImport.update({
|
||||
id: '/changelog',
|
||||
path: '/changelog',
|
||||
id: "/changelog",
|
||||
path: "/changelog",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const BlogRoute = BlogRouteImport.update({
|
||||
id: '/blog',
|
||||
path: '/blog',
|
||||
id: "/blog",
|
||||
path: "/blog",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
id: "/",
|
||||
path: "/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const DocsIndexRoute = DocsIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
id: "/",
|
||||
path: "/",
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any)
|
||||
} as any);
|
||||
const BlogIndexRoute = BlogIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
id: "/",
|
||||
path: "/",
|
||||
getParentRoute: () => BlogRoute,
|
||||
} as any)
|
||||
} as any);
|
||||
const DocsWorktreesRoute = DocsWorktreesRouteImport.update({
|
||||
id: '/worktrees',
|
||||
path: '/worktrees',
|
||||
id: "/worktrees",
|
||||
path: "/worktrees",
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any)
|
||||
} as any);
|
||||
const DocsVoiceRoute = DocsVoiceRouteImport.update({
|
||||
id: '/voice',
|
||||
path: '/voice',
|
||||
id: "/voice",
|
||||
path: "/voice",
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any)
|
||||
} as any);
|
||||
const DocsUpdatesRoute = DocsUpdatesRouteImport.update({
|
||||
id: '/updates',
|
||||
path: '/updates',
|
||||
id: "/updates",
|
||||
path: "/updates",
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any)
|
||||
} as any);
|
||||
const DocsSkillsRoute = DocsSkillsRouteImport.update({
|
||||
id: '/skills',
|
||||
path: '/skills',
|
||||
id: "/skills",
|
||||
path: "/skills",
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any)
|
||||
} as any);
|
||||
const DocsSecurityRoute = DocsSecurityRouteImport.update({
|
||||
id: '/security',
|
||||
path: '/security',
|
||||
id: "/security",
|
||||
path: "/security",
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any)
|
||||
} as any);
|
||||
const DocsConfigurationRoute = DocsConfigurationRouteImport.update({
|
||||
id: '/configuration',
|
||||
path: '/configuration',
|
||||
id: "/configuration",
|
||||
path: "/configuration",
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any)
|
||||
} as any);
|
||||
const DocsCliRoute = DocsCliRouteImport.update({
|
||||
id: '/cli',
|
||||
path: '/cli',
|
||||
id: "/cli",
|
||||
path: "/cli",
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any)
|
||||
} as any);
|
||||
const DocsBestPracticesRoute = DocsBestPracticesRouteImport.update({
|
||||
id: '/best-practices',
|
||||
path: '/best-practices',
|
||||
id: "/best-practices",
|
||||
path: "/best-practices",
|
||||
getParentRoute: () => DocsRoute,
|
||||
} as any)
|
||||
} as any);
|
||||
const BlogSplatRoute = BlogSplatRouteImport.update({
|
||||
id: '/$',
|
||||
path: '/$',
|
||||
id: "/$",
|
||||
path: "/$",
|
||||
getParentRoute: () => BlogRoute,
|
||||
} as any)
|
||||
} as any);
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/blog': typeof BlogRouteWithChildren
|
||||
'/changelog': typeof ChangelogRoute
|
||||
'/claude-code': typeof ClaudeCodeRoute
|
||||
'/codex': typeof CodexRoute
|
||||
'/docs': typeof DocsRouteWithChildren
|
||||
'/download': typeof DownloadRoute
|
||||
'/opencode': typeof OpencodeRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/blog/$': typeof BlogSplatRoute
|
||||
'/docs/best-practices': typeof DocsBestPracticesRoute
|
||||
'/docs/cli': typeof DocsCliRoute
|
||||
'/docs/configuration': typeof DocsConfigurationRoute
|
||||
'/docs/security': typeof DocsSecurityRoute
|
||||
'/docs/skills': typeof DocsSkillsRoute
|
||||
'/docs/updates': typeof DocsUpdatesRoute
|
||||
'/docs/voice': typeof DocsVoiceRoute
|
||||
'/docs/worktrees': typeof DocsWorktreesRoute
|
||||
'/blog/': typeof BlogIndexRoute
|
||||
'/docs/': typeof DocsIndexRoute
|
||||
"/": typeof IndexRoute;
|
||||
"/blog": typeof BlogRouteWithChildren;
|
||||
"/changelog": typeof ChangelogRoute;
|
||||
"/claude-code": typeof ClaudeCodeRoute;
|
||||
"/codex": typeof CodexRoute;
|
||||
"/docs": typeof DocsRouteWithChildren;
|
||||
"/download": typeof DownloadRoute;
|
||||
"/opencode": typeof OpencodeRoute;
|
||||
"/privacy": typeof PrivacyRoute;
|
||||
"/blog/$": typeof BlogSplatRoute;
|
||||
"/docs/best-practices": typeof DocsBestPracticesRoute;
|
||||
"/docs/cli": typeof DocsCliRoute;
|
||||
"/docs/configuration": typeof DocsConfigurationRoute;
|
||||
"/docs/security": typeof DocsSecurityRoute;
|
||||
"/docs/skills": typeof DocsSkillsRoute;
|
||||
"/docs/updates": typeof DocsUpdatesRoute;
|
||||
"/docs/voice": typeof DocsVoiceRoute;
|
||||
"/docs/worktrees": typeof DocsWorktreesRoute;
|
||||
"/blog/": typeof BlogIndexRoute;
|
||||
"/docs/": typeof DocsIndexRoute;
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/changelog': typeof ChangelogRoute
|
||||
'/claude-code': typeof ClaudeCodeRoute
|
||||
'/codex': typeof CodexRoute
|
||||
'/download': typeof DownloadRoute
|
||||
'/opencode': typeof OpencodeRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/blog/$': typeof BlogSplatRoute
|
||||
'/docs/best-practices': typeof DocsBestPracticesRoute
|
||||
'/docs/cli': typeof DocsCliRoute
|
||||
'/docs/configuration': typeof DocsConfigurationRoute
|
||||
'/docs/security': typeof DocsSecurityRoute
|
||||
'/docs/skills': typeof DocsSkillsRoute
|
||||
'/docs/updates': typeof DocsUpdatesRoute
|
||||
'/docs/voice': typeof DocsVoiceRoute
|
||||
'/docs/worktrees': typeof DocsWorktreesRoute
|
||||
'/blog': typeof BlogIndexRoute
|
||||
'/docs': typeof DocsIndexRoute
|
||||
"/": typeof IndexRoute;
|
||||
"/changelog": typeof ChangelogRoute;
|
||||
"/claude-code": typeof ClaudeCodeRoute;
|
||||
"/codex": typeof CodexRoute;
|
||||
"/download": typeof DownloadRoute;
|
||||
"/opencode": typeof OpencodeRoute;
|
||||
"/privacy": typeof PrivacyRoute;
|
||||
"/blog/$": typeof BlogSplatRoute;
|
||||
"/docs/best-practices": typeof DocsBestPracticesRoute;
|
||||
"/docs/cli": typeof DocsCliRoute;
|
||||
"/docs/configuration": typeof DocsConfigurationRoute;
|
||||
"/docs/security": typeof DocsSecurityRoute;
|
||||
"/docs/skills": typeof DocsSkillsRoute;
|
||||
"/docs/updates": typeof DocsUpdatesRoute;
|
||||
"/docs/voice": typeof DocsVoiceRoute;
|
||||
"/docs/worktrees": typeof DocsWorktreesRoute;
|
||||
"/blog": typeof BlogIndexRoute;
|
||||
"/docs": typeof DocsIndexRoute;
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/blog': typeof BlogRouteWithChildren
|
||||
'/changelog': typeof ChangelogRoute
|
||||
'/claude-code': typeof ClaudeCodeRoute
|
||||
'/codex': typeof CodexRoute
|
||||
'/docs': typeof DocsRouteWithChildren
|
||||
'/download': typeof DownloadRoute
|
||||
'/opencode': typeof OpencodeRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/blog/$': typeof BlogSplatRoute
|
||||
'/docs/best-practices': typeof DocsBestPracticesRoute
|
||||
'/docs/cli': typeof DocsCliRoute
|
||||
'/docs/configuration': typeof DocsConfigurationRoute
|
||||
'/docs/security': typeof DocsSecurityRoute
|
||||
'/docs/skills': typeof DocsSkillsRoute
|
||||
'/docs/updates': typeof DocsUpdatesRoute
|
||||
'/docs/voice': typeof DocsVoiceRoute
|
||||
'/docs/worktrees': typeof DocsWorktreesRoute
|
||||
'/blog/': typeof BlogIndexRoute
|
||||
'/docs/': typeof DocsIndexRoute
|
||||
__root__: typeof rootRouteImport;
|
||||
"/": typeof IndexRoute;
|
||||
"/blog": typeof BlogRouteWithChildren;
|
||||
"/changelog": typeof ChangelogRoute;
|
||||
"/claude-code": typeof ClaudeCodeRoute;
|
||||
"/codex": typeof CodexRoute;
|
||||
"/docs": typeof DocsRouteWithChildren;
|
||||
"/download": typeof DownloadRoute;
|
||||
"/opencode": typeof OpencodeRoute;
|
||||
"/privacy": typeof PrivacyRoute;
|
||||
"/blog/$": typeof BlogSplatRoute;
|
||||
"/docs/best-practices": typeof DocsBestPracticesRoute;
|
||||
"/docs/cli": typeof DocsCliRoute;
|
||||
"/docs/configuration": typeof DocsConfigurationRoute;
|
||||
"/docs/security": typeof DocsSecurityRoute;
|
||||
"/docs/skills": typeof DocsSkillsRoute;
|
||||
"/docs/updates": typeof DocsUpdatesRoute;
|
||||
"/docs/voice": typeof DocsVoiceRoute;
|
||||
"/docs/worktrees": typeof DocsWorktreesRoute;
|
||||
"/blog/": typeof BlogIndexRoute;
|
||||
"/docs/": typeof DocsIndexRoute;
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/blog'
|
||||
| '/changelog'
|
||||
| '/claude-code'
|
||||
| '/codex'
|
||||
| '/docs'
|
||||
| '/download'
|
||||
| '/opencode'
|
||||
| '/privacy'
|
||||
| '/blog/$'
|
||||
| '/docs/best-practices'
|
||||
| '/docs/cli'
|
||||
| '/docs/configuration'
|
||||
| '/docs/security'
|
||||
| '/docs/skills'
|
||||
| '/docs/updates'
|
||||
| '/docs/voice'
|
||||
| '/docs/worktrees'
|
||||
| '/blog/'
|
||||
| '/docs/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
| "/"
|
||||
| "/blog"
|
||||
| "/changelog"
|
||||
| "/claude-code"
|
||||
| "/codex"
|
||||
| "/docs"
|
||||
| "/download"
|
||||
| "/opencode"
|
||||
| "/privacy"
|
||||
| "/blog/$"
|
||||
| "/docs/best-practices"
|
||||
| "/docs/cli"
|
||||
| "/docs/configuration"
|
||||
| "/docs/security"
|
||||
| "/docs/skills"
|
||||
| "/docs/updates"
|
||||
| "/docs/voice"
|
||||
| "/docs/worktrees"
|
||||
| "/blog/"
|
||||
| "/docs/";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to:
|
||||
| '/'
|
||||
| '/changelog'
|
||||
| '/claude-code'
|
||||
| '/codex'
|
||||
| '/download'
|
||||
| '/opencode'
|
||||
| '/privacy'
|
||||
| '/blog/$'
|
||||
| '/docs/best-practices'
|
||||
| '/docs/cli'
|
||||
| '/docs/configuration'
|
||||
| '/docs/security'
|
||||
| '/docs/skills'
|
||||
| '/docs/updates'
|
||||
| '/docs/voice'
|
||||
| '/docs/worktrees'
|
||||
| '/blog'
|
||||
| '/docs'
|
||||
| "/"
|
||||
| "/changelog"
|
||||
| "/claude-code"
|
||||
| "/codex"
|
||||
| "/download"
|
||||
| "/opencode"
|
||||
| "/privacy"
|
||||
| "/blog/$"
|
||||
| "/docs/best-practices"
|
||||
| "/docs/cli"
|
||||
| "/docs/configuration"
|
||||
| "/docs/security"
|
||||
| "/docs/skills"
|
||||
| "/docs/updates"
|
||||
| "/docs/voice"
|
||||
| "/docs/worktrees"
|
||||
| "/blog"
|
||||
| "/docs";
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/blog'
|
||||
| '/changelog'
|
||||
| '/claude-code'
|
||||
| '/codex'
|
||||
| '/docs'
|
||||
| '/download'
|
||||
| '/opencode'
|
||||
| '/privacy'
|
||||
| '/blog/$'
|
||||
| '/docs/best-practices'
|
||||
| '/docs/cli'
|
||||
| '/docs/configuration'
|
||||
| '/docs/security'
|
||||
| '/docs/skills'
|
||||
| '/docs/updates'
|
||||
| '/docs/voice'
|
||||
| '/docs/worktrees'
|
||||
| '/blog/'
|
||||
| '/docs/'
|
||||
fileRoutesById: FileRoutesById
|
||||
| "__root__"
|
||||
| "/"
|
||||
| "/blog"
|
||||
| "/changelog"
|
||||
| "/claude-code"
|
||||
| "/codex"
|
||||
| "/docs"
|
||||
| "/download"
|
||||
| "/opencode"
|
||||
| "/privacy"
|
||||
| "/blog/$"
|
||||
| "/docs/best-practices"
|
||||
| "/docs/cli"
|
||||
| "/docs/configuration"
|
||||
| "/docs/security"
|
||||
| "/docs/skills"
|
||||
| "/docs/updates"
|
||||
| "/docs/voice"
|
||||
| "/docs/worktrees"
|
||||
| "/blog/"
|
||||
| "/docs/";
|
||||
fileRoutesById: FileRoutesById;
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
BlogRoute: typeof BlogRouteWithChildren
|
||||
ChangelogRoute: typeof ChangelogRoute
|
||||
ClaudeCodeRoute: typeof ClaudeCodeRoute
|
||||
CodexRoute: typeof CodexRoute
|
||||
DocsRoute: typeof DocsRouteWithChildren
|
||||
DownloadRoute: typeof DownloadRoute
|
||||
OpencodeRoute: typeof OpencodeRoute
|
||||
PrivacyRoute: typeof PrivacyRoute
|
||||
IndexRoute: typeof IndexRoute;
|
||||
BlogRoute: typeof BlogRouteWithChildren;
|
||||
ChangelogRoute: typeof ChangelogRoute;
|
||||
ClaudeCodeRoute: typeof ClaudeCodeRoute;
|
||||
CodexRoute: typeof CodexRoute;
|
||||
DocsRoute: typeof DocsRouteWithChildren;
|
||||
DownloadRoute: typeof DownloadRoute;
|
||||
OpencodeRoute: typeof OpencodeRoute;
|
||||
PrivacyRoute: typeof PrivacyRoute;
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
declare module "@tanstack/react-router" {
|
||||
interface FileRoutesByPath {
|
||||
'/privacy': {
|
||||
id: '/privacy'
|
||||
path: '/privacy'
|
||||
fullPath: '/privacy'
|
||||
preLoaderRoute: typeof PrivacyRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/opencode': {
|
||||
id: '/opencode'
|
||||
path: '/opencode'
|
||||
fullPath: '/opencode'
|
||||
preLoaderRoute: typeof OpencodeRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/download': {
|
||||
id: '/download'
|
||||
path: '/download'
|
||||
fullPath: '/download'
|
||||
preLoaderRoute: typeof DownloadRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/docs': {
|
||||
id: '/docs'
|
||||
path: '/docs'
|
||||
fullPath: '/docs'
|
||||
preLoaderRoute: typeof DocsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/codex': {
|
||||
id: '/codex'
|
||||
path: '/codex'
|
||||
fullPath: '/codex'
|
||||
preLoaderRoute: typeof CodexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/claude-code': {
|
||||
id: '/claude-code'
|
||||
path: '/claude-code'
|
||||
fullPath: '/claude-code'
|
||||
preLoaderRoute: typeof ClaudeCodeRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/changelog': {
|
||||
id: '/changelog'
|
||||
path: '/changelog'
|
||||
fullPath: '/changelog'
|
||||
preLoaderRoute: typeof ChangelogRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/blog': {
|
||||
id: '/blog'
|
||||
path: '/blog'
|
||||
fullPath: '/blog'
|
||||
preLoaderRoute: typeof BlogRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/docs/': {
|
||||
id: '/docs/'
|
||||
path: '/'
|
||||
fullPath: '/docs/'
|
||||
preLoaderRoute: typeof DocsIndexRouteImport
|
||||
parentRoute: typeof DocsRoute
|
||||
}
|
||||
'/blog/': {
|
||||
id: '/blog/'
|
||||
path: '/'
|
||||
fullPath: '/blog/'
|
||||
preLoaderRoute: typeof BlogIndexRouteImport
|
||||
parentRoute: typeof BlogRoute
|
||||
}
|
||||
'/docs/worktrees': {
|
||||
id: '/docs/worktrees'
|
||||
path: '/worktrees'
|
||||
fullPath: '/docs/worktrees'
|
||||
preLoaderRoute: typeof DocsWorktreesRouteImport
|
||||
parentRoute: typeof DocsRoute
|
||||
}
|
||||
'/docs/voice': {
|
||||
id: '/docs/voice'
|
||||
path: '/voice'
|
||||
fullPath: '/docs/voice'
|
||||
preLoaderRoute: typeof DocsVoiceRouteImport
|
||||
parentRoute: typeof DocsRoute
|
||||
}
|
||||
'/docs/updates': {
|
||||
id: '/docs/updates'
|
||||
path: '/updates'
|
||||
fullPath: '/docs/updates'
|
||||
preLoaderRoute: typeof DocsUpdatesRouteImport
|
||||
parentRoute: typeof DocsRoute
|
||||
}
|
||||
'/docs/skills': {
|
||||
id: '/docs/skills'
|
||||
path: '/skills'
|
||||
fullPath: '/docs/skills'
|
||||
preLoaderRoute: typeof DocsSkillsRouteImport
|
||||
parentRoute: typeof DocsRoute
|
||||
}
|
||||
'/docs/security': {
|
||||
id: '/docs/security'
|
||||
path: '/security'
|
||||
fullPath: '/docs/security'
|
||||
preLoaderRoute: typeof DocsSecurityRouteImport
|
||||
parentRoute: typeof DocsRoute
|
||||
}
|
||||
'/docs/configuration': {
|
||||
id: '/docs/configuration'
|
||||
path: '/configuration'
|
||||
fullPath: '/docs/configuration'
|
||||
preLoaderRoute: typeof DocsConfigurationRouteImport
|
||||
parentRoute: typeof DocsRoute
|
||||
}
|
||||
'/docs/cli': {
|
||||
id: '/docs/cli'
|
||||
path: '/cli'
|
||||
fullPath: '/docs/cli'
|
||||
preLoaderRoute: typeof DocsCliRouteImport
|
||||
parentRoute: typeof DocsRoute
|
||||
}
|
||||
'/docs/best-practices': {
|
||||
id: '/docs/best-practices'
|
||||
path: '/best-practices'
|
||||
fullPath: '/docs/best-practices'
|
||||
preLoaderRoute: typeof DocsBestPracticesRouteImport
|
||||
parentRoute: typeof DocsRoute
|
||||
}
|
||||
'/blog/$': {
|
||||
id: '/blog/$'
|
||||
path: '/$'
|
||||
fullPath: '/blog/$'
|
||||
preLoaderRoute: typeof BlogSplatRouteImport
|
||||
parentRoute: typeof BlogRoute
|
||||
}
|
||||
"/privacy": {
|
||||
id: "/privacy";
|
||||
path: "/privacy";
|
||||
fullPath: "/privacy";
|
||||
preLoaderRoute: typeof PrivacyRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/opencode": {
|
||||
id: "/opencode";
|
||||
path: "/opencode";
|
||||
fullPath: "/opencode";
|
||||
preLoaderRoute: typeof OpencodeRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/download": {
|
||||
id: "/download";
|
||||
path: "/download";
|
||||
fullPath: "/download";
|
||||
preLoaderRoute: typeof DownloadRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/docs": {
|
||||
id: "/docs";
|
||||
path: "/docs";
|
||||
fullPath: "/docs";
|
||||
preLoaderRoute: typeof DocsRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/codex": {
|
||||
id: "/codex";
|
||||
path: "/codex";
|
||||
fullPath: "/codex";
|
||||
preLoaderRoute: typeof CodexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/claude-code": {
|
||||
id: "/claude-code";
|
||||
path: "/claude-code";
|
||||
fullPath: "/claude-code";
|
||||
preLoaderRoute: typeof ClaudeCodeRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/changelog": {
|
||||
id: "/changelog";
|
||||
path: "/changelog";
|
||||
fullPath: "/changelog";
|
||||
preLoaderRoute: typeof ChangelogRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/blog": {
|
||||
id: "/blog";
|
||||
path: "/blog";
|
||||
fullPath: "/blog";
|
||||
preLoaderRoute: typeof BlogRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/": {
|
||||
id: "/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/docs/": {
|
||||
id: "/docs/";
|
||||
path: "/";
|
||||
fullPath: "/docs/";
|
||||
preLoaderRoute: typeof DocsIndexRouteImport;
|
||||
parentRoute: typeof DocsRoute;
|
||||
};
|
||||
"/blog/": {
|
||||
id: "/blog/";
|
||||
path: "/";
|
||||
fullPath: "/blog/";
|
||||
preLoaderRoute: typeof BlogIndexRouteImport;
|
||||
parentRoute: typeof BlogRoute;
|
||||
};
|
||||
"/docs/worktrees": {
|
||||
id: "/docs/worktrees";
|
||||
path: "/worktrees";
|
||||
fullPath: "/docs/worktrees";
|
||||
preLoaderRoute: typeof DocsWorktreesRouteImport;
|
||||
parentRoute: typeof DocsRoute;
|
||||
};
|
||||
"/docs/voice": {
|
||||
id: "/docs/voice";
|
||||
path: "/voice";
|
||||
fullPath: "/docs/voice";
|
||||
preLoaderRoute: typeof DocsVoiceRouteImport;
|
||||
parentRoute: typeof DocsRoute;
|
||||
};
|
||||
"/docs/updates": {
|
||||
id: "/docs/updates";
|
||||
path: "/updates";
|
||||
fullPath: "/docs/updates";
|
||||
preLoaderRoute: typeof DocsUpdatesRouteImport;
|
||||
parentRoute: typeof DocsRoute;
|
||||
};
|
||||
"/docs/skills": {
|
||||
id: "/docs/skills";
|
||||
path: "/skills";
|
||||
fullPath: "/docs/skills";
|
||||
preLoaderRoute: typeof DocsSkillsRouteImport;
|
||||
parentRoute: typeof DocsRoute;
|
||||
};
|
||||
"/docs/security": {
|
||||
id: "/docs/security";
|
||||
path: "/security";
|
||||
fullPath: "/docs/security";
|
||||
preLoaderRoute: typeof DocsSecurityRouteImport;
|
||||
parentRoute: typeof DocsRoute;
|
||||
};
|
||||
"/docs/configuration": {
|
||||
id: "/docs/configuration";
|
||||
path: "/configuration";
|
||||
fullPath: "/docs/configuration";
|
||||
preLoaderRoute: typeof DocsConfigurationRouteImport;
|
||||
parentRoute: typeof DocsRoute;
|
||||
};
|
||||
"/docs/cli": {
|
||||
id: "/docs/cli";
|
||||
path: "/cli";
|
||||
fullPath: "/docs/cli";
|
||||
preLoaderRoute: typeof DocsCliRouteImport;
|
||||
parentRoute: typeof DocsRoute;
|
||||
};
|
||||
"/docs/best-practices": {
|
||||
id: "/docs/best-practices";
|
||||
path: "/best-practices";
|
||||
fullPath: "/docs/best-practices";
|
||||
preLoaderRoute: typeof DocsBestPracticesRouteImport;
|
||||
parentRoute: typeof DocsRoute;
|
||||
};
|
||||
"/blog/$": {
|
||||
id: "/blog/$";
|
||||
path: "/$";
|
||||
fullPath: "/blog/$";
|
||||
preLoaderRoute: typeof BlogSplatRouteImport;
|
||||
parentRoute: typeof BlogRoute;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface BlogRouteChildren {
|
||||
BlogSplatRoute: typeof BlogSplatRoute
|
||||
BlogIndexRoute: typeof BlogIndexRoute
|
||||
BlogSplatRoute: typeof BlogSplatRoute;
|
||||
BlogIndexRoute: typeof BlogIndexRoute;
|
||||
}
|
||||
|
||||
const BlogRouteChildren: BlogRouteChildren = {
|
||||
BlogSplatRoute: BlogSplatRoute,
|
||||
BlogIndexRoute: BlogIndexRoute,
|
||||
}
|
||||
};
|
||||
|
||||
const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren)
|
||||
const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren);
|
||||
|
||||
interface DocsRouteChildren {
|
||||
DocsBestPracticesRoute: typeof DocsBestPracticesRoute
|
||||
DocsCliRoute: typeof DocsCliRoute
|
||||
DocsConfigurationRoute: typeof DocsConfigurationRoute
|
||||
DocsSecurityRoute: typeof DocsSecurityRoute
|
||||
DocsSkillsRoute: typeof DocsSkillsRoute
|
||||
DocsUpdatesRoute: typeof DocsUpdatesRoute
|
||||
DocsVoiceRoute: typeof DocsVoiceRoute
|
||||
DocsWorktreesRoute: typeof DocsWorktreesRoute
|
||||
DocsIndexRoute: typeof DocsIndexRoute
|
||||
DocsBestPracticesRoute: typeof DocsBestPracticesRoute;
|
||||
DocsCliRoute: typeof DocsCliRoute;
|
||||
DocsConfigurationRoute: typeof DocsConfigurationRoute;
|
||||
DocsSecurityRoute: typeof DocsSecurityRoute;
|
||||
DocsSkillsRoute: typeof DocsSkillsRoute;
|
||||
DocsUpdatesRoute: typeof DocsUpdatesRoute;
|
||||
DocsVoiceRoute: typeof DocsVoiceRoute;
|
||||
DocsWorktreesRoute: typeof DocsWorktreesRoute;
|
||||
DocsIndexRoute: typeof DocsIndexRoute;
|
||||
}
|
||||
|
||||
const DocsRouteChildren: DocsRouteChildren = {
|
||||
@@ -454,9 +454,9 @@ const DocsRouteChildren: DocsRouteChildren = {
|
||||
DocsVoiceRoute: DocsVoiceRoute,
|
||||
DocsWorktreesRoute: DocsWorktreesRoute,
|
||||
DocsIndexRoute: DocsIndexRoute,
|
||||
}
|
||||
};
|
||||
|
||||
const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren)
|
||||
const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren);
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
@@ -468,16 +468,16 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
DownloadRoute: DownloadRoute,
|
||||
OpencodeRoute: OpencodeRoute,
|
||||
PrivacyRoute: PrivacyRoute,
|
||||
}
|
||||
};
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
._addFileTypes<FileRouteTypes>();
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
import type { getRouter } from "./router.tsx";
|
||||
import type { createStart } from "@tanstack/react-start";
|
||||
declare module "@tanstack/react-start" {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
ssr: true;
|
||||
router: Awaited<ReturnType<typeof getRouter>>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,31 @@ import type { ReactNode } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import { Outlet, createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
|
||||
import { getLatestRelease } from "~/release";
|
||||
import { getStarCount } from "~/stars";
|
||||
|
||||
interface ReleaseContext {
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface StarsContext {
|
||||
stars: string;
|
||||
}
|
||||
|
||||
const ReleaseCtx = createContext<ReleaseContext>({ version: "" });
|
||||
const StarsCtx = createContext<StarsContext>({ stars: "" });
|
||||
|
||||
export function useRelease(): ReleaseContext {
|
||||
return useContext(ReleaseCtx);
|
||||
}
|
||||
|
||||
export function useStars(): StarsContext {
|
||||
return useContext(StarsCtx);
|
||||
}
|
||||
|
||||
export const Route = createRootRoute({
|
||||
loader: async () => {
|
||||
return getLatestRelease();
|
||||
const [release, stars] = await Promise.all([getLatestRelease(), getStarCount()]);
|
||||
return { ...release, ...stars };
|
||||
},
|
||||
head: () => ({
|
||||
meta: [
|
||||
@@ -38,12 +49,14 @@ export const Route = createRootRoute({
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
const release = Route.useLoaderData();
|
||||
const data = Route.useLoaderData();
|
||||
return (
|
||||
<ReleaseCtx value={release}>
|
||||
<RootDocument>
|
||||
<Outlet />
|
||||
</RootDocument>
|
||||
<ReleaseCtx value={data}>
|
||||
<StarsCtx value={data}>
|
||||
<RootDocument>
|
||||
<Outlet />
|
||||
</RootDocument>
|
||||
</StarsCtx>
|
||||
</ReleaseCtx>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ function GettingStarted() {
|
||||
<div>
|
||||
<h1 className="text-3xl font-medium font-title mb-4">Getting Started</h1>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Paseo has three main pieces: the daemon is the local server that manages your agents,
|
||||
the app is the client you use from mobile, web, or desktop, and the CLI is the terminal
|
||||
Paseo has three main pieces: the daemon is the local server that manages your agents, the
|
||||
app is the client you use from mobile, web, or desktop, and the CLI is the terminal
|
||||
interface that can also launch the daemon.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -26,9 +26,9 @@ function Skills() {
|
||||
<h1 className="text-3xl font-medium font-title mb-4">Orchestration Skills</h1>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Paseo ships orchestration skills that teach coding agents (Claude Code, Codex) how to use
|
||||
the Paseo CLI to spawn, coordinate, and manage other agents. Skills are slash commands your
|
||||
agent can invoke — they provide the prompts, context, and workflows so agents know how to
|
||||
orchestrate without you writing boilerplate. Install them from the desktop app's
|
||||
the Paseo CLI to spawn, coordinate, and manage other agents. Skills are slash commands
|
||||
your agent can invoke — they provide the prompts, context, and workflows so agents know
|
||||
how to orchestrate without you writing boilerplate. Install them from the desktop app's
|
||||
Integrations settings or via the CLI.
|
||||
</p>
|
||||
</div>
|
||||
@@ -42,9 +42,10 @@ function Skills() {
|
||||
<strong>Desktop app:</strong> Settings → Integrations → Install
|
||||
</li>
|
||||
<li>
|
||||
<strong>Manual:</strong> <code className="font-mono">npx skills add getpaseo/paseo</code>{" "}
|
||||
— this installs to <code className="font-mono">~/.agents/skills/</code> and sets up
|
||||
symlinks for each agent.
|
||||
<strong>Manual:</strong>{" "}
|
||||
<code className="font-mono">npx skills add getpaseo/paseo</code> — this installs to{" "}
|
||||
<code className="font-mono">~/.agents/skills/</code> and sets up symlinks for each
|
||||
agent.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -90,8 +91,8 @@ function Skills() {
|
||||
</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Runs an agent in a loop with automatic verification until an exit condition is met. Worker
|
||||
runs, verifier checks, repeat until done or max iterations. Supports different providers for
|
||||
worker vs verifier (e.g., Codex implements, Claude verifies).
|
||||
runs, verifier checks, repeat until done or max iterations. Supports different providers
|
||||
for worker vs verifier (e.g., Codex implements, Claude verifies).
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Stop conditions: <code className="font-mono">--max-iterations</code>,{" "}
|
||||
@@ -110,8 +111,8 @@ function Skills() {
|
||||
</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Builds and manages a team of agents coordinating through a shared chat room. You describe
|
||||
the work, it sets up roles, launches agents, and coordinates through chat. Uses a heartbeat
|
||||
schedule to check progress.
|
||||
the work, it sets up roles, launches agents, and coordinates through chat. Uses a
|
||||
heartbeat schedule to check progress.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Cross-provider: typically Codex for implementation, Claude for review.
|
||||
@@ -127,8 +128,9 @@ function Skills() {
|
||||
<code className="font-mono">/paseo-chat</code> — Chat Rooms
|
||||
</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Use persistent chat rooms for asynchronous agent coordination. Create rooms, post messages,
|
||||
read history, wait for replies. Supports @mentions for specific agents or @everyone.
|
||||
Use persistent chat rooms for asynchronous agent coordination. Create rooms, post
|
||||
messages, read history, wait for replies. Supports @mentions for specific agents or
|
||||
@everyone.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Typically used by the orchestrator skill, but can be used directly.
|
||||
@@ -145,9 +147,9 @@ function Skills() {
|
||||
<code className="font-mono">/paseo-committee</code> — Committee Planning
|
||||
</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Forms a committee of two high-reasoning agents (Claude Opus + GPT 5.4) to analyze a problem
|
||||
before implementing. Both agents reason in parallel, then plans are merged. Useful when
|
||||
stuck, looping, or facing a hard architectural decision.
|
||||
Forms a committee of two high-reasoning agents (Claude Opus + GPT 5.4) to analyze a
|
||||
problem before implementing. Both agents reason in parallel, then plans are merged. Useful
|
||||
when stuck, looping, or facing a hard architectural decision.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Agents are prevented from editing code — they only produce a plan.
|
||||
|
||||
@@ -172,9 +172,9 @@ function Worktrees() {
|
||||
checkout to the worktree.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
<code className="font-mono">$PASEO_WORKTREE_PORT</code> is available when the worktree
|
||||
was bootstrapped with a port. That makes it useful for both starting services in setup
|
||||
and stopping them again in teardown.
|
||||
<code className="font-mono">$PASEO_WORKTREE_PORT</code> is available when the worktree was
|
||||
bootstrapped with a port. That makes it useful for both starting services in setup and
|
||||
stopping them again in teardown.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -90,9 +90,7 @@ function Download() {
|
||||
</header>
|
||||
|
||||
<h1 className="text-3xl md:text-4xl font-semibold tracking-tight mb-2">Download</h1>
|
||||
<p className="text-muted-foreground mb-10">
|
||||
v{version}
|
||||
</p>
|
||||
<p className="text-muted-foreground mb-10">v{version}</p>
|
||||
|
||||
{/* Desktop */}
|
||||
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
|
||||
@@ -130,10 +128,7 @@ function Download() {
|
||||
<span className="font-medium">Windows</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill
|
||||
href={urls.windowsExe}
|
||||
label="Download"
|
||||
/>
|
||||
<DownloadPill href={urls.windowsExe} label="Download" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,10 +139,7 @@ function Download() {
|
||||
<span className="font-medium">Linux</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill
|
||||
href={urls.linuxAppImage}
|
||||
label="AppImage"
|
||||
/>
|
||||
<DownloadPill href={urls.linuxAppImage} label="AppImage" />
|
||||
<DownloadPill href={urls.linuxDeb} label="DEB" />
|
||||
<DownloadPill href={urls.linuxRpm} label="RPM" />
|
||||
</div>
|
||||
@@ -170,15 +162,8 @@ function Download() {
|
||||
<span className="font-medium">Android</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill
|
||||
href={playStoreUrl}
|
||||
label="Play Store"
|
||||
external
|
||||
/>
|
||||
<DownloadPill
|
||||
href={urls.androidApk}
|
||||
label="APK"
|
||||
/>
|
||||
<DownloadPill href={playStoreUrl} label="Play Store" external />
|
||||
<DownloadPill href={urls.androidApk} label="APK" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -189,11 +174,7 @@ function Download() {
|
||||
<span className="font-medium">iOS</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill
|
||||
href={appStoreUrl}
|
||||
label="App Store"
|
||||
external
|
||||
/>
|
||||
<DownloadPill href={appStoreUrl} label="App Store" external />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,11 +194,7 @@ function Download() {
|
||||
<span className="font-medium">Web App</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<DownloadPill
|
||||
href={webAppUrl}
|
||||
label="Open"
|
||||
external
|
||||
/>
|
||||
<DownloadPill href={webAppUrl} label="Open" external />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,7 +15,13 @@ export const Route = createFileRoute("/")({
|
||||
function Home() {
|
||||
return (
|
||||
<LandingPage
|
||||
title={<>Orchestrate coding agents<br />from your desk and your phone</>}
|
||||
title={
|
||||
<>
|
||||
Orchestrate coding agents
|
||||
<br />
|
||||
from your desk and your phone
|
||||
</>
|
||||
}
|
||||
subtitle="Run any coding agent from your phone, desktop, or terminal. Self-hosted, multi-provider, open source."
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
|
||||
interface GitHubRepo {
|
||||
stargazers_count: number;
|
||||
}
|
||||
|
||||
function formatStars(count: number): string {
|
||||
if (count < 1000) return String(count);
|
||||
const k = count / 1000;
|
||||
return `${k % 1 === 0 ? k.toFixed(0) : k.toFixed(1)}k`;
|
||||
}
|
||||
|
||||
const GITHUB_REPO_URL = "https://api.github.com/repos/getpaseo/paseo";
|
||||
|
||||
async function fetchStarCount(): Promise<string> {
|
||||
try {
|
||||
const res = await fetch(GITHUB_REPO_URL, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": "paseo-website",
|
||||
},
|
||||
cf: {
|
||||
cacheEverything: true,
|
||||
cacheTtl: 60,
|
||||
cacheKey: "github-repo-stars",
|
||||
},
|
||||
} as RequestInit);
|
||||
if (!res.ok) return "";
|
||||
|
||||
const repo = (await res.json()) as GitHubRepo;
|
||||
return formatStars(repo.stargazers_count);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export const getStarCount = createServerFn({ method: "GET" }).handler(async () => {
|
||||
const stars = await fetchStarCount();
|
||||
return { stars };
|
||||
});
|
||||
@@ -1,8 +1,12 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@keyframes voice-bar {
|
||||
0% { transform: scaleY(1); }
|
||||
100% { transform: scaleY(0.3); }
|
||||
0% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* Title font - centralized for easy changes */
|
||||
@@ -189,26 +193,26 @@
|
||||
@theme {
|
||||
--color-background: #101615;
|
||||
--color-foreground: #fafafa;
|
||||
--color-muted: #252B2A;
|
||||
--color-muted-foreground: #A8ADAC;
|
||||
--color-card: #171D1C;
|
||||
--color-border: #252B2A;
|
||||
--color-muted: #252b2a;
|
||||
--color-muted-foreground: #a8adac;
|
||||
--color-card: #171d1c;
|
||||
--color-border: #252b2a;
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-foreground: #ffffff;
|
||||
--color-secondary: #252B2A;
|
||||
--color-secondary: #252b2a;
|
||||
--color-secondary-foreground: #fafafa;
|
||||
|
||||
/* Mockup palette (from app unistyles theme) */
|
||||
--color-mock-surface0: #181B1A;
|
||||
--color-mock-surface1: #1E2120;
|
||||
--color-mock-surface2: #272A29;
|
||||
--color-mock-surface0: #181b1a;
|
||||
--color-mock-surface1: #1e2120;
|
||||
--color-mock-surface2: #272a29;
|
||||
--color-mock-surface3: #434645;
|
||||
--color-mock-sidebar: #141716;
|
||||
--color-mock-fg: #fafafa;
|
||||
--color-mock-fg-muted: #A1A5A4;
|
||||
--color-mock-border: #272A29;
|
||||
--color-mock-fg-muted: #a1a5a4;
|
||||
--color-mock-border: #272a29;
|
||||
--color-mock-border-accent: #313433;
|
||||
--color-mock-accent: #20744A;
|
||||
--color-mock-accent: #20744a;
|
||||
--color-mock-green: #22c55e;
|
||||
--color-mock-green-400: #4ade80;
|
||||
--color-mock-red: #ef4444;
|
||||
@@ -243,12 +247,30 @@
|
||||
up to 4 dots are visible at decreasing opacity (head=1, trail=0.72/0.46/0.22).
|
||||
steps(1,end) keeps each opacity level solid for its full 1/6-cycle slot. */
|
||||
@keyframes synced-snake-dot {
|
||||
0% { opacity: 1; animation-timing-function: steps(1, end); }
|
||||
16.667% { opacity: 0.72; animation-timing-function: steps(1, end); }
|
||||
33.333% { opacity: 0.46; animation-timing-function: steps(1, end); }
|
||||
50% { opacity: 0.22; animation-timing-function: steps(1, end); }
|
||||
66.667% { opacity: 0; animation-timing-function: steps(1, end); }
|
||||
83.333% { opacity: 0; animation-timing-function: steps(1, end); }
|
||||
0% {
|
||||
opacity: 1;
|
||||
animation-timing-function: steps(1, end);
|
||||
}
|
||||
16.667% {
|
||||
opacity: 0.72;
|
||||
animation-timing-function: steps(1, end);
|
||||
}
|
||||
33.333% {
|
||||
opacity: 0.46;
|
||||
animation-timing-function: steps(1, end);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.22;
|
||||
animation-timing-function: steps(1, end);
|
||||
}
|
||||
66.667% {
|
||||
opacity: 0;
|
||||
animation-timing-function: steps(1, end);
|
||||
}
|
||||
83.333% {
|
||||
opacity: 0;
|
||||
animation-timing-function: steps(1, end);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flutter-left {
|
||||
|
||||
Reference in New Issue
Block a user