From e24a499008b24c3ecbbfb4b55ae7d0c90584f936 Mon Sep 17 00:00:00 2001 From: stuffbucket <231133237+stuffbucket@users.noreply.github.com> Date: Wed, 24 Jun 2026 03:56:15 -0700 Subject: [PATCH 1/7] design(site): god-rays backdrop + brand-red hero + OS-aware download - Add a subtle WebGL god-rays background (inlined GLSL; no Three.js / no shader dependency). Dark mode only; light mode keeps the warm-paper background. Brand jewel tones (turquoise -> indigo -> magenta), light source positioned behind the hero wordmark, prefers-reduced-motion renders a single frame, paused on hidden tab, DPR-capped. - Hero is now a solid brand-red card (drops the per-section top accent rule), cream ink. - Hero download button detects the OS: Windows -> inert "coming soon" chip; macOS/other -> the macOS download (server-rendered default, no-JS safe). --- site/src/components/GodRays.astro | 220 +++++++++++++++++++++++++ site/src/components/markdoc/Hero.astro | 38 ++++- site/src/pages/index.astro | 4 + 3 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 site/src/components/GodRays.astro diff --git a/site/src/components/GodRays.astro b/site/src/components/GodRays.astro new file mode 100644 index 0000000..bdcf3b1 --- /dev/null +++ b/site/src/components/GodRays.astro @@ -0,0 +1,220 @@ +--- +// God rays — a subtle volumetric light-shaft backdrop. +// +// - Inlined GLSL (no third-party shader library) + raw WebGL (no Three.js) to +// keep the page lean. +// - DARK MODE ONLY: light shafts read on the black dark-theme background; on the +// light (warm paper) theme the canvas is hidden via CSS and the render loop +// stays paused. +// - Reduced motion is a contract: under prefers-reduced-motion we draw a single +// static frame, never an animation loop. +// - Pauses when the tab is hidden. pointer-events:none, z-index 0 — it sits +// behind every card, peeking through the card gaps and the page margins. +--- + + + + + + diff --git a/site/src/components/markdoc/Hero.astro b/site/src/components/markdoc/Hero.astro index 54ccc9c..4a9bff0 100644 --- a/site/src/components/markdoc/Hero.astro +++ b/site/src/components/markdoc/Hero.astro @@ -46,6 +46,21 @@ const { macDmg: downloadUrl, releasesUrl } = await getDownloadInfo(); diff --git a/site/src/components/Spark.astro b/site/src/components/Spark.astro new file mode 100644 index 0000000..d413681 --- /dev/null +++ b/site/src/components/Spark.astro @@ -0,0 +1,34 @@ +--- +// A chromed spark icon: a Paint canvas in "chrome" mode (polished-metal GLSL) +// masked to the four-point spark shape, so the star itself is a glossy metal +// reflection rather than a flat fill. Used on primary buttons. +import Paint from "./Paint.astro"; +interface Props { + class?: string; +} +const { class: cls = "" } = Astro.props; +--- + + + + diff --git a/site/src/components/markdoc/GetStarted.astro b/site/src/components/markdoc/GetStarted.astro index 9ce2eee..7b194eb 100644 --- a/site/src/components/markdoc/GetStarted.astro +++ b/site/src/components/markdoc/GetStarted.astro @@ -2,15 +2,18 @@ // Get started in 3 steps. Step 1 carries the actual download (macOS live, // Windows "coming soon"); the standalone download section folded in here. // Homebrew + other install options live in the repo README. +import Paint from "../Paint.astro"; +import Spark from "../Spark.astro"; import { getDownloadInfo } from "../../lib/downloads"; const { macDmg, versionLabel, releasesUrl, repoUrl } = await getDownloadInfo(); const readmeUrl = `${repoUrl}#readme`; +const M = `maximal`; const steps = [ { title: "Install maximal", - body: "Grab the macOS app — it lives quietly in your menu bar. Windows is coming soon.", + body: "Grab the macOS app. It lives quietly in your menu bar; Windows is coming soon.", }, { title: "Sign in with GitHub", @@ -18,16 +21,14 @@ const steps = [ }, { title: "Pick your tools", - body: "Claude Code, Codex, opencode, Cursor — point them at maximal and they run on your Copilot models. Switch anytime; no re-signing-in.", + body: `Point Claude Code, Codex, opencode, or GitHub Copilot at ${M} and they run on your Copilot models. Switch anytime; no re-signing-in.`, }, ]; --- -
+
+
-

