Skip to content
This repository was archived by the owner on Oct 10, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/desktop/src/wsl/DesktopWslEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
formatNodePtyProbeFailureReason,
formatWslShellTransportFailureReason,
parseNodePath,
parseNodeVersion,
parseResolvedEnv,
parseResolvedPath,
parseToolchainReport,
Expand Down Expand Up @@ -189,6 +190,29 @@ describe("parseNodePath", () => {
});
});

describe("parseNodeVersion", () => {
it("extracts the node version from a nodeVersion: line", () => {
expect(parseNodeVersion("nodeVersion:24.10.0")).toBe("24.10.0");
});

it("returns null when the version value is empty", () => {
expect(parseNodeVersion("nodeVersion:")).toBeNull();
});

it("returns null when there is no nodeVersion line at all", () => {
expect(parseNodeVersion("nodePath:/usr/bin/node\nresolvedPath:/usr/bin")).toBeNull();
});

it("ignores surrounding noise and trims whitespace", () => {
const stdout = [
"some preamble noise",
" nodeVersion:22.16.0 ",
"nodePath:/usr/bin/node",
].join("\n");
expect(parseNodeVersion(stdout)).toBe("22.16.0");
});
});

describe("parseResolvedPath", () => {
it("preserves spaces and apostrophes in the resolved login-shell PATH", () => {
const resolvedPath = "/home/test user/bin:/opt/test's tools/bin:/usr/bin:/bin";
Expand Down
28 changes: 27 additions & 1 deletion apps/desktop/src/wsl/DesktopWslEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ const NODE_PTY_PROBE_SCRIPT = (
linuxServerDir: string,
) => `node_path="$(command -v node 2>/dev/null)"
printf 'nodePath:%s\\n' "$node_path"
printf 'nodeVersion:%s\\n' "$(node -p 'process.versions.node' 2>/dev/null)"
printf 'resolvedPath:%s\\n' "$PATH"
if [ -n "$node_path" ]; then
"$node_path" <<'NODE'
Expand Down Expand Up @@ -349,6 +350,16 @@ export const parseNodePath = (stdout: string): string | null => {
return path ?? null;
};

export const parseNodeVersion = (stdout: string): string | null => {
const version = stdout
.split("\n")
.map((line) => line.trim())
.filter((line) => line.startsWith("nodeVersion:"))
.map((line) => line.slice("nodeVersion:".length).trim())
.find((value) => value.length > 0);
return version ?? null;
};

// Captures the login-shell PATH after the shared resolver has loaded version
// managers. Preserve the value byte-for-byte apart from a Windows-style CR so
// paths containing spaces or apostrophes can be forwarded as one env argv.
Expand Down Expand Up @@ -513,7 +524,22 @@ const ensureNodePtyImpl = (
} as const;
}

if (probe.exitCode === 0) return { ok: true, nodePath, resolvedPath, resolvedEnv } as const;
if (probe.exitCode === 0) {
const rawVersion = parseNodeVersion(probe.stdout);
if (
rawVersion !== null &&
options.nodeEngineRange &&
!satisfiesSemverRange(rawVersion, options.nodeEngineRange.trim())
) {
const range = options.nodeEngineRange.trim();
return {
ok: false,
reason: `WSL Node.js ${rawVersion} does not satisfy the server's required engine range (${range}). Install a compatible version, and restart the desktop app.`,
fatal: true,
} as const;
}
return { ok: true, nodePath, resolvedPath, resolvedEnv } as const;
}

if (options.allowBuild !== true) {
const packagedProbeFailure = formatNodePtyProbeFailureReason(probe.exitCode);
Expand Down
4 changes: 4 additions & 0 deletions apps/marketing/public/harnesses/grok-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 41 additions & 9 deletions apps/marketing/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const mobileEndorsementRows = [
<span class="hero-float-mark hf-cursor" title="Cursor">
<img src="/harnesses/cursor_light.svg" alt="" />
</span>
<span class="hero-float-mark hf-grok" title="Grok">
<img src="/harnesses/grok-dark.svg" alt="" />
</span>
</div>

<div class="container hero-inner">
Expand All @@ -41,7 +44,7 @@ const mobileEndorsementRows = [
</h1>

<p class="hero-sub">
Orchestrate Claude Code, Codex, OpenCode and Cursor from one surface.
Orchestrate Claude Code, Codex, OpenCode, Cursor, and Grok from one surface.
Bring your own subscription. Fork the whole thing.
</p>

Expand Down Expand Up @@ -173,7 +176,7 @@ const mobileEndorsementRows = [
<h2>Bring your own sub</h2>
<p>
T3 Code doesn't resell tokens. Plug in Claude Code, Codex, OpenCode,
or Cursor with the credentials you already have — we orchestrate
Cursor, or Grok with the credentials you already have — we orchestrate
them, you keep your plan.
</p>
</div>
Expand Down Expand Up @@ -207,6 +210,13 @@ const mobileEndorsementRows = [
<div class="harness-tag">cursor-agent</div>
</div>
</div>
<div class="harness">
<div class="harness-mark"><img src="/harnesses/grok-dark.svg" alt="" /></div>
<div class="harness-meta">
<div class="harness-name">Grok CLI</div>
<div class="harness-tag">grok login</div>
</div>
</div>
</div>

<div class="harness-footer">
Expand Down Expand Up @@ -615,8 +625,9 @@ const mobileEndorsementRows = [

.hero-float-mark.hf-claude { background: radial-gradient(circle at 30% 25%, rgba(217, 119, 87, 0.22), rgba(20, 20, 24, 0.92) 65%); top: 8%; left: 7%; animation-delay: 0s; transform: rotate(-8deg); }
.hero-float-mark.hf-codex { background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.08), rgba(20, 20, 24, 0.92) 65%); top: 6%; right: 7%; animation-delay: -2.5s; transform: rotate(6deg); }
.hero-float-mark.hf-opencode { background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.06), rgba(20, 20, 24, 0.92) 65%); top: 32%; left: 5%; animation-delay: -5s; transform: rotate(4deg); }
.hero-float-mark.hf-opencode { background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.06), rgba(20, 20, 24, 0.92) 65%); top: 30%; left: 5%; animation-delay: -5s; transform: rotate(4deg); }
.hero-float-mark.hf-cursor { background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.08), rgba(20, 20, 24, 0.92) 65%); top: 30%; right: 5%; animation-delay: -7s; transform: rotate(-5deg); }
.hero-float-mark.hf-grok { background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.08), rgba(20, 20, 24, 0.92) 65%); top: 52%; left: 6%; animation-delay: -1.2s; transform: rotate(3deg); }

