diff --git a/src/components/StoreBadge/index.tsx b/src/components/StoreBadge/index.tsx
index e0099c4..544f185 100644
--- a/src/components/StoreBadge/index.tsx
+++ b/src/components/StoreBadge/index.tsx
@@ -286,25 +286,34 @@ export function DirectDownloadButton({
* labels its downloads, and the visitor this page is for wants to know that
* clicking gets them the app, not what a .dmg is.
*/
+// What the block calls each architecture, where a platform offers more than
+// one and the block has to say which build it is.
+const ARCH_LABEL = { x64: 'x86_64', arm64: 'ARM64' } as const;
+
export function DirectDownloadBlock({
platform,
+ arch,
size,
variant = 'primary',
}: {
platform: DirectPlatform;
+ // Set only where the platform ships more than one build (Linux); the block
+ // then names the architecture in place of who the build covers.
+ arch?: 'x64' | 'arm64';
// Unknown until the release fetch returns; the block reads fine without it.
size?: string;
variant?: ButtonVariant;
}): ReactNode {
- const { mark, name, platform: releasePlatform, format, covers } = DIRECT_PLATFORM[platform];
- const path = useInstallerUrl()(releasePlatform);
+ const { mark, name, platform: releasePlatform, format, covers: platformCovers } = DIRECT_PLATFORM[platform];
+ const path = useInstallerUrl()(releasePlatform, arch);
+ const covers = arch ? ARCH_LABEL[arch] : platformCovers;
return (
diff --git a/src/components/StoreBadge/styles.module.css b/src/components/StoreBadge/styles.module.css
index 700713d..50ebd95 100644
--- a/src/components/StoreBadge/styles.module.css
+++ b/src/components/StoreBadge/styles.module.css
@@ -107,9 +107,7 @@
}
.directCta:hover,
-.directCta:focus-visible,
-.directBlock:hover,
-.directBlock:focus-visible {
+.directCta:focus-visible {
--dl-plate: var(--sl-white);
--dl-ink: var(--sl-ink);
color: var(--dl-ink);
@@ -118,8 +116,9 @@
}
/* On the download page the outline is the card's own border colour, so the
- button sits in its card as part of it; it turns solid only on hover. The
- light value matches the light theme's override of the card border in
+ button sits in its card as part of it. It never fills: on hover the outline
+ turns the brand pink; the card itself stays still. The light
+ value matches the light theme's override of the card border in
pages/index.module.css. */
.directBlock {
--dl-edge: var(--sl-line);
@@ -133,7 +132,9 @@
.directBlock:focus-visible,
[data-theme='light'] .directBlock:hover,
[data-theme='light'] .directBlock:focus-visible {
- --dl-edge: var(--sl-white);
+ --dl-edge: var(--sl-accent);
+ text-decoration: none;
+ transform: translateY(-1px);
}
/* The home page hero's button is the one exception: the solid brand pink, for
diff --git a/src/hooks/useLatestRelease.ts b/src/hooks/useLatestRelease.ts
index 6ff89ff..2afe3b8 100644
--- a/src/hooks/useLatestRelease.ts
+++ b/src/hooks/useLatestRelease.ts
@@ -59,17 +59,22 @@ export default function useLatestRelease(): LatestRelease {
: { status: 'unavailable', release: null };
}
-/** First artifact matching a platform/kind pair, or null. */
+/**
+ * The artifact for a platform/kind pair, or null. With `arch`, only that
+ * build; without it, x64 leads where there is one, as the API's download
+ * redirect does.
+ */
export function findArtifact(
release: Release | null,
platform: ArtifactPlatform,
kind: ArtifactKind,
+ arch?: ArtifactArch,
): ReleaseArtifact | null {
if (!release) return null;
- return (
- release.artifacts.find((a) => a.platform === platform && a.kind === kind) ??
- null
+ const matches = release.artifacts.filter(
+ (a) => a.platform === platform && a.kind === kind && (!arch || a.arch === arch),
);
+ return matches.find((a) => a.arch === 'x64') ?? matches[0] ?? null;
}
/**
diff --git a/src/pages/download.tsx b/src/pages/download.tsx
index 06c19c9..95b7cb7 100644
--- a/src/pages/download.tsx
+++ b/src/pages/download.tsx
@@ -1,4 +1,4 @@
-import { type ReactNode } from 'react';
+import { type ReactNode, useEffect, useState } from 'react';
import Head from '@docusaurus/Head';
import Header from '../components/Header';
import Footer from '../components/Footer';
@@ -7,6 +7,7 @@ import useLatestRelease, {
formatSize,
type ArtifactKind,
type ArtifactPlatform,
+ type Release,
} from '../hooks/useLatestRelease';
import {
DirectDownloadBlock,
@@ -54,6 +55,8 @@ type StudioCard = {
// payloads and must never be offered as a download link.
platform: ArtifactPlatform;
kind: ArtifactKind;
+ // One download per architecture where the platform ships more than one.
+ arches?: ('x64' | 'arm64')[];
};
const studioCards: StudioCard[] = [
@@ -90,10 +93,10 @@ const studioCards: StudioCard[] = [
{
key: 'linux',
title: 'Linux',
- meta: 'Linux · x86_64',
+ meta: 'Linux · x86_64 · ARM64',
icon: ,
minimum: [
- 'Linux x86_64 — Debian, Ubuntu, or a derivative',
+ 'Linux x86_64 or ARM64 — Debian, Ubuntu, or a derivative',
'Vulkan-capable GPU — NVIDIA, AMD, or Intel, including integrated graphics',
'16 GB system RAM',
'10 GB free disk space',
@@ -101,9 +104,130 @@ const studioCards: StudioCard[] = [
recommended: ['More system RAM is better — 24 GB or above', 'A discrete NVIDIA GPU (RTX 40-series or newer, 8 GB+ VRAM)'],
platform: 'linux',
kind: 'deb',
+ arches: ['x64', 'arm64'],
},
];
+type Arch = 'x64' | 'arm64';
+
+const ARCH_NAME: Record = { x64: 'x86_64', arm64: 'ARM64' };
+
+/**
+ * The machine's architecture, where the browser says. Chromium reports it
+ * through User-Agent Client Hints; other browsers sometimes name it in the
+ * user agent (aarch64 / arm64 on Linux). Null off Linux, or when there is no
+ * telling, and the switch stays on x86_64.
+ */
+async function detectArch(): Promise {
+ const nav = navigator as Navigator & {
+ userAgentData?: {
+ platform?: string;
+ getHighEntropyValues(hints: string[]): Promise<{ architecture?: string }>;
+ };
+ };
+ // Only a Linux machine's own architecture says which Linux build it wants;
+ // on a Mac or PC the download is for some other box, so leave the default.
+ const platform = nav.userAgentData?.platform ?? navigator.userAgent;
+ if (!/linux/i.test(platform) || /android/i.test(navigator.userAgent)) return null;
+ try {
+ const hints = await nav.userAgentData?.getHighEntropyValues(['architecture']);
+ if (hints?.architecture === 'arm') return 'arm64';
+ if (hints?.architecture === 'x86') return 'x64';
+ } catch {
+ // Hints refused; fall back to the user agent.
+ }
+ if (/aarch64|arm64|armv8/i.test(navigator.userAgent)) return 'arm64';
+ if (/x86_64|amd64|x64/i.test(navigator.userAgent)) return 'x64';
+ return null;
+}
+
+/**
+ * The build a multi-architecture card offers. It starts on the visitor's own
+ * architecture when the browser reports it, so most people never touch the
+ * switch.
+ */
+function useArch(arches: Arch[] | undefined): [Arch | undefined, (a: Arch) => void] {
+ const [arch, setArch] = useState(arches?.[0]);
+
+ useEffect(() => {
+ if (!arches) return;
+ let live = true;
+ detectArch().then((found) => {
+ if (live && found && arches.includes(found)) setArch(found);
+ });
+ return () => {
+ live = false;
+ };
+ }, []);
+
+ return [arch, setArch];
+}
+
+/**
+ * A platform's card. Where the platform ships more than one architecture, the
+ * switch takes the place of the line under its name, and the one button at
+ * the foot downloads whichever build is picked.
+ */
+function DownloadCard({ card, release }: { card: StudioCard; release: Release | null }): ReactNode {
+ const [arch, setArch] = useArch(card.arches);
+ const artifact = findArtifact(release, card.platform, card.kind, arch);
+
+ return (
+ // The download link carries its own click, so the card itself is a plain
+ // container rather than one big link, and does not light up on hover.
+
+
+
{card.icon}
+
+
{card.title}
+ {card.arches && arch ? (
+
+ {card.arches.map((a) => (
+ setArch(a)}
+ >
+ {ARCH_NAME[a]}
+
+ ))}
+
+ ) : (
+
{card.meta}
+ )}
+
+
+
Minimum
+
+ {card.minimum.map((req, i) => (
+ {req}
+ ))}
+
+
Recommended
+
+ {card.recommended.map((req, i) => (
+ {req}
+ ))}
+
+
+ {/* The block links to the API's tracked redirect, which resolves the
+ latest build itself; the release fetch only adds the size. Every
+ card gets the same plain button, so the visitor's own platform is
+ not singled out. */}
+
+
+
+ );
+}
+
export default function Download(): ReactNode {
const { status, release } = useLatestRelease();
@@ -138,48 +262,9 @@ export default function Download(): ReactNode {
{/* Studio downloads */}
- {studioCards.map((card) => {
- const artifact = findArtifact(release, card.platform, card.kind);
- // Every card gets the plain button; hover is what highlights
- // one, so the visitor's own platform is not singled out.
- const variant = 'secondary';
-
- return (
- // The badge and the direct link each carry their own click, so
- // the card itself is a plain container rather than one big link.
-
-
-
{card.icon}
-
-
{card.title}
-
{card.meta}
-
-
-
Minimum
-
- {card.minimum.map((req, i) => (
- {req}
- ))}
-
-
Recommended
-
- {card.recommended.map((req, i) => (
- {req}
- ))}
-
-
- {/* The block links to the API's tracked redirect,
- which resolves the latest build itself; the release
- fetch only adds the size. */}
-
-
-
- );
- })}
+ {studioCards.map((card) => (
+
+ ))}
diff --git a/src/pages/index.module.css b/src/pages/index.module.css
index 95e6625..f0992cc 100644
--- a/src/pages/index.module.css
+++ b/src/pages/index.module.css
@@ -746,13 +746,6 @@ details[open] .faqQuestion::after {
flex-direction: column;
gap: 14px;
text-decoration: none;
- transition: border-color 0.2s, background 0.2s;
-}
-
-.downloadCard:hover {
- border-color: var(--sl-accent);
- background: var(--sl-panel);
- text-decoration: none;
}
.downloadCardDisabled {
@@ -1027,6 +1020,23 @@ details[open] .faqQuestion::after {
color: var(--sl-accent);
}
+/* The architecture switch in the header of a card whose platform ships more
+ than one build, in place of the line under its name. Rounded like the
+ download button, and in the builds' own spelling. */
+.archSwitch {
+ margin-top: 4px;
+ padding: 2px;
+ border-radius: var(--r-sm, 10px);
+}
+
+.archSwitch .downloadOsTab {
+ padding: 3px 10px;
+ border-radius: calc(var(--r-sm, 10px) - 2px);
+ text-transform: none;
+ letter-spacing: 0.02em;
+ font-family: var(--sl-mono);
+}
+
/* ===================== Pricing ===================== */
.pricingGrid {
display: grid;
@@ -1375,10 +1385,6 @@ details[open] .faqQuestion::after {
border-color: rgba(204, 204, 204, 0.5);
}
-[data-theme='light'] .downloadCard:hover {
- background: #ededed;
-}
-
[data-theme='light'] .downloadCardTitle {
color: #0a0a0a;
}
@@ -1397,11 +1403,6 @@ details[open] .faqQuestion::after {
border-color: rgba(204, 204, 204, 0.5);
}
-[data-theme='light'] .downloadCard:hover {
- border-color: var(--sl-accent);
- background: #ededed;
-}
-
[data-theme='light'] .downloadCardAction {
background: rgba(0, 0, 0, 0.04);
color: #0a0a0a;
@@ -2828,15 +2829,6 @@ details[open] .faqQuestion::after {
box-shadow: var(--shadow-soft);
}
-.downloadCard:hover {
- border-color: rgba(236, 72, 153, 0.35);
- transform: translateY(-2px);
-}
-
-.downloadCard {
- transition: border-color 0.2s, transform 0.2s;
-}
-
.planCardFeatured {
border-color: rgba(236, 72, 153, 0.45);
box-shadow: var(--shadow-soft), 0 0 60px -10px var(--sl-accent-wash);
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 4f4b918..a2854a8 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -518,10 +518,11 @@ export default function Home(): ReactNode {
Same agent. No window.
- Install docent to chat with your documents, search the web and connect MCP servers — in your scripts or over SSH, with the same models and projects as Studio.
+ An agent harness for your terminal: build products, write code and chat with your documents, with the same models and projects as Studio.
- One command. Sets itself up on first run.
+ Writes code. Changes land as diffs, checked by your build until it passes.
+ Runs the work. Commits, installs and tests, once you say so.
Your files. Drop a PDF on the terminal and ask.
Scriptable. Pipe answers with --plain.
diff --git a/src/utils/api.ts b/src/utils/api.ts
index 1dd1f46..648e874 100644
--- a/src/utils/api.ts
+++ b/src/utils/api.ts
@@ -15,11 +15,14 @@ export function useApiBaseUrl(): string {
/**
* The tracked installer download for a platform. The API logs the hit,
* rate-limits it per address, and 302s to the artifact — the bundle never
- * carries a direct CDN link a scraper could loop on.
+ * carries a direct CDN link a scraper could loop on. `arch` picks the build
+ * where a platform ships more than one (Linux: x64 and arm64).
*/
-export function useInstallerUrl(): (platform: ReleasePlatform) => string {
+export function useInstallerUrl(): (platform: ReleasePlatform, arch?: 'x64' | 'arm64') => string {
const base = useApiBaseUrl();
- return (platform) => `${base}/v1/releases/download/${platform}`;
+ // Without arch the API serves x64 where a platform ships both.
+ return (platform, arch) =>
+ `${base}/v1/releases/download/${platform}${arch ? `?arch=${arch}` : ''}`;
}
export type ContactRequest = {