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
0046ff0
feat(server): include runtime model and effort in Codex developer ins…
Jul 18, 2026
c008b5d
fix(server): thread cwd through Claude capability probe (#2048) (#2124)
Jul 18, 2026
aac53d9
[codex] prevent ACP assistant ID collisions after restarts (#3932)
Jul 18, 2026
fd9d78d
fix(cursor): default legacy binary path to cursor-agent (#4094)
Jul 18, 2026
92a8d11
Normalize protocol-relative remote host input as https (#3971)
kridaydave Jul 17, 2026
a879ac5
Fix documented task-runner commands (bun run -> vp) (#3965)
kridaydave Jul 17, 2026
be39c9d
Update marketing GitHub star count (#4088)
AmoonPod Jul 17, 2026
5f7c385
fix(marketing): correct Cursor icon color (#4090)
AmoonPod Jul 17, 2026
8f1d277
feat: show nightly update changelog tooltip (#3832)
HugoVizcainoSantana Jul 17, 2026
dde5d16
Preserve T3 Code identity in macOS development launcher (#4102)
juliusmarminge Jul 17, 2026
5ab443d
Refresh app icons across release variants (#4080)
juliusmarminge Jul 17, 2026
3dd7b90
Allow preview panel to grow on wide displays (#4044)
olivoil Jul 17, 2026
1a0a774
fix: prevent initial right-click from selecting a context menu item (…
Fazalkadivar21 Jul 17, 2026
1c0dd76
Remove unused code parameter from describePreviewError (#3970)
kridaydave Jul 17, 2026
edc355b
fix(web): inset Windows desktop scrollbars from resize edge (#4097)
nateEc Jul 17, 2026
186d9d4
[codex] fix mobile composer Enter behavior (#3930)
maxwellyoung Jul 17, 2026
11aa592
fix(ux): spamming cmd + , no longer stack opening settings (#2757)
jamesx0416 Jul 17, 2026
3da4263
[codex] fix: guard invalid web timestamps (#3515)
StiensWout Jul 17, 2026
9539307
[codex] fix: tolerate invalid latest user message timestamps (#3521)
StiensWout Jul 17, 2026
ef3dd7e
[codex] Fix provider update checks restore defaults (#3531)
StiensWout Jul 17, 2026
2c50028
fix(web): increase contrast of question option descriptions (#3867)
xxashxx-svg Jul 17, 2026
bfd7b95
fix(web): clamp preview max width to the chat+preview row
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
43 changes: 34 additions & 9 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const APP_BUNDLE_ID = isDevelopment
? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`
: "com.t3tools.t3code";
const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
const LAUNCHER_VERSION = 12;
const LAUNCHER_VERSION = 14;
const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns");
const developmentMacIconPngPath = NodePath.join(
repoRoot,
Expand Down Expand Up @@ -220,11 +220,12 @@ function ensureDevelopmentIconIcns(runtimeDir) {
}
}

function patchMainBundleInfoPlist(appBundlePath, iconPath) {
function patchMainBundleInfoPlist(appBundlePath, iconPath, executableName) {
const infoPlistPath = NodePath.join(appBundlePath, "Contents", "Info.plist");
setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);
setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME);
setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID);
setPlistString(infoPlistPath, "CFBundleExecutable", executableName);
setPlistString(infoPlistPath, "CFBundleIconFile", "icon.icns");
setPlistJson(infoPlistPath, "CFBundleURLTypes", [
{
Expand Down Expand Up @@ -277,11 +278,25 @@ function readJson(path) {
}
}

export function resolveMacLauncherPaths(appBundlePath, displayName = APP_DISPLAY_NAME) {
const executableDir = NodePath.join(appBundlePath, "Contents", "MacOS");
const launcherExecutableName = `${displayName} Launcher`;
return {
launcherExecutableName,
launcherBinaryPath: NodePath.join(executableDir, launcherExecutableName),
runtimeElectronBinaryPath: NodePath.join(executableDir, "Electron"),
};
}

function buildMacLauncher(electronBinaryPath) {
const sourceAppBundlePath = NodePath.resolve(NodePath.dirname(electronBinaryPath), "../..");
const runtimeDir = NodePath.join(desktopDir, ".electron-runtime");
const targetAppBundlePath = NodePath.join(runtimeDir, `${APP_DISPLAY_NAME}.app`);
const targetBinaryPath = NodePath.join(targetAppBundlePath, "Contents", "MacOS", "Electron");
const developmentPaths = resolveMacLauncherPaths(targetAppBundlePath);
const runtimeElectronBinaryPath = developmentPaths.runtimeElectronBinaryPath;
const launcherBinaryPath = isDevelopment
? developmentPaths.launcherBinaryPath
: runtimeElectronBinaryPath;
const iconPath = isDevelopment ? ensureDevelopmentIconIcns(runtimeDir) : defaultIconPath;
const metadataPath = NodePath.join(runtimeDir, "metadata.json");

Expand All @@ -298,18 +313,19 @@ function buildMacLauncher(electronBinaryPath) {

const currentMetadata = readJson(metadataPath);
if (
NodeFS.existsSync(targetBinaryPath) &&
NodeFS.existsSync(launcherBinaryPath) &&
(!isDevelopment || NodeFS.existsSync(runtimeElectronBinaryPath)) &&
currentMetadata &&
JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata)
) {
if (isDevelopment) {
// The launcher also handles protocol activations outside the dev runner,
// so refresh its fallback environment on every launch. Never let a value
// captured by an older parent app override the live dev-runner environment.
writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath);
writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
}
registerMacLauncherBundle(targetAppBundlePath);
return targetBinaryPath;
return launcherBinaryPath;
}

NodeFS.rmSync(targetAppBundlePath, { recursive: true, force: true });
Expand All @@ -321,15 +337,24 @@ function buildMacLauncher(electronBinaryPath) {
recursive: true,
verbatimSymlinks: true,
});
patchMainBundleInfoPlist(targetAppBundlePath, iconPath);
patchMainBundleInfoPlist(
targetAppBundlePath,
iconPath,
isDevelopment ? developmentPaths.launcherExecutableName : "Electron",
);
patchHelperBundleInfoPlists(targetAppBundlePath);
if (isDevelopment) {
writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath);
// Keep Electron's native executable inside the branded bundle. Launching the
// node_modules copy makes macOS associate the process (and Dock label) with
// Electron.app even though this bundle's Info.plist has the T3 Code name.
// Its conventional executable name also keeps Electron's default-app runtime
// in development mode instead of making app.isPackaged report true.
writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
}
NodeFS.writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
registerMacLauncherBundle(targetAppBundlePath);

return targetBinaryPath;
return launcherBinaryPath;
}

function isLinuxSetuidSandboxConfigured(electronBinaryPath) {
Expand Down
35 changes: 34 additions & 1 deletion apps/desktop/scripts/electron-launcher.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { assert, describe, it } from "vite-plus/test";

import { makeDevelopmentLauncherScript, resolveElectronBinaryPath } from "./electron-launcher.mjs";
import {
makeDevelopmentLauncherScript,
resolveElectronBinaryPath,
resolveMacLauncherPaths,
} from "./electron-launcher.mjs";

describe("electron development launcher", () => {
it("uses captured values only as fallbacks for a live runner environment", () => {
Expand Down Expand Up @@ -45,4 +49,33 @@ describe("electron development launcher", () => {
);
assert.deepEqual(calls, ["ensure", "require:electron"]);
});

it("keeps the native Electron executable name inside the branded macOS bundle", () => {
const paths = resolveMacLauncherPaths(
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app",
"T3 Code (Dev)",
);

assert.equal(paths.launcherExecutableName, "T3 Code (Dev) Launcher");
assert.equal(
paths.launcherBinaryPath,
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/T3 Code (Dev) Launcher",
);
assert.equal(
paths.runtimeElectronBinaryPath,
"/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron",
);

const script = makeDevelopmentLauncherScript({
electronBinaryPath: paths.runtimeElectronBinaryPath,
mainEntryPath: "/repo/apps/desktop/dist-electron/main.cjs",
desktopRoot: "/repo/apps/desktop",
environment: {},
});
assert.include(
script,
"exec '/repo/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron'",
);
assert.notInclude(script, "node_modules/electron");
});
});
14 changes: 14 additions & 0 deletions apps/desktop/src/electron/ElectronUpdater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const { autoUpdaterMock } = vi.hoisted(() => ({
autoInstallOnAppQuit: true,
channel: "latest",
disableDifferentialDownload: false,
fullChangelog: false,
checkForUpdates: vi.fn(() => Promise.resolve(null)),
downloadUpdate: vi.fn(() => Promise.resolve([])),
on: vi.fn(),
Expand All @@ -33,6 +34,7 @@ describe("ElectronUpdater", () => {
autoUpdaterMock.autoInstallOnAppQuit = true;
autoUpdaterMock.channel = "latest";
autoUpdaterMock.disableDifferentialDownload = false;
autoUpdaterMock.fullChangelog = false;
autoUpdaterMock.checkForUpdates.mockClear();
autoUpdaterMock.checkForUpdates.mockImplementation(() => Promise.resolve(null));
autoUpdaterMock.downloadUpdate.mockClear();
Expand Down Expand Up @@ -98,6 +100,18 @@ describe("ElectronUpdater", () => {
}).pipe(Effect.provide(ElectronUpdater.layer)),
);

it.effect("sets full changelog mode", () =>
Effect.gen(function* () {
const updater = yield* ElectronUpdater.ElectronUpdater;

yield* updater.setFullChangelog(true);
assert.equal(autoUpdaterMock.fullChangelog, true);

yield* updater.setFullChangelog(false);
assert.equal(autoUpdaterMock.fullChangelog, false);
}).pipe(Effect.provide(ElectronUpdater.layer)),
);

it.effect("preserves quit-and-install flags and the execution-time channel", () =>
Effect.gen(function* () {
const cause = new Error("quit and install failed");
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/electron/ElectronUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class ElectronUpdater extends Context.Service<
readonly setAllowPrerelease: (value: boolean) => Effect.Effect<void>;
readonly allowDowngrade: Effect.Effect<boolean>;
readonly setAllowDowngrade: (value: boolean) => Effect.Effect<void>;
readonly setFullChangelog: (value: boolean) => Effect.Effect<void>;
readonly setDisableDifferentialDownload: (value: boolean) => Effect.Effect<void>;
readonly checkForUpdates: Effect.Effect<void, ElectronUpdaterCheckForUpdatesError>;
readonly downloadUpdate: Effect.Effect<void, ElectronUpdaterDownloadUpdateError>;
Expand Down Expand Up @@ -112,6 +113,11 @@ export const make = ElectronUpdater.of({
autoUpdater.allowDowngrade = value;
return Effect.void;
}),
setFullChangelog: (value) =>
Effect.suspend(() => {
autoUpdater.fullChangelog = value;
return Effect.void;
}),
setDisableDifferentialDownload: (value) =>
Effect.suspend(() => {
autoUpdater.disableDifferentialDownload = value;
Expand Down
49 changes: 49 additions & 0 deletions apps/desktop/src/updates/DesktopUpdates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const flushCallbacks = Effect.yieldNow;
function makeHarness(options: UpdatesHarnessOptions = {}) {
let checkCount = 0;
let allowDowngrade = false;
let fullChangelog = false;
const feedUrls: ElectronUpdater.ElectronUpdaterFeedUrl[] = [];
const listeners = new Map<string, Set<(...args: readonly unknown[]) => void>>();
const sentStates: DesktopUpdateState[] = [];
Expand Down Expand Up @@ -73,6 +74,10 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
Effect.sync(() => {
allowDowngrade = value;
}),
setFullChangelog: (value) =>
Effect.sync(() => {
fullChangelog = value;
}),
setDisableDifferentialDownload: () => options.setDisableDifferentialDownload ?? Effect.void,
checkForUpdates: Effect.sync(() => {
checkCount += 1;
Expand Down Expand Up @@ -186,6 +191,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
layer,
checkCount: () => checkCount,
feedUrls: () => feedUrls,
fullChangelog: () => fullChangelog,
listenerCount: () =>
Array.from(listeners.values()).reduce(
(total, eventListeners) => total + eventListeners.size,
Expand Down Expand Up @@ -287,6 +293,49 @@ describe("DesktopUpdates", () => {
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});

it.effect("enables nightly full changelog release notes and broadcasts summaries", () => {
const harness = makeHarness();

return Effect.scoped(
Effect.gen(function* () {
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;

yield* updates.setChannel("nightly");
assert.equal(harness.fullChangelog(), true);

harness.emit("update-available", {
version: "1.2.4-nightly.20260709.766",
releaseNotes: [
{
version: "1.2.4-nightly.20260709.766",
note: `<h2>What's Changed</h2><ul><li>feat(client): persist offline environment data by <a>@juliusmarminge</a> in <a>#3795</a></li></ul><h2>Full Changelog</h2>`,
},
{
version: "1.2.4-nightly.20260709.765",
note: "- [codex] Upgrade Clerk stack by @juliusmarminge in #3821",
},
],
});
yield* flushCallbacks;

const state = yield* updates.getState;
assert.equal(state.status, "available");
assert.deepEqual(state.releaseNotes, [
{
version: "1.2.4-nightly.20260709.766",
items: ["feat(client): persist offline environment data by @juliusmarminge in #3795"],
},
{
version: "1.2.4-nightly.20260709.765",
items: ["[codex] Upgrade Clerk stack by @juliusmarminge in #3821"],
},
]);
assert.deepEqual(harness.sentStates.at(-1)?.releaseNotes, state.releaseNotes);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});

it.effect("keeps raw updater event failures out of update state", () => {
const harness = makeHarness();
const cause = new Error(
Expand Down
15 changes: 13 additions & 2 deletions apps/desktop/src/updates/DesktopUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as ElectronUpdater from "../electron/ElectronUpdater.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import * as IpcChannels from "../ipc/channels.ts";
import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
import { normalizeDesktopUpdateReleaseNotes } from "./releaseNotes.ts";
import { resolveDefaultDesktopUpdateChannel } from "./updateChannels.ts";
import {
createInitialDesktopUpdateState,
Expand All @@ -49,6 +50,10 @@ type AppUpdateYmlConfig = typeof AppUpdateYmlConfig.Type;

const UpdateInfo = Schema.Struct({
version: Schema.String,
// Left unvalidated on purpose: a malformed release-notes payload must never
// fail the decode and block the update state transition. The shape is
// validated defensively in normalizeDesktopUpdateReleaseNotes.
releaseNotes: Schema.optional(Schema.Unknown),
});

const DownloadProgressInfo = Schema.Struct({
Expand Down Expand Up @@ -330,10 +335,12 @@ export const make = Effect.gen(function* () {
yield* electronUpdater.setChannel(channel);
yield* electronUpdater.setAllowPrerelease(allowsPrerelease);
yield* electronUpdater.setAllowDowngrade(allowsPrerelease);
yield* electronUpdater.setFullChangelog(allowsPrerelease);
yield* logUpdaterInfo("using update channel", {
channel,
allowPrerelease: allowsPrerelease,
allowDowngrade: allowsPrerelease,
fullChangelog: allowsPrerelease,
});
});

Expand Down Expand Up @@ -567,11 +574,15 @@ export const make = Effect.gen(function* () {
}

const checkedAt = yield* currentIsoTimestamp;
const releaseNotes = normalizeDesktopUpdateReleaseNotes(info.releaseNotes, info.version);
yield* setState(
reduceDesktopUpdateStateOnUpdateAvailable(state, info.version, checkedAt),
reduceDesktopUpdateStateOnUpdateAvailable(state, info.version, checkedAt, releaseNotes),
);
yield* Ref.set(lastLoggedDownloadMilestoneRef, -1);
yield* logUpdaterInfo("update available", { version: info.version });
yield* logUpdaterInfo("update available", {
version: info.version,
releaseNoteGroups: releaseNotes.length,
});
}),
),
Effect.catchCause((cause) => {
Expand Down
67 changes: 67 additions & 0 deletions apps/desktop/src/updates/releaseNotes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vite-plus/test";

import { normalizeDesktopUpdateReleaseNotes } from "./releaseNotes.ts";

describe("normalizeDesktopUpdateReleaseNotes", () => {
it("splits a plain string note into items under the fallback version", () => {
const notes = normalizeDesktopUpdateReleaseNotes(
"## What's changed\n- First fix\n- Second fix",
"1.2.3",
);
expect(notes).toEqual([{ version: "1.2.3", items: ["First fix", "Second fix"] }]);
});

it("keeps per-version groups and drops empty ones", () => {
const notes = normalizeDesktopUpdateReleaseNotes(
[
{ version: "1.2.3", note: "- Newer change" },
{ version: "1.2.2", note: "Full changelog: https://example.com/compare/x...y" },
{ version: "1.2.1", note: "- Older change" },
],
"1.2.3",
);
expect(notes).toEqual([
{ version: "1.2.3", items: ["Newer change"] },
{ version: "1.2.1", items: ["Older change"] },
]);
});

it("decodes valid HTML entities", () => {
const notes = normalizeDesktopUpdateReleaseNotes("- Fix &amp; polish &#128512;", "1.0.0");
expect(notes).toEqual([{ version: "1.0.0", items: ["Fix & polish 😀"] }]);
});

it("ignores malformed entries instead of throwing", () => {
const notes = normalizeDesktopUpdateReleaseNotes(
[
{ version: "1.2.3", note: "- Valid change" },
{ version: 42, note: "- Bad version type" },
{ version: "1.2.1", note: { html: "<p>object note</p>" } },
"not an object",
null,
],
"1.2.3",
);
expect(notes).toEqual([{ version: "1.2.3", items: ["Valid change"] }]);
});

it("returns non-empty groups even when preceded by many boilerplate-only groups", () => {
const boilerplate = Array.from({ length: 7 }, (_, index) => ({
version: `1.3.${9 - index}`,
note: "Full changelog: https://example.com/compare/x...y",
}));
const notes = normalizeDesktopUpdateReleaseNotes(
[...boilerplate, { version: "1.3.2", note: "- Older but real change" }],
"1.3.9",
);
expect(notes).toEqual([{ version: "1.3.2", items: ["Older but real change"] }]);
});

it("does not throw on out-of-range numeric entities and keeps the literal", () => {
expect(() =>
normalizeDesktopUpdateReleaseNotes("- Broken entity &#9999999999;", "1.0.0"),
).not.toThrow();
const notes = normalizeDesktopUpdateReleaseNotes("- Broken entity &#9999999999;", "1.0.0");
expect(notes).toEqual([{ version: "1.0.0", items: ["Broken entity &#9999999999;"] }]);
});
});
Loading
Loading