@media (max-width: 1180px) {
.hero-float-mark { width: 76px; height: 76px; border-radius: 20px; }
Expand All @@ -625,6 +636,7 @@ const mobileEndorsementRows = [
.hero-float-mark.hf-codex { top: 2%; right: 3%; }
.hero-float-mark.hf-opencode { top: 26%; left: 2%; }
.hero-float-mark.hf-cursor { top: 24%; right: 2%; }
.hero-float-mark.hf-grok { top: 48%; left: 2%; transform: rotate(3deg); }
}

@media (max-width: 820px) {
Expand Down Expand Up @@ -659,17 +671,18 @@ const mobileEndorsementRows = [
height: 44px;
}

/* Two above the title, two flanking the call-to-action area. */
/* Three stacked on the left, two on the right — keeps the center CTA clear. */
.hero-float-mark.hf-claude {
top: 44px;
left: 10px;
transform: rotate(-10deg);
}

.hero-float-mark.hf-codex {
top: 44px;
right: 10px;
transform: rotate(8deg);
.hero-float-mark.hf-grok {
top: 240px;
left: 4px;
right: auto;
transform: rotate(-4deg);
}

.hero-float-mark.hf-opencode {
Expand All @@ -679,6 +692,12 @@ const mobileEndorsementRows = [
transform: rotate(-6deg);
}

.hero-float-mark.hf-codex {
top: 44px;
right: 10px;
transform: rotate(8deg);
}

.hero-float-mark.hf-cursor {
top: 474px;
right: 4px;
Expand All @@ -688,6 +707,14 @@ const mobileEndorsementRows = [
}

@media (max-width: 340px) {
.hero-float-mark.hf-grok {
top: 220px;
left: 0;
width: 52px;
height: 52px;
border-radius: 14px;
}

.hero-float-mark.hf-opencode,
.hero-float-mark.hf-cursor {
top: 580px;
Expand All @@ -704,6 +731,7 @@ const mobileEndorsementRows = [
right: 0;
}

.hero-float-mark.hf-grok img,
.hero-float-mark.hf-opencode img,
.hero-float-mark.hf-cursor img {
width: 30px;
Expand Down Expand Up @@ -750,7 +778,7 @@ const mobileEndorsementRows = [

.harness-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(5, 1fr);
gap: 0;
border: 1px solid var(--border);
border-radius: var(--radius);
Expand Down Expand Up @@ -1205,6 +1233,10 @@ const mobileEndorsementRows = [
.harness-grid { grid-template-columns: 1fr 1fr; }
.harness { border-right: 0; border-bottom: 1px solid var(--border); }
.harness:nth-child(odd) { border-right: 1px solid var(--border); }
.harness:nth-child(5):last-child {
grid-column: 1 / -1;
border-right: 0;
}
.git-inner { grid-template-columns: 1fr; gap: 40px; }
.open-grid { grid-template-columns: 1fr; }
}
Expand Down
Binary file modified apps/web/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/web/public/favicon.ico
Binary file not shown.
2 changes: 1 addition & 1 deletion assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The three Icon Composer projects are the source of truth for full application ic

Each project uses `text.svg` for the T3 mark and `background.svg` when the background is a vector layer. Additional layers use semantic names that describe their role and placement.

Run `vp run icons:export` from the repository root to regenerate the tracked iOS, Linux, Windows, and web assets. Run `vp run icons:check` to verify that those generated assets match their sources without changing files.
Run `vp run icons:export` from the repository root to regenerate the tracked iOS, Linux, Windows, and web assets. The development web exports are also copied to `apps/web/public` for the browser favicon and splash screen. Run `vp run icons:check` to verify that the generated assets and public copies match their sources without changing files.

Exporting requires Icon Composer 2 or newer on macOS. The script selects the newest compatible exporter from Xcode or a standalone Icon Composer installation and pins design generation 26. Set `ICON_COMPOSER_TOOL` to the full path of `Icon Composer.app/Contents/Executables/ictool` to override automatic discovery.

Expand Down
6 changes: 6 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
resolveDesktopBuildIconAssets,
resolveDesktopProductName,
resolveDesktopUpdateChannel,
resolveDesktopWebAssetBrand,
resolveGitHubPublishConfig,
resolveMockUpdateServerPort,
resolveMockUpdateServerUrl,
Expand Down Expand Up @@ -102,6 +103,11 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
});
});

it("switches the bundled splash and favicon branding for nightly versions", () => {
assert.equal(resolveDesktopWebAssetBrand("0.0.17"), "production");
assert.equal(resolveDesktopWebAssetBrand("0.0.17-nightly.20260413.42"), "nightly");
});

it.effect("resolves GitHub desktop publish config from Effect config", () =>
Effect.gen(function* () {
const latestConfig = yield* resolveGitHubPublishConfig("latest").pipe(
Expand Down
14 changes: 13 additions & 1 deletion scripts/build-desktop-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import rootPackageJson from "../package.json" with { type: "json" };
import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };
import serverPackageJson from "../apps/server/package.json" with { type: "json" };

import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
import { applyWebBrandAssets } from "./apply-web-brand-assets.ts";
import {
BRAND_ASSET_PATHS,
resolveWebAssetBrandForChannel,
type WebAssetBrand,
} from "./lib/brand-assets.ts";
import { getDefaultBuildArch } from "./lib/build-target-arch.ts";
import { loadRepoEnv } from "./lib/public-config.ts";
import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";
Expand Down Expand Up @@ -1340,6 +1345,10 @@ export function resolveDesktopUpdateChannel(version: string): "latest" | "nightl
return /-nightly\.\d{8}\.\d+$/.test(version) ? "nightly" : "latest";
}

export function resolveDesktopWebAssetBrand(version: string): WebAssetBrand {
return resolveWebAssetBrandForChannel(resolveDesktopUpdateChannel(version));
}

export function resolveDesktopBuildIconAssets(version: string): DesktopBuildIconAssets {
if (resolveDesktopUpdateChannel(version) === "nightly") {
return {
Expand Down Expand Up @@ -1695,6 +1704,9 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
});
}

const webAssetBrand = resolveDesktopWebAssetBrand(appVersion);
yield* applyWebBrandAssets(webAssetBrand, "apps/server/dist/client");
yield* Effect.log(`[desktop-artifact] Applied ${webAssetBrand} web client branding.`);
yield* validateBundledClientAssets(path.dirname(bundledClientEntry));

yield* fs.makeDirectory(path.join(stageAppDir, "apps/desktop"), { recursive: true });
Expand Down
12 changes: 11 additions & 1 deletion scripts/export-brand-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as Stream from "effect/Stream";
import { Command, Flag } from "effect/unstable/cli";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
import { BRAND_ASSET_PATHS, DEVELOPMENT_PUBLIC_ICON_OVERRIDES } from "./lib/brand-assets.ts";
import { encodePngIco, readPngDimensions, WINDOWS_ICON_SIZES } from "./lib/icon-export.ts";

const DESIGN_GENERATION = 26;
Expand Down Expand Up @@ -751,6 +751,16 @@ export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOn
}
}

for (const override of DEVELOPMENT_PUBLIC_ICON_OVERRIDES) {
const sourceContents = generated.get(override.sourceRelativePath);
if (sourceContents === undefined) {
return yield* Effect.die(
new Error(`Generated development web icon is missing: ${override.sourceRelativePath}`),
);
}
generated.set(override.targetRelativePath, sourceContents);
}

if (checkOnly) {
const stale = yield* Effect.filter(
[...generated.entries()],
Expand Down
22 changes: 22 additions & 0 deletions scripts/lib/brand-assets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from "vite-plus/test";
import {
BRAND_ASSET_PATHS,
DEVELOPMENT_ICON_OVERRIDES,
DEVELOPMENT_PUBLIC_ICON_OVERRIDES,
PUBLISH_ICON_OVERRIDES,
resolveWebAssetBrandForChannel,
resolveWebIconOverrides,
Expand Down Expand Up @@ -37,6 +38,27 @@ describe("brand-assets", () => {
});
});

it("maps development web assets to the public splash and favicon files", () => {
expect(DEVELOPMENT_PUBLIC_ICON_OVERRIDES).toEqual([
{
sourceRelativePath: BRAND_ASSET_PATHS.developmentWebFaviconIco,
targetRelativePath: "apps/web/public/favicon.ico",
},
{
sourceRelativePath: BRAND_ASSET_PATHS.developmentWebFavicon16Png,
targetRelativePath: "apps/web/public/favicon-16x16.png",
},
{
sourceRelativePath: BRAND_ASSET_PATHS.developmentWebFavicon32Png,
targetRelativePath: "apps/web/public/favicon-32x32.png",
},
{
sourceRelativePath: BRAND_ASSET_PATHS.developmentWebAppleTouchIconPng,
targetRelativePath: "apps/web/public/apple-touch-icon.png",
},
]);
});

it("can target hosted web dist directly", () => {
expect(resolveWebIconOverrides("production", "apps/web/dist")).toContainEqual({
sourceRelativePath: BRAND_ASSET_PATHS.productionWebAppleTouchIconPng,
Expand Down
5 changes: 5 additions & 0 deletions scripts/lib/brand-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,9 @@ export function resolveWebIconOverrides(

export const DEVELOPMENT_ICON_OVERRIDES = resolveWebIconOverrides("development", "dist/client");

export const DEVELOPMENT_PUBLIC_ICON_OVERRIDES = resolveWebIconOverrides(
"development",
"apps/web/public",
);

export const PUBLISH_ICON_OVERRIDES = resolveWebIconOverrides("production", "dist/client");
Loading