From 52484283733e706d583a8cfcc4abec341f948383 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 21 Oct 2026 19:28:46 -0700 Subject: [PATCH 1/2] Add download page with server-side Windows detection Agent Relay Desktop ships Windows builds now, but the site had no download page. This adds /download and detects the visitor's platform from the request (User-Agent plus Sec-CH-UA-* hints) so Windows visitors get the Windows build in the first response rather than after hydration. Windows links point at the release zips, since the workflow publishes AgentRelay-Windows-{x64,arm64}.zip rather than a standalone installer. Windows users are told to unpack and run agent-relay.exe and warned about the unsigned-binary SmartScreen prompt. Also lists macOS and Linux options, adds tarball alternatives for Linux, links to release checksums, and adds the route to the sitemap, nav, and footer. --- web/app/download/download.module.css | 266 +++++++++++++++++++++++++ web/app/download/page.tsx | 180 +++++++++++++++++ web/app/sitemap.ts | 6 + web/components/SiteFooter.tsx | 1 + web/components/SiteNav.tsx | 10 + web/lib/desktop-downloads.ts | 167 ++++++++++++++++ web/lib/test/desktop-downloads.test.ts | 81 ++++++++ 7 files changed, 711 insertions(+) create mode 100644 web/app/download/download.module.css create mode 100644 web/app/download/page.tsx create mode 100644 web/lib/desktop-downloads.ts create mode 100644 web/lib/test/desktop-downloads.test.ts diff --git a/web/app/download/download.module.css b/web/app/download/download.module.css new file mode 100644 index 00000000..5511f69b --- /dev/null +++ b/web/app/download/download.module.css @@ -0,0 +1,266 @@ +.page { + min-height: 100svh; + background: var(--bg); + color: var(--fg); + font-family: var(--font-geist-sans), sans-serif; + -webkit-font-smoothing: antialiased; + transition: + background 0.3s ease, + color 0.3s ease; +} + +.main { + max-width: 72rem; + margin: 0 auto; + padding: 3.5rem 1.25rem 5rem; +} + +/* ── Hero ── */ + +.hero { + text-align: center; + padding: 1rem 0 2.75rem; +} + +.eyebrow { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--primary); + margin: 0 0 12px; +} + +.headline { + font-family: var(--font-heading), sans-serif; + font-size: clamp(2.4rem, 6vw, 4rem); + font-weight: 700; + letter-spacing: -0.04em; + color: var(--primary); + margin: 0 0 16px; +} + +.lead { + max-width: 42rem; + margin: 0 auto; + color: var(--fg-muted); + font-size: 1.05rem; + line-height: 1.7; +} + +.recommendation { + width: fit-content; + max-width: 100%; + margin: 2rem auto 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.65rem; +} + +.detected { + margin: 0; + color: var(--fg-faint); + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.primary { + composes: btn btn-primary from global; + min-height: 56px; + padding: 16px 28px; + font-size: 1.0625rem; + gap: 10px; +} + +.installNote { + margin: 0; + color: var(--fg-muted); + font-size: 0.9rem; + line-height: 1.5; +} + +.warning { + max-width: 36rem; + margin: 0.25rem auto 0; + color: var(--fg-faint); + font-size: 0.82rem; + line-height: 1.55; +} + +.warning em { + color: var(--fg-muted); + font-style: italic; +} + +.warning strong { + color: var(--fg-muted); + font-weight: 600; +} + +.choose { + margin: 1.75rem 0 0; + color: var(--fg-muted); + font-size: 0.95rem; +} + +/* ── Platform cards ── */ + +.platforms { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.platform { + display: flex; + flex-direction: column; + gap: 1.1rem; + padding: 1.35rem; + border: 1px solid var(--line); + border-radius: var(--r-lg); + background: var(--card-bg); + transition: + border-color 0.2s, + box-shadow 0.2s; +} + +.platformRecommended { + border-color: color-mix(in srgb, var(--primary) 55%, var(--line)); + box-shadow: 0 2px 20px color-mix(in srgb, var(--primary) 8%, transparent); +} + +.platformHeader h2 { + margin: 0 0 0.45rem; + font-family: var(--font-heading), sans-serif; + font-size: 1.35rem; + letter-spacing: -0.02em; + color: var(--fg); +} + +.platformHeader p { + margin: 0; + color: var(--fg-muted); + font-size: 0.9rem; + line-height: 1.55; +} + +.choices { + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.choice { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + min-height: 48px; + padding: 0.7rem 0.9rem; + border: 1px solid var(--line); + border-radius: var(--r-md); + background: var(--surface); + color: var(--fg); + text-decoration: none; + transition: + border-color 0.2s, + background 0.2s; +} + +@media (hover: hover) and (pointer: fine) { + .choice:hover { + border-color: var(--primary); + background: var(--secondary-surface); + } +} + +.choiceRecommended { + border-color: var(--primary); + background: color-mix(in srgb, var(--primary) 10%, var(--surface)); +} + +.choiceArch { + font-size: 0.92rem; + font-weight: 600; +} + +.choiceFormat { + color: var(--fg-faint); + font-family: var(--font-geist-mono), monospace; + font-size: 0.78rem; +} + +.alternative { + margin: -0.35rem 0 0; + color: var(--fg-faint); + font-size: 0.82rem; +} + +.alternative a { + color: var(--primary); + text-decoration: underline; + text-underline-offset: 2px; +} + +.platformNote { + margin: auto 0 0; + color: var(--fg-faint); + font-size: 0.78rem; + line-height: 1.55; +} + +/* ── Release note ── */ + +.releaseNote { + max-width: 46rem; + margin: 2.5rem auto 0; + padding-top: 1.75rem; + border-top: 1px solid var(--line); + text-align: center; + color: var(--fg-faint); + font-size: 0.86rem; + line-height: 1.65; +} + +.releaseNote p { + margin: 0.4rem 0; +} + +.releaseNote a { + color: var(--primary); + text-decoration: underline; + text-underline-offset: 2px; +} + +@media (max-width: 900px) { + .platforms { + grid-template-columns: 1fr; + } + + .main { + padding-top: 2.5rem; + } +} + +@media (max-width: 560px) { + .main { + padding-inline: 1rem; + } + + .headline { + font-size: clamp(2.1rem, 10vw, 2.8rem); + } + + .primary { + width: 100%; + justify-content: center; + } + + .platform { + padding: 1.1rem; + } +} diff --git a/web/app/download/page.tsx b/web/app/download/page.tsx new file mode 100644 index 00000000..d7114fbc --- /dev/null +++ b/web/app/download/page.tsx @@ -0,0 +1,180 @@ +import type { Metadata } from 'next'; +import { headers } from 'next/headers'; + +import { cn } from '../../lib/utils'; +import { SiteFooter } from '../../components/SiteFooter'; +import { SiteNav } from '../../components/SiteNav'; +import { + LINUX_TARBALLS, + detectDesktopDownload, + downloadLabel, + downloadsFor, + platformName, + type DesktopPlatform, +} from '../../lib/desktop-downloads'; +import { defaultOgImage } from '../../lib/og-meta'; +import { absoluteUrl, SITE_NAME } from '../../lib/site'; +import s from './download.module.css'; + +// The recommendation comes from the request's User-Agent, so each visitor +// gets their platform's download in the first response. +export const dynamic = 'force-dynamic'; + +const TITLE = 'Download Agent Relay for macOS, Windows, and Linux'; +const DESCRIPTION = + 'Download Agent Relay for macOS, Windows, or Linux. The same local relay for Claude Code, Codex, and your other agents on every desktop platform.'; + +export const metadata: Metadata = { + title: TITLE, + description: DESCRIPTION, + alternates: { + canonical: absoluteUrl('/download'), + }, + openGraph: { + siteName: SITE_NAME, + title: TITLE, + description: DESCRIPTION, + url: absoluteUrl('/download'), + type: 'website', + images: [defaultOgImage()], + }, + twitter: { + card: 'summary_large_image', + title: TITLE, + description: DESCRIPTION, + images: [defaultOgImage().url], + }, +}; + +const platforms: Array<{ + id: DesktopPlatform; + name: string; + blurb: string; + note: string; +}> = [ + { + id: 'macos', + name: 'macOS', + blurb: 'For Apple silicon and Intel Macs.', + note: 'Requires macOS 13 or later. Open the disk image and drag Agent Relay to Applications.', + }, + { + id: 'windows', + name: 'Windows', + blurb: 'For Intel, AMD, and Arm PCs.', + note: 'Windows 10 and 11. Unpack the .zip and run agent-relay.exe. Releases are not code-signed yet, so SmartScreen may ask once; Smart App Control must be off.', + }, + { + id: 'linux', + name: 'Linux', + blurb: 'For x64 and arm64 distributions.', + note: 'Install the .deb with your package manager, or use a tarball if you prefer a relocatable install.', + }, +]; + +export default async function DownloadPage() { + const requestHeaders = await headers(); + const detected = detectDesktopDownload({ + userAgent: requestHeaders.get('user-agent') ?? '', + platform: requestHeaders.get('sec-ch-ua-platform'), + architecture: requestHeaders.get('sec-ch-ua-arch'), + }); + + return ( +
+ + +
+
+

