Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1bd2fa2
feat: draft hero landing on the index route (#4055)
yordis Jul 18, 2026
4cfec8c
feat: file explorer mention actions and zoom-aware context menus (#4054)
yordis Jul 18, 2026
2318e00
fix(web): avoid duplicate mention text on paste
juliusmarminge Jul 18, 2026
03e20c8
fix(mobile): restore iOS home screen branding (#4025)
PixPMusic Jul 18, 2026
765e1b5
perf(client): defer active thread cache writes (#4006)
Chrrxs Jul 18, 2026
38e0bb0
Default diffs to working changes (#3974)
jakeleventhal Jul 18, 2026
e350c0a
Add Grok to marketing site provider list (#3484)
Aditya190803 Jul 18, 2026
31caa56
Fix reopening existing Diff tab (#3973)
jakeleventhal Jul 18, 2026
d8b12ae
Fix sending messages during active turns (#3919)
jakeleventhal Jul 18, 2026
d0b9f8d
[codex] Route OpenCode missing-session errors through Effect (#3608)
StiensWout Jul 18, 2026
c0e161a
[fix/feat:ui] Show default option badge (#3232)
sandersonstabo Jul 18, 2026
e63403f
[fix/feat:ui] Preserve open-in editor brand colors (#3225)
sandersonstabo Jul 18, 2026
b859a57
fix(web): handle macOS Home and End in composer (#2508)
GuilhermeVieiraDev Jul 18, 2026
0506508
Allow failed remote environments to be removed (#4084)
zepi2509 Jul 18, 2026
da3c203
[codex] canonicalize client timestamps (#4112)
maxwellyoung Jul 18, 2026
5c66110
[fix/feat:ui] Make selected menu checks blue (#3234)
sandersonstabo Jul 18, 2026
bf76535
fix(desktop): Validate WSL node version against engine range after pr…
UtkarshUsername Jul 18, 2026
ebe8afb
Refresh splash screen and favicon branding (#4120)
juliusmarminge Jul 18, 2026
5c252ba
sync upstream main
tarik02 Jul 18, 2026
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
14 changes: 12 additions & 2 deletions apps/desktop/src/electron/ElectronMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ const TestLayer = ElectronMenu.layer.pipe(
Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
);

const makeWindow = (zoomFactor = 1): Electron.BrowserWindow =>
({
id: 7,
webContents: { getZoomFactor: () => zoomFactor },
}) as unknown as Electron.BrowserWindow;

describe("ElectronMenu", () => {
beforeEach(() => {
buildFromTemplateMock.mockReset();
Expand Down Expand Up @@ -70,7 +76,7 @@ describe("ElectronMenu", () => {

const electronMenu = yield* ElectronMenu.ElectronMenu;
const selectedItemId = yield* electronMenu.showContextMenu({
window: {} as Electron.BrowserWindow,
window: makeWindow(),
items: [{ id: "copy", label: "Copy" }],
position: Option.none(),
});
Expand All @@ -81,20 +87,24 @@ describe("ElectronMenu", () => {

it.effect("resolves with none when the menu closes without a click", () =>
Effect.gen(function* () {
let popupOptions: Electron.PopupOptions | undefined;
buildFromTemplateMock.mockImplementation(() => ({
popup: (options: Electron.PopupOptions) => {
popupOptions = options;
options.callback?.();
},
}));

const electronMenu = yield* ElectronMenu.ElectronMenu;
const selectedItemId = yield* electronMenu.showContextMenu({
window: {} as Electron.BrowserWindow,
window: makeWindow(2),
items: [{ id: "copy", label: "Copy" }],
position: Option.some({ x: 10.8, y: 20.2 }),
});

assert.isTrue(Option.isNone(selectedItemId));
assert.equal(popupOptions?.x, 21);
assert.equal(popupOptions?.y, 40);
assert.deepEqual(buildFromTemplateMock.mock.calls[0]?.[0][0], {
label: "Copy",
enabled: true,
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/electron/ElectronMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,20 @@ function normalizeContextMenuItems(source: readonly ContextMenuItem[]): ContextM
return normalizedItems;
}

// Renderer positions arrive in CSS pixels; popup() expects window points, so
// page zoom must be factored in or menus drift proportionally to their
// distance from the window origin.
const normalizePosition = (
position: Option.Option<ElectronMenuPosition>,
zoomFactor: number,
): Option.Option<ElectronMenuPosition> =>
Option.filter(
position,
({ x, y }) => Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0,
).pipe(Option.map(({ x, y }) => ({ x: Math.floor(x), y: Math.floor(y) })));
({ x, y }) =>
Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0 && Number.isFinite(zoomFactor),
).pipe(
Option.map(({ x, y }) => ({ x: Math.floor(x * zoomFactor), y: Math.floor(y * zoomFactor) })),
);

export const make = Effect.gen(function* () {
const platform = yield* HostProcessPlatform;
Expand Down Expand Up @@ -214,7 +221,10 @@ export const make = Effect.gen(function* () {

try {
const menu = Electron.Menu.buildFromTemplate(buildTemplate(normalizedItems, complete));
const popupPosition = normalizePosition(input.position);
const popupPosition = normalizePosition(
input.position,
input.window.webContents.getZoomFactor(),
);
const popupOptions = Option.match(popupPosition, {
onNone: (): Electron.PopupOptions => ({
window: input.window,
Expand Down
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,
parseResolvedPath,
parseToolchainReport,
probeWslDistros,
Expand Down Expand Up @@ -173,6 +174,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
46 changes: 42 additions & 4 deletions apps/desktop/src/wsl/DesktopWslEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export interface EnsureWslNodePtyOptions {
}

export type EnsureWslNodePtyResult =
| { readonly ok: true; readonly nodePath: string; readonly resolvedPath: string }
| {
readonly ok: true;
readonly nodePath: string;
readonly resolvedPath: string;
}
| {
readonly ok: false;
readonly reason: string;
Expand Down Expand Up @@ -222,6 +226,7 @@ export const formatNodePtyProbeFailureReason = (exitCode: number): string | null
const NODE_PTY_PROBE_SCRIPT = (
linuxServerDir: string,
) => `printf 'nodePath:%s\\n' "$(command -v node 2>/dev/null)"
printf 'nodeVersion:%s\\n' "$(node -p 'process.versions.node' 2>/dev/null)"
printf 'resolvedPath:%s\\n' "$PATH"
cd ${shellQuote(linuxServerDir)} && node <<'NODE' >/dev/null 2>&1
// The server bundle externalizes its deps to node_modules, and the WSL Node
Expand Down Expand Up @@ -318,6 +323,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 @@ -404,7 +419,11 @@ const ensureNodePtyImpl = (

const transportFailureReason = formatWslShellTransportFailureReason(probe.transportFailure);
if (transportFailureReason !== null) {
return { ok: false, reason: transportFailureReason, fatal: false } as const;
return {
ok: false,
reason: transportFailureReason,
fatal: false,
} as const;
}

// No node at all, even after the shared resolver repaired PATH. Surface
Expand Down Expand Up @@ -457,12 +476,31 @@ const ensureNodePtyImpl = (
} as const;
}

if (probe.exitCode === 0) return { ok: true, nodePath, resolvedPath } 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 } as const;
}

if (options.allowBuild !== true) {
const packagedProbeFailure = formatNodePtyProbeFailureReason(probe.exitCode);
if (packagedProbeFailure !== null) {
return { ok: false, reason: packagedProbeFailure, fatal: true } as const;
return {
ok: false,
reason: packagedProbeFailure,
fatal: true,
} as const;
}
}

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
4 changes: 3 additions & 1 deletion apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DynamicColorIOS, Platform, Pressable, ScrollView, StyleSheet } from "re
import { useResolveClassNames } from "uniwind";

import { AppText as Text } from "./components/AppText";
import { renderCompactBrandTitle } from "./components/CompactBrandTitle";
import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";
import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation";
import { ClerkSettingsSheetDetentProvider } from "./features/cloud/ClerkSettingsSheetDetent";
Expand Down Expand Up @@ -377,7 +378,8 @@ export const RootStack = createNativeStackNavigator({
...GLASS_HEADER_OPTIONS,
contentStyle: { backgroundColor: "transparent" },
headerBackVisible: false,
title: "Threads",
headerTitle: renderCompactBrandTitle,
title: "T3 Code",
},
}),
Thread: createNativeStackScreen({
Expand Down
Loading
Loading