Get started

Up and running in three steps.

@@ -55,15 +56,9 @@ const steps = [ href={macDmg} rel="noopener" > + - + macOS {versionLabel && {versionLabel}} @@ -112,16 +107,9 @@ const steps = [ margin-bottom: 1.75rem; } - .getstarted__spark { - width: 1.25rem; - height: 1.25rem; - fill: var(--accent-gold); - align-self: center; - } - .getstarted__sub { font-family: var(--sans); - color: var(--text-muted); + color: var(--text); margin: 0; flex-basis: 100%; } @@ -167,7 +155,7 @@ const steps = [ .step__text { font-family: var(--sans); - color: var(--text-muted); + color: var(--text); line-height: 1.55; margin: 0; } @@ -179,7 +167,7 @@ const steps = [ .step__readme { margin: 0.7rem 0 0; font-size: 0.85rem; - color: var(--text-muted); + color: var(--text); } @media (max-width: 640px) { diff --git a/site/src/components/markdoc/Hero.astro b/site/src/components/markdoc/Hero.astro index 4a9bff0..f437485 100644 --- a/site/src/components/markdoc/Hero.astro +++ b/site/src/components/markdoc/Hero.astro @@ -3,6 +3,8 @@ // typed in on load. The full tagline is server-rendered (SEO + no-JS + // prefers-reduced-motion all get the complete sentence); JS only replays it as // a typing effect when motion is allowed. +import Paint from "../Paint.astro"; +import Spark from "../Spark.astro"; import { getDownloadInfo } from "../../lib/downloads"; interface Props { tagline: string; @@ -11,10 +13,11 @@ interface Props { const { tagline, downloadLabel = "Download for macOS" } = Astro.props; // Markdoc doesn't forward $variables into tag attributes, so pull the resolved // release straight from the shared (memoized) build-time module. -const { macDmg: downloadUrl, releasesUrl } = await getDownloadInfo(); +const { macDmg: downloadUrl, versionLabel } = await getDownloadInfo(); ---
+

maximal

{tagline} + - + {downloadLabel} - - ) - } - { - releasesUrl && ( - - Other ways to install + {versionLabel && {versionLabel}} ) } @@ -56,9 +52,28 @@ const { macDmg: downloadUrl, releasesUrl } = await getDownloadInfo(); .hero::before { display: none; } + /* Lift the hero content above the paint canvas (which is positioned, so it + would otherwise paint over the non-positioned content). A soft shadow keeps + the cream type crisp over the moving gloss highlights. */ .wordmark, + .tagline, + .hero-cta { + position: relative; + z-index: 1; + } + /* Cream type, shaded as if lit from above the card: a top-bright gradient + fill on the wordmark + a soft shadow cast down onto the paint. */ + .wordmark { + background: linear-gradient(180deg, #fcf5e6 0%, #f1e5cb 55%, #d6c4a0 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; + filter: drop-shadow(0 5px 9px rgba(26, 0, 4, 0.5)); + } .tagline { color: #f4ead4; + text-shadow: 0 2px 5px rgba(26, 0, 4, 0.45); } .hero-typed { @@ -83,20 +98,10 @@ const { macDmg: downloadUrl, releasesUrl } = await getDownloadInfo(); margin: 1.5rem 0 1.25rem; } .hero-cta__primary { - /* Let the primary CTA size to its content rather than stretch. */ + /* Size to content rather than stretch. The glossy primary-button effect + (clear-coat Paint + lit label) is shared from global.css .btn--primary. */ flex: 0 0 auto; } - .hero-cta__secondary { - font-family: var(--mono); - font-size: 0.9rem; - color: #f4ead4; - text-decoration: underline; - text-underline-offset: 3px; - text-decoration-color: var(--ink-gold); - } - .hero-cta__secondary:hover { - text-decoration-thickness: 2px; - } @keyframes hero-caret-blink { 50% { @@ -148,7 +153,11 @@ const { macDmg: downloadUrl, releasesUrl } = await getDownloadInfo(); btn.classList.add("btn--soon"); btn.removeAttribute("href"); btn.setAttribute("aria-disabled", "true"); + var paint = btn.querySelector(".btn-paint"); + if (paint) paint.style.display = "none"; var label = btn.querySelector(".btn-label span:last-child"); - if (label) label.textContent = "Windows — coming soon"; + if (label) label.textContent = "Windows"; + var meta = btn.querySelector(".btn-meta"); + if (meta) meta.textContent = "coming soon"; })(); diff --git a/site/src/components/markdoc/ShowTell.astro b/site/src/components/markdoc/ShowTell.astro index 0141cfd..bf83e40 100644 --- a/site/src/components/markdoc/ShowTell.astro +++ b/site/src/components/markdoc/ShowTell.astro @@ -2,6 +2,7 @@ // "Show, don't tell": the tools you use } maximal { your Copilot models. // Labels sit in a row ABOVE the braces; the tall braces wrap only the lists // (gather → route → fan out). Brand marks aid scanning. Presentational. +import Paint from "../Paint.astro"; // Brand marks from Simple Icons (CC0). `fill: true` = solid logo whose path // carries its own fill="currentColor"; `fill: false` = a monoline stroke glyph. @@ -61,15 +62,15 @@ const BRACE = `M5 1C10 1 10 6 10 16L10 42C10 47 12 50 15 50C12 50 10 53 10 58L10 const shot = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/screenshots/app-dashboard.png`; --- -

+
+

Why try maximal

- maximal runs on your own machine. The tools you already use point at - maximal, and it does the work of connecting them to the models you want — - all on the GitHub Copilot subscription you already pay for, with Copilot's - privacy and security guarantees. No hosted gateway to wire up, no per-token - bill to track — none of what a service like OpenRouter asks of you. + maximal swaps out the engine your AI tools use + with the models that power GitHub Copilot. You get all of the agility and + creativity without sacrificing the power, privacy, and security of GitHub + Copilot.

@@ -161,7 +162,7 @@ const shot = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/screenshots/app-das
maximal's usage dashboard — token usage broken down by model across your tools over the last 30 days
- Every tool, every model, every token — see exactly where your Copilot + Every tool, every model, every token. See exactly where your Copilot subscription goes.
@@ -193,14 +194,17 @@ const shot = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/screenshots/app-das } /* Two rows: labels on top (row 1), then the braced lists + node (row 2), - so the braces wrap only the lists — not the labels. */ + so the braces wrap only the lists — not the labels. Columns are content- + sized and the whole group is centered, so each list hugs its brace and the + left/right blocks stay symmetric around the node (no stranded whitespace). */ .flow { display: grid; - grid-template-columns: minmax(0, 1fr) 22px auto 22px minmax(0, 1fr); + grid-template-columns: auto 24px auto 24px auto; grid-template-rows: auto 1fr; + justify-content: center; align-items: stretch; - column-gap: 1.25rem; - row-gap: 1rem; + column-gap: 1.5rem; + row-gap: 1.1rem; margin-top: 1.75rem; } @@ -213,6 +217,12 @@ const shot = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/screenshots/app-das color: var(--text); margin: 0; align-self: end; + justify-self: start; + } + /* The models label hugs the right block's left edge so it sits above its + first list item, mirroring the tools label on the left. */ + .flow__label--models { + justify-self: start; } .flow__label--tools { grid-column: 1; @@ -311,9 +321,14 @@ const shot = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/screenshots/app-das } .flow__mark { + /* Match the hero wordmark's cut exactly (weight + variable-font axes + + tracking), just smaller — so the island "maximal" reads as the same + wordmark, not a different font. */ font-family: var(--serif); - font-size: 2.2rem; - font-weight: 600; + font-size: 2.4rem; + font-weight: 900; + font-variation-settings: "SOFT" 30, "WONK" 1, "opsz" 144; + letter-spacing: -0.04em; color: var(--text); line-height: 1; margin: 0; diff --git a/site/src/content/landing/index.mdoc b/site/src/content/landing/index.mdoc index 50e2b55..26ea4d3 100644 --- a/site/src/content/landing/index.mdoc +++ b/site/src/content/landing/index.mdoc @@ -1,5 +1,5 @@ {% hero - tagline="Connect the AI tools you use to the models in your GitHub Copilot subscription." + tagline="Connect the AI tools you use to the models in GitHub Copilot." downloadLabel="Download for macOS" /%} {% showtell /%} diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index fd6daa4..5755615 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -28,10 +28,10 @@ const { Content } = await render(entry); href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght,SOFT,WONK@9..144,400..900,0..100,0..1&display=swap" /> - maximal — your AI tools, your Copilot models, one connection + maximal · your AI tools, your Copilot models, one connection - - @@ -116,5 +100,27 @@ const { Content } = await render(entry); }); })(); + + diff --git a/site/src/styles/global.css b/site/src/styles/global.css index fdb4070..56be1f0 100644 --- a/site/src/styles/global.css +++ b/site/src/styles/global.css @@ -203,14 +203,41 @@ main > article { --card-accent: var(--brand-ochre); } -/* Footer reads as a quiet base, not a section. */ -.card--console { - background: var(--surface); - color: var(--text-muted); +/* ===== Painted cards ===== + Cards that carry the candy-paint shader (a ) read as + saturated brand surfaces, like the hero. Drop the top accent rule, lift the + content above the canvas, and locally re-point the neutral text/border tokens + to a cream palette so copy stays legible on the deep brand base in both + themes. (--accent is left alone so the live download button keeps its fill.) */ +.card--painted { + --text: #f4ead4; + --text-muted: #d9ccad; + --border: rgb(244 234 212 / 0.18); + --border-strong: rgb(244 234 212 / 0.32); + --link: #f3d886; + --accent-gold: #f0d27a; + color: var(--text); + /* Subtle "lit from above" elevation so the cream copy lifts off the deep + brand base. Inherited by all descendant text. */ + text-shadow: 0 1px 2px rgb(8 16 22 / 0.38); } -.card--console::before { +.card--painted::before { display: none; } +.card--painted > .paint { + z-index: 0; +} +.card--painted > :not(.paint) { + position: relative; + z-index: 1; +} +/* Brand-tone fallback (and the silhouette behind the paint) if WebGL is off. */ +.card--turquoise.card--painted { + background: #1b6f7a; +} +.card--indigo.card--painted { + background: #43357f; +} /* ===== Hero ===== */ @@ -248,6 +275,16 @@ main > article { font-size: 1.19em; } +/* Inline brand wordmark for body copy: Fraunces bold at the running text size, + so "maximal" reads as the product name wherever it appears in a sentence. */ +.brandword { + font-family: var(--serif); + font-weight: 700; + font-style: normal; + font-variation-settings: "opsz" 40, "SOFT" 0, "WONK" 0; + letter-spacing: -0.01em; +} + .tagline { font-family: var(--serif); font-weight: 500; @@ -396,21 +433,103 @@ pre code { font-weight: 400; } +/* Primary button: one shared glossy clear-coat treatment used everywhere a + primary CTA appears (hero download, get-started download). A clear-coat Paint + canvas () sits behind a label that + is lit from above like the wordmark — top-bright gradient fill + soft cast + shadow. Fixed deep-teal base (matching the Paint) so the cream label keeps + contrast in both themes; the bg is also the no-WebGL fallback. */ .btn--primary { - background: var(--accent); - color: var(--accent-ink); - border-color: var(--accent); + position: relative; + overflow: hidden; + background: #15656f; + color: #fcf5e6; + border-color: #15656f; font-weight: 600; } .btn--primary:hover { - filter: brightness(1.06); - border-color: var(--accent); + filter: brightness(1.05); + border-color: #15656f; } +.btn--primary .btn-label, .btn--primary .btn-meta { - color: var(--accent-ink); - opacity: 0.8; + position: relative; + z-index: 1; +} + +/* Light the label text from above. The gradient can't tint the SVG (it uses + currentColor), so only the text span gets the clip; the spark stays solid. + A tight dark shadow plus a soft drop separates the cream label from the + glossy teal coat so it pops. */ +.btn--primary .btn-label > span:last-child { + background: linear-gradient(180deg, #ffffff 0%, #f2e6cb 58%, #d4c099 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; + filter: drop-shadow(0 1px 1px rgba(0, 14, 18, 0.7)) + drop-shadow(0 2px 5px rgba(0, 14, 18, 0.45)); +} + +.btn--primary .btn-spark { + color: #ffffff; + filter: drop-shadow(0 1px 1px rgba(0, 14, 18, 0.7)) + drop-shadow(0 2px 4px rgba(0, 14, 18, 0.4)); +} + +.btn--primary .btn-meta { + color: #f6efda; + opacity: 0.95; + filter: drop-shadow(0 1px 2px rgba(0, 14, 18, 0.5)); +} + +/* Click celebration: a soft light bloom from the click point + a tiny press. + Subtle and quick. JS sets --rx/--ry to the click coords and toggles + .btn--celebrate; skipped under prefers-reduced-motion (the JS bails). */ +.btn--primary::after { + content: ""; + position: absolute; + left: var(--rx, 50%); + top: var(--ry, 50%); + width: 10px; + height: 10px; + margin: -5px; + border-radius: 50%; + background: radial-gradient( + circle, + rgba(255, 255, 255, 0.6), + rgba(255, 255, 255, 0) + ); + transform: scale(0); + opacity: 0; + pointer-events: none; + z-index: 2; +} + +.btn--celebrate { + animation: btn-press 260ms ease; +} +.btn--celebrate::after { + animation: btn-bloom 560ms ease-out; +} + +@keyframes btn-press { + 40% { + transform: scale(0.975); + } +} + +@keyframes btn-bloom { + 0% { + transform: scale(0); + opacity: 0.7; + } + 100% { + transform: scale(20); + opacity: 0; + } } .btn-spark { @@ -452,23 +571,6 @@ pre code { pointer-events: none; } -/* ===== Footer ===== */ - -.site-footer { - display: flex; - flex-wrap: wrap; - gap: 0.5rem 1.5rem; - justify-content: space-between; - align-items: center; - font-size: 0.88rem; -} - -.site-footer a { - color: var(--link); - text-decoration: underline; - text-underline-offset: 3px; -} - /* ===== Bottom-pinned glass dock ===== */ .dock { From aba5850cedc8054d5475598c406e6453cd8ce3f1 Mon Sep 17 00:00:00 2001 From: stuffbucket <231133237+stuffbucket@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:20:08 -0700 Subject: [PATCH 6/7] fix(windows): detect MSIX/Store Claude Desktop install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Windows installs are MSIX, not Squirrel — they don't create %LOCALAPPDATA%\AnthropicClaude or always drop the WindowsApps alias, so the Apps card showed 'Not installed' on a real install. Add the known MSIX package dir (Packages\Claude_pzs8sxrjxfjjc) as a candidate and scan the Packages dir for the Claude_/AnthropicPBC.Claude family prefix (hashed family names mean no fixed path catches every install). macOS and the unknown-platform 'can't tell → don't block' path are untouched. --- src/configure-claude-desktop.ts | 51 +++++++++++++++++++++----- tests/configure-claude-desktop.test.ts | 38 +++++++++++++++++++ 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/configure-claude-desktop.ts b/src/configure-claude-desktop.ts index dba0e2e..1258ea6 100644 --- a/src/configure-claude-desktop.ts +++ b/src/configure-claude-desktop.ts @@ -46,12 +46,15 @@ const MANAGED_PROFILE_OUT = "maximal-claude-3p.mobileconfig" * Candidate Claude Desktop install locations to probe, per platform. * * - macOS: the app bundle in `/Applications`. - * - Windows: the per-user Squirrel install dir (`%LOCALAPPDATA%\\AnthropicClaude`) - * and the launcher alias the installer drops on PATH - * (`%LOCALAPPDATA%\\Microsoft\\WindowsApps\\Claude.exe`). Either present - * means Claude Desktop is installed. (The MSIX / Microsoft-Store build - * lives under `%LOCALAPPDATA%\\Packages\\Claude_*` — we don't enumerate - * that package-family dir here; `--force` covers it.) + * - Windows: the per-user Squirrel install dir (`%LOCALAPPDATA%\\AnthropicClaude`), + * the launcher alias the installer drops on PATH + * (`%LOCALAPPDATA%\\Microsoft\\WindowsApps\\Claude.exe`), and the MSIX / + * Microsoft-Store build's known package dir + * (`%LOCALAPPDATA%\\Packages\\Claude_pzs8sxrjxfjjc`). Any present means + * Claude Desktop is installed. New Windows installs are MSIX, which does + * NOT create the Squirrel dir or always drop the alias, so the Packages + * signal — plus the prefix scan in `claudeAppInstalled` — is what catches + * a modern install. * * Returns an empty list on unsupported platforms (caller treats that as * "can't tell" and only blocks on darwin/win32). @@ -62,16 +65,41 @@ function claudeAppCandidates( ): Array { if (platform === "darwin") return [CLAUDE_APP_PATH] if (platform === "win32") { - const localAppData = - process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local") + const localAppData = windowsLocalAppData(home) return [ path.join(localAppData, "AnthropicClaude"), path.join(localAppData, "Microsoft", "WindowsApps", "Claude.exe"), + path.join(localAppData, "Packages", "Claude_pzs8sxrjxfjjc"), ] } return [] } +/** `%LOCALAPPDATA%`, or its default location under `home` when unset. */ +function windowsLocalAppData(home: string): string { + return process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local") +} + +/** + * MSIX package family names mutate (`Claude_`, + * `AnthropicPBC.Claude_`), so an exact path can't catch every install. + * Scan `%LOCALAPPDATA%\\Packages` for any entry in the Claude family. No-ops + * (returns false) when the Packages dir is absent or unreadable. + */ +function windowsMsixClaudeInstalled(home: string): boolean { + const packages = path.join(windowsLocalAppData(home), "Packages") + try { + return fs + .readdirSync(packages) + .some( + (name) => + name.startsWith("Claude_") || name.startsWith("AnthropicPBC.Claude"), + ) + } catch { + return false + } +} + interface ConfigureOptions { force: boolean revert: boolean @@ -177,7 +205,7 @@ export function claudeAppInstalled( const candidates = claudeAppCandidates(platform, home) // Unsupported platform: nothing to probe → can't tell, don't block. if (candidates.length === 0) return true - return candidates.some((p) => { + const hasCandidate = candidates.some((p) => { try { // Accept either a directory (macOS .app bundle, Windows Squirrel dir) // or a file (Windows launcher alias) — any existing candidate counts. @@ -187,6 +215,11 @@ export function claudeAppInstalled( return false } }) + if (hasCandidate) return true + // Windows MSIX installs use a hashed package-family dir that no fixed path + // can pin down — scan the Packages dir for the Claude family as a fallback. + if (platform === "win32") return windowsMsixClaudeInstalled(home) + return false } export const configureClaudeDesktop = defineCommand({ diff --git a/tests/configure-claude-desktop.test.ts b/tests/configure-claude-desktop.test.ts index 4c37a50..8f30810 100644 --- a/tests/configure-claude-desktop.test.ts +++ b/tests/configure-claude-desktop.test.ts @@ -58,6 +58,44 @@ describe("claudeAppInstalled — Windows (platform injected)", () => { expect(claudeAppInstalled("win32", home)).toBe(true) }) + it("detects the MSIX package dir (Packages/Claude_pzs8sxrjxfjjc)", () => { + fs.mkdirSync( + path.join(home, "AppData", "Local", "Packages", "Claude_pzs8sxrjxfjjc"), + { recursive: true }, + ) + expect(claudeAppInstalled("win32", home)).toBe(true) + }) + + it("detects a Claude-family MSIX package by prefix (hashed family name)", () => { + // The publisher-hash suffix mutates between installs, so detection scans + // the Packages dir for the family prefix rather than an exact name. + fs.mkdirSync( + path.join( + home, + "AppData", + "Local", + "Packages", + "AnthropicPBC.Claude_1a2b3c4d5e6f7", + ), + { recursive: true }, + ) + expect(claudeAppInstalled("win32", home)).toBe(true) + }) + + it("does not false-positive on an unrelated Packages entry", () => { + fs.mkdirSync( + path.join( + home, + "AppData", + "Local", + "Packages", + "Microsoft.SomethingElse", + ), + { recursive: true }, + ) + expect(claudeAppInstalled("win32", home)).toBe(false) + }) + it("returns true on unsupported platforms (can't tell → don't block)", () => { expect(claudeAppInstalled("linux", home)).toBe(true) }) From 6fdb0fcb17d2202fb901852a1ed619a25702a094 Mon Sep 17 00:00:00 2001 From: stuffbucket <231133237+stuffbucket@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:20:19 -0700 Subject: [PATCH 7/7] fix(shell): show splash only once the webview has painted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows/WebView2 the native surface is presented before the compositor draws its first frame, so the transparent splash showed an empty outline for hundreds of ms–seconds before the brand-red fill popped in. Build the splash hidden and .show() it from on_page_load once the DOM reports Finished. macOS/WKWebView paints in lockstep with show so this fires immediately there — no regression. Also broaden the macOS Reopen handler: clicking the 'Maximal is running' banner (or the Dock icon) with no visible windows now opens Settings — account section when signed out, plain Settings otherwise. Desktop notifications can't carry a routable click (plugin show() is fire-and-forget), so Reopen is the lever on macOS. On Windows the plugin (2.3.3) exposes no toast-activation callback; tray left-click already opens Settings and the toast body points there. --- shell/src-tauri/src/lib.rs | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/shell/src-tauri/src/lib.rs b/shell/src-tauri/src/lib.rs index 034cf37..52e23e9 100644 --- a/shell/src-tauri/src/lib.rs +++ b/shell/src-tauri/src/lib.rs @@ -49,6 +49,7 @@ use tauri::{ ipc::Channel, menu::{IsMenuItem, Menu, MenuItem, PredefinedMenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, + webview::PageLoadEvent, AppHandle, Emitter, Manager, RunEvent, State, WebviewUrl, WebviewWindowBuilder, WindowEvent, }; @@ -660,21 +661,26 @@ pub fn run() { kill_sidecar(app_handle); } // macOS delivers Reopen when the app is re-activated — clicking its - // notification banner, its Dock icon, etc. Desktop notifications - // can't carry a routable button (the plugin's show() is - // fire-and-forget), so this is how the sign-in nudge's "click here" - // lands somewhere: if we're up but not signed in and nothing's on - // screen, bring up Settings → account. + // notification banner ("Maximal is running"), its Dock icon, etc. + // Desktop notifications can't carry a routable button (the plugin's + // show() is fire-and-forget), so Reopen is how a banner click lands + // somewhere: if nothing's on screen, open Settings. Route to the + // account section when we're up but not signed in (the sign-in + // nudge), otherwise plain Settings. #[cfg(target_os = "macos")] RunEvent::Reopen { has_visible_windows, .. } => { - if !has_visible_windows - && app_handle.state::().get() + if !has_visible_windows { + let section = if app_handle.state::().get() == SidecarState::RunningUnauthenticated - { - open_settings_window(app_handle, Some("account")); + { + Some("account") + } else { + None + }; + open_settings_window(app_handle, section); } } _ => {} @@ -1406,6 +1412,18 @@ fn create_splash(app: &AppHandle) { .transparent(true) .always_on_top(true) .skip_taskbar(true) + // Build hidden and only show once the webview reports the DOM has + // loaded. On Windows/WebView2 the native surface is presented before + // the compositor draws its first frame, so a visible-from-launch + // transparent window shows an empty outline for hundreds of ms–seconds + // before the brand-red `.splash` div pops in. macOS/WKWebView paints in + // lockstep with show, so this fires immediately there — no regression. + .visible(false) + .on_page_load(|window, payload| { + if payload.event() == PageLoadEvent::Finished { + let _ = window.show(); + } + }) .center() .build(); match result {