Agent Relay Desktop

+

Download Agent Relay

+

+ The same window for your local agents on macOS, Windows, and Linux. Sign in once and your + sessions, agents, and messages are there. +

+ + {detected ? ( +
+

+ Detected {platformName(detected.platform)} + {detected.arch === 'arm64' ? ' on Arm' : ''} +

+ + {downloadLabel(detected)} + +

{detected.installNote}

+ {detected.platform === 'windows' ? ( +

+ Windows builds are not code-signed yet: SmartScreen may show Windows protected your PC{' '} + once. Choose More info → Run anyway. +

+ ) : null} +
+ ) : ( +

Choose the download for your computer.

+ )} +
+ +
+ {platforms.map((platform) => { + const items = downloadsFor(platform.id); + const isRecommended = detected?.platform === platform.id; + + return ( +
+
+

{platform.name}

+

{platform.blurb}

+
+ +
+ {items.map((item) => { + const isRecommendedChoice = + detected?.platform === item.platform && detected.arch === item.arch; + + return ( + + {item.archLabel} + .{item.format} + + ); + })} +
+ + {platform.id === 'linux' ? ( +

+ Prefer a tarball? x64 or arm64 +

+ ) : null} + +

{platform.note}

+
+ ); + })} +
+ +
+

+ Every link points at the latest release, so it always serves the newest version. Each release also + publishes version-pinned files and a SHA-256 checksum for every download. +

