Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
db902e7
Fix dropped events during initial thread snapshot (#4079)
D3OXY Jul 17, 2026
d6a1d8f
[codex] fix mobile composer Enter behavior (#3930)
maxwellyoung Jul 17, 2026
9118e75
feat: draft hero landing on the index route (#4055)
yordis Jul 18, 2026
3bf2854
feat: file explorer mention actions and zoom-aware context menus (#4054)
yordis Jul 18, 2026
f958c00
fix(web): avoid duplicate mention text on paste
juliusmarminge Jul 18, 2026
78ef1cc
fix(mobile): restore iOS home screen branding (#4025)
PixPMusic Jul 18, 2026
93da3ae
perf(client): defer active thread cache writes (#4006)
Chrrxs Jul 18, 2026
8c2f39c
Default diffs to working changes (#3974)
jakeleventhal Jul 18, 2026
738f338
Add Grok to marketing site provider list (#3484)
Aditya190803 Jul 18, 2026
ca62d44
Fix reopening existing Diff tab (#3973)
jakeleventhal Jul 18, 2026
665042f
Fix sending messages during active turns (#3919)
jakeleventhal Jul 18, 2026
d104601
[codex] Route OpenCode missing-session errors through Effect (#3608)
StiensWout Jul 18, 2026
94ed435
[fix/feat:ui] Show default option badge (#3232)
sandersonstabo Jul 18, 2026
d699243
[fix/feat:ui] Preserve open-in editor brand colors (#3225)
sandersonstabo Jul 18, 2026
6fe6279
fix(web): handle macOS Home and End in composer (#2508)
GuilhermeVieiraDev Jul 18, 2026
b4a30b6
Allow failed remote environments to be removed (#4084)
zepi2509 Jul 18, 2026
d571a98
[codex] canonicalize client timestamps (#4112)
maxwellyoung Jul 18, 2026
1739a1b
[fix/feat:ui] Make selected menu checks blue (#3234)
sandersonstabo Jul 18, 2026
d53dc9a
fix(desktop): Validate WSL node version against engine range after pr…
UtkarshUsername Jul 18, 2026
a2ff94a
Refresh splash screen and favicon branding (#4120)
juliusmarminge Jul 18, 2026
f4ee4e0
Add terminal selection copy action (#2904)
tarik02 Jul 18, 2026
9d89c08
fix(sync): reconcile upstream changes with fork-local behaviour
wizzoapp[bot] Jul 19, 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
60 changes: 56 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
const fs = require("node:fs");
Expand Down Expand Up @@ -309,6 +314,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 @@ -395,7 +410,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 @@ -434,12 +453,45 @@ const ensureNodePtyImpl = (
} as const;
}

if (probe.exitCode === 0) return { ok: true, nodePath, resolvedPath } as const;
// Server dependencies (e.g. "effect") couldn't be resolved on the WSL
// filesystem — a packaging regression, since the server bundle needs its
// node_modules unpacked from the asar. Fatal so wsl-only mode falls back to
// Windows and dual mode surfaces the reason inline, instead of the server
// crash-looping on ERR_MODULE_NOT_FOUND once it actually launches.
if (probe.exitCode === 3) {
return {
ok: false,
reason:
"WSL server dependencies could not be loaded (for example \"effect\"). The server's bundled node_modules is not readable by the WSL distro's Node — this is a packaging problem with this build. Please report it.",
fatal: true,
} 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