+

+ + See the latest release, checksums, and version history + +

+
+
+ + +
+ ); +} diff --git a/web/app/sitemap.ts b/web/app/sitemap.ts index 21b8901c..27404a28 100644 --- a/web/app/sitemap.ts +++ b/web/app/sitemap.ts @@ -21,6 +21,12 @@ export default function sitemap(): MetadataRoute.Sitemap { changeFrequency: 'weekly', priority: 1.0, }, + { + url: absoluteUrl('/download'), + lastModified: now, + changeFrequency: 'weekly', + priority: 0.9, + }, { url: absoluteUrl('/pear'), lastModified: now, diff --git a/web/components/SiteFooter.tsx b/web/components/SiteFooter.tsx index 39c34ba5..d30b63b0 100644 --- a/web/components/SiteFooter.tsx +++ b/web/components/SiteFooter.tsx @@ -21,6 +21,7 @@ const footerColumns: FooterColumn[] = [ title: 'Products', links: [ { label: 'Relay Cloud', href: 'https://agentrelay.com/cloud' }, + { label: 'Download', href: '/download' }, { label: 'Flows', href: '/flows' }, { label: 'Skip', href: 'https://heyskip.dev' }, { label: 'Pear', href: '/pear' }, diff --git a/web/components/SiteNav.tsx b/web/components/SiteNav.tsx index b91f0453..e49f3f69 100644 --- a/web/components/SiteNav.tsx +++ b/web/components/SiteNav.tsx @@ -101,6 +101,11 @@ export function SiteNav({ Log in +
  • + + Download + +
  • Docs @@ -172,6 +177,11 @@ export function SiteNav({ Log in )} + {!hideLinks && ( + setMenuOpen(false)}> + Download + + )} {!hideLinks && !hideMobileDocsLink && ( setMenuOpen(false)}> Docs diff --git a/web/lib/desktop-downloads.ts b/web/lib/desktop-downloads.ts new file mode 100644 index 00000000..17382dd6 --- /dev/null +++ b/web/lib/desktop-downloads.ts @@ -0,0 +1,167 @@ +const RELEASES = 'https://github.com/AgentWorkforce/relay-desktop-releases/releases/latest/download'; + +export type DesktopPlatform = 'macos' | 'windows' | 'linux'; +export type DesktopArch = 'arm64' | 'x64'; +export type DesktopDownloadFormat = 'dmg' | 'zip' | 'deb'; + +export type DesktopDownload = { + platform: DesktopPlatform; + arch: DesktopArch; + format: DesktopDownloadFormat; + filename: string; + url: string; + /** Short architecture name for a button, such as "Apple silicon" or "x64". */ + archLabel: string; + /** What to do with the file once it arrives. */ + installNote: string; +}; + +const download = ( + platform: DesktopPlatform, + arch: DesktopArch, + format: DesktopDownloadFormat, + filename: string, + archLabel: string, + installNote: string, +): DesktopDownload => ({ + platform, + arch, + format, + filename, + url: `${RELEASES}/${filename}`, + archLabel, + installNote, +}); + +/** + * Every download link the website offers, one entry per platform and + * architecture. The stable asset names are published by relay-desktop's + * Release workflow, so these URLs follow a new release without a website + * deploy. + */ +export const DESKTOP_DOWNLOADS: readonly DesktopDownload[] = [ + download( + 'macos', + 'arm64', + 'dmg', + 'AgentRelay-macOS-arm64.dmg', + 'Apple silicon', + 'Open the disk image and drag Agent Relay to Applications.', + ), + download( + 'macos', + 'x64', + 'dmg', + 'AgentRelay-macOS-x64.dmg', + 'Intel', + 'Open the disk image and drag Agent Relay to Applications.', + ), + download( + 'windows', + 'x64', + 'zip', + 'AgentRelay-Windows-x64.zip', + 'x64', + 'Unpack the .zip and run agent-relay.exe.', + ), + download( + 'windows', + 'arm64', + 'zip', + 'AgentRelay-Windows-arm64.zip', + 'Arm64', + 'Unpack the .zip and run agent-relay.exe.', + ), + download( + 'linux', + 'x64', + 'deb', + 'AgentRelay-Linux-x64.deb', + 'x64', + 'Install the .deb with your package manager.', + ), + download( + 'linux', + 'arm64', + 'deb', + 'AgentRelay-Linux-arm64.deb', + 'arm64', + 'Install the .deb with your package manager.', + ), +]; + +export const LINUX_TARBALLS: Record = { + x64: `${RELEASES}/AgentRelay-Linux-x64.tar.gz`, + arm64: `${RELEASES}/AgentRelay-Linux-arm64.tar.gz`, +}; + +export function platformName(platform: DesktopPlatform): string { + if (platform === 'macos') return 'macOS'; + if (platform === 'windows') return 'Windows'; + return 'Linux'; +} + +export function downloadsFor(platform: DesktopPlatform): DesktopDownload[] { + return DESKTOP_DOWNLOADS.filter((item) => item.platform === platform); +} + +export function downloadFor(platform: DesktopPlatform, arch: DesktopArch): DesktopDownload { + const match = DESKTOP_DOWNLOADS.find((item) => item.platform === platform && item.arch === arch); + if (!match) throw new Error(`No ${platform} ${arch} download`); + return match; +} + +export function downloadLabel(item: DesktopDownload): string { + return `Download for ${platformName(item.platform)} (${item.archLabel})`; +} + +export type DesktopDetectInput = { + userAgent: string; + /** `navigator.platform`, or the quoted `Sec-CH-UA-Platform` request header. */ + platform?: string | null; + /** `Sec-CH-UA-Arch`, when a browser sends it. */ + architecture?: string | null; +}; + +/** + * Work out which download to put first. Runs on the server from the request's + * User-Agent (and Client Hint headers), so Windows visitors get the Windows + * build in the first response instead of a Mac download that swaps after + * hydration. When the platform is unclear, the page shows every option. + */ +export function detectDesktopDownload(input: DesktopDetectInput): DesktopDownload | null { + const userAgent = input.userAgent ?? ''; + const platformHint = (input.platform ?? '').replaceAll('"', '').toLowerCase(); + + // Phones, tablets, and ChromeOS are not desktop installs. + if (/iphone|ipad|ipod|android|windows phone|mobile/i.test(userAgent) || /cros/i.test(userAgent)) { + return null; + } + + let platform: DesktopPlatform | null = null; + if (platformHint.includes('windows') || /windows|win32|win64/i.test(userAgent)) { + platform = 'windows'; + } else if (platformHint.includes('mac') || /macintosh|mac os x/i.test(userAgent)) { + platform = 'macos'; + } else if (platformHint.includes('linux') || /linux|x11/i.test(userAgent)) { + platform = 'linux'; + } + + if (!platform) return null; + + return downloadFor(platform, detectArch(platform, userAgent, input.architecture)); +} + +function detectArch( + platform: DesktopPlatform, + userAgent: string, + architecture: string | null | undefined, +): DesktopArch { + const hint = (architecture ?? '').replaceAll('"', '').toLowerCase(); + if (hint === 'arm' || /arm64|aarch64|armv8/i.test(userAgent)) return 'arm64'; + if (hint === 'x86' || /x86_64|amd64|win64/i.test(userAgent)) return 'x64'; + + // Safari does not expose the Mac's architecture. Apple silicon is the + // common case; Intel Macs can still choose Intel from the options below. + return platform === 'macos' ? 'arm64' : 'x64'; +} diff --git a/web/lib/test/desktop-downloads.test.ts b/web/lib/test/desktop-downloads.test.ts new file mode 100644 index 00000000..43a81271 --- /dev/null +++ b/web/lib/test/desktop-downloads.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest'; + +import { + LINUX_TARBALLS, + downloadFor, + detectDesktopDownload, + downloadLabel, +} from '../desktop-downloads'; + +const WINDOWS_X64 = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'; +const WINDOWS_ARM64 = + 'Mozilla/5.0 (Windows NT 10.0; Win64; ARM64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'; +const MAC_APPLE_SILICON = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15'; +const LINUX_X64 = + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'; + +describe('desktop downloads', () => { + it('detects Windows and recommends the Windows zip, not the Mac disk image', () => { + const detected = detectDesktopDownload({ userAgent: WINDOWS_X64 }); + + expect(detected?.platform).toBe('windows'); + expect(detected?.arch).toBe('x64'); + expect(detected?.filename).toBe('AgentRelay-Windows-x64.zip'); + expect(detected?.url).toBe( + 'https://github.com/AgentWorkforce/relay-desktop-releases/releases/latest/download/AgentRelay-Windows-x64.zip', + ); + expect(downloadLabel(detected!)).toBe('Download for Windows (x64)'); + }); + + it('uses Client Hints or the user agent to pick Windows on Arm', () => { + expect( + detectDesktopDownload({ userAgent: WINDOWS_X64, architecture: '"arm"' })?.filename, + ).toBe('AgentRelay-Windows-arm64.zip'); + expect(detectDesktopDownload({ userAgent: WINDOWS_ARM64 })?.filename).toBe( + 'AgentRelay-Windows-arm64.zip', + ); + }); + + it('uses the Sec-CH-UA-Platform header when the user agent is reduced', () => { + expect( + detectDesktopDownload({ userAgent: 'Mozilla/5.0', platform: '"Windows"' })?.platform, + ).toBe('windows'); + expect( + detectDesktopDownload({ userAgent: 'Mozilla/5.0', platform: '"macOS"' })?.platform, + ).toBe('macos'); + }); + + it('defaults Macs to Apple silicon and honors an Intel Client Hint', () => { + expect(detectDesktopDownload({ userAgent: MAC_APPLE_SILICON })?.filename).toBe( + 'AgentRelay-macOS-arm64.dmg', + ); + expect( + detectDesktopDownload({ userAgent: MAC_APPLE_SILICON, architecture: '"x86"' })?.filename, + ).toBe('AgentRelay-macOS-x64.dmg'); + }); + + it('detects Linux and links both package formats', () => { + expect(detectDesktopDownload({ userAgent: LINUX_X64 })?.filename).toBe('AgentRelay-Linux-x64.deb'); + expect(LINUX_TARBALLS.x64).toMatch(/AgentRelay-Linux-x64\.tar\.gz$/); + expect(LINUX_TARBALLS.arm64).toMatch(/AgentRelay-Linux-arm64\.tar\.gz$/); + }); + + it('offers every option when the platform is unclear or not a desktop', () => { + expect(detectDesktopDownload({ userAgent: 'curl/8.0' })).toBeNull(); + expect( + detectDesktopDownload({ + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1', + }), + ).toBeNull(); + expect( + detectDesktopDownload({ + userAgent: + 'Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36', + }), + ).toBeNull(); + expect(downloadFor('windows', 'arm64').format).toBe('zip'); + }); +}); From f350ca78f700f607d5b1165c0cb2f5121dbc9003 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Thu, 22 Oct 2026 00:11:54 -0700 Subject: [PATCH 2/2] Link the Windows installer instead of the portable zip v2026.10.55 publishes AgentRelay-Windows-{x64,arm64}-Setup.exe, the per-user installer the release notes recommend, so point the primary Windows download at it. The zips stay as the portable alternative, matched by the existing tarball treatment on the Linux card. --- web/app/download/page.tsx | 12 ++++++++++-- web/lib/desktop-downloads.ts | 23 ++++++++++++++++------- web/lib/test/desktop-downloads.test.ts | 17 ++++++++++------- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/web/app/download/page.tsx b/web/app/download/page.tsx index d7114fbc..e9c452a7 100644 --- a/web/app/download/page.tsx +++ b/web/app/download/page.tsx @@ -6,6 +6,7 @@ import { SiteFooter } from '../../components/SiteFooter'; import { SiteNav } from '../../components/SiteNav'; import { LINUX_TARBALLS, + WINDOWS_PORTABLE, detectDesktopDownload, downloadLabel, downloadsFor, @@ -62,7 +63,7 @@ const platforms: Array<{ id: 'windows', name: 'Windows', blurb: 'For Intel, AMD, and Arm PCs.', - note: 'Windows 10 and 11. Unpack the .zip and run agent-relay.exe. Releases are not code-signed yet, so SmartScreen may ask once; Smart App Control must be off.', + note: 'Windows 10 and 11. Run the installer: it installs for your account without administrator rights. Releases are not code-signed yet, so SmartScreen may ask once; Smart App Control must be off.', }, { id: 'linux', @@ -105,7 +106,7 @@ export default async function DownloadPage() {

    {detected.installNote}

    {detected.platform === 'windows' ? (

    - Windows builds are not code-signed yet: SmartScreen may show Windows protected your PC{' '} + The Windows installer is not code-signed yet: SmartScreen may show Windows protected your PC{' '} once. Choose More info → Run anyway.

    ) : null} @@ -149,6 +150,13 @@ export default async function DownloadPage() { })} + {platform.id === 'windows' ? ( +

    + Prefer portable? x64 .zip or{' '} + arm64 .zip +

    + ) : null} + {platform.id === 'linux' ? (

    Prefer a tarball? x64 or arm64 diff --git a/web/lib/desktop-downloads.ts b/web/lib/desktop-downloads.ts index 17382dd6..d0216847 100644 --- a/web/lib/desktop-downloads.ts +++ b/web/lib/desktop-downloads.ts @@ -2,7 +2,7 @@ const RELEASES = 'https://github.com/AgentWorkforce/relay-desktop-releases/relea export type DesktopPlatform = 'macos' | 'windows' | 'linux'; export type DesktopArch = 'arm64' | 'x64'; -export type DesktopDownloadFormat = 'dmg' | 'zip' | 'deb'; +export type DesktopDownloadFormat = 'dmg' | 'exe' | 'zip' | 'deb'; export type DesktopDownload = { platform: DesktopPlatform; @@ -59,18 +59,18 @@ export const DESKTOP_DOWNLOADS: readonly DesktopDownload[] = [ download( 'windows', 'x64', - 'zip', - 'AgentRelay-Windows-x64.zip', + 'exe', + 'AgentRelay-Windows-x64-Setup.exe', 'x64', - 'Unpack the .zip and run agent-relay.exe.', + 'Run the installer. It installs for your account without administrator rights.', ), download( 'windows', 'arm64', - 'zip', - 'AgentRelay-Windows-arm64.zip', + 'exe', + 'AgentRelay-Windows-arm64-Setup.exe', 'Arm64', - 'Unpack the .zip and run agent-relay.exe.', + 'Run the installer. It installs for your account without administrator rights.', ), download( 'linux', @@ -90,6 +90,15 @@ export const DESKTOP_DOWNLOADS: readonly DesktopDownload[] = [ ), ]; +/** + * The zips are the portable alternative to the Windows installer: unpack one + * and run agent-relay.exe without installing anything. + */ +export const WINDOWS_PORTABLE: Record = { + x64: `${RELEASES}/AgentRelay-Windows-x64.zip`, + arm64: `${RELEASES}/AgentRelay-Windows-arm64.zip`, +}; + export const LINUX_TARBALLS: Record = { x64: `${RELEASES}/AgentRelay-Linux-x64.tar.gz`, arm64: `${RELEASES}/AgentRelay-Linux-arm64.tar.gz`, diff --git a/web/lib/test/desktop-downloads.test.ts b/web/lib/test/desktop-downloads.test.ts index 43a81271..0439a0fc 100644 --- a/web/lib/test/desktop-downloads.test.ts +++ b/web/lib/test/desktop-downloads.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { LINUX_TARBALLS, + WINDOWS_PORTABLE, downloadFor, detectDesktopDownload, downloadLabel, @@ -17,24 +18,24 @@ const LINUX_X64 = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'; describe('desktop downloads', () => { - it('detects Windows and recommends the Windows zip, not the Mac disk image', () => { + it('detects Windows and recommends the Windows installer, not the Mac disk image', () => { const detected = detectDesktopDownload({ userAgent: WINDOWS_X64 }); expect(detected?.platform).toBe('windows'); expect(detected?.arch).toBe('x64'); - expect(detected?.filename).toBe('AgentRelay-Windows-x64.zip'); + expect(detected?.filename).toBe('AgentRelay-Windows-x64-Setup.exe'); expect(detected?.url).toBe( - 'https://github.com/AgentWorkforce/relay-desktop-releases/releases/latest/download/AgentRelay-Windows-x64.zip', + 'https://github.com/AgentWorkforce/relay-desktop-releases/releases/latest/download/AgentRelay-Windows-x64-Setup.exe', ); expect(downloadLabel(detected!)).toBe('Download for Windows (x64)'); }); - it('uses Client Hints or the user agent to pick Windows on Arm', () => { + it('uses Client Hints or the user agent to pick the Arm installer', () => { expect( detectDesktopDownload({ userAgent: WINDOWS_X64, architecture: '"arm"' })?.filename, - ).toBe('AgentRelay-Windows-arm64.zip'); + ).toBe('AgentRelay-Windows-arm64-Setup.exe'); expect(detectDesktopDownload({ userAgent: WINDOWS_ARM64 })?.filename).toBe( - 'AgentRelay-Windows-arm64.zip', + 'AgentRelay-Windows-arm64-Setup.exe', ); }); @@ -76,6 +77,8 @@ describe('desktop downloads', () => { 'Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36', }), ).toBeNull(); - expect(downloadFor('windows', 'arm64').format).toBe('zip'); + expect(downloadFor('windows', 'arm64').format).toBe('exe'); + expect(WINDOWS_PORTABLE.x64).toMatch(/AgentRelay-Windows-x64\.zip$/); + expect(WINDOWS_PORTABLE.arm64).toMatch(/AgentRelay-Windows-arm64\.zip$/); }); });