Skip to content
This repository was archived by the owner on May 4, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a75f125
Merge upstream main while preserving Copilot provider and web UX inte…
zortos293 Apr 17, 2026
8277727
Fix upstream-sync regressions from CI and PR review
zortos293 Apr 17, 2026
f557a7b
Fix provider status cache quotaSnapshots omission
zortos293 Apr 17, 2026
c94fe72
Fix remaining upstream-sync review regressions
zortos293 Apr 17, 2026
c367391
Fix latest upstream-sync review regressions
zortos293 Apr 17, 2026
398e200
Fix newest upstream-sync review regressions
zortos293 Apr 17, 2026
2b39a03
Fix remaining upstream-sync review regressions
zortos293 Apr 17, 2026
862c2db
Fix newest upstream-sync review regressions
zortos293 Apr 17, 2026
8357e9a
Fix remaining late-review regressions on upstream sync PR
zortos293 Apr 17, 2026
8d174a3
Fix remaining upstream merge regressions covering data fidelity, prov…
zortos293 Apr 17, 2026
d82941e
Fix Claude launchArgs persistence via settings patch API
zortos293 Apr 17, 2026
0735ce1
Fix Claude alias gating, prerelease version parsing, and provider ses…
zortos293 Apr 17, 2026
9e34a6e
Preserve user preference on disabled provider, fix provider patch def…
zortos293 Apr 17, 2026
cc650d4
Merge Windows PATH sources and add regression coverage
zortos293 Apr 17, 2026
06966fe
Reapply Task 15 Opus 4.7 gating and runtime fixes onto upstream merge
zortos293 Apr 17, 2026
e9a0432
Reapply web state persistence fixes for logical project IDs and deter…
zortos293 Apr 17, 2026
0c2e587
Fix provider replacement invariants and desktop migration safety
zortos293 Apr 17, 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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ jobs:

- name: Verify preload bundle output
run: |
test -f apps/desktop/dist-electron/preload.js
grep -nE "desktopBridge|getLocalEnvironmentBootstrap|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.js
test -f apps/desktop/dist-electron/preload.cjs
grep -nE "desktopBridge|getLocalEnvironmentBootstrap|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.cjs

release_smoke:
name: Release Smoke
Expand Down
59 changes: 54 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,25 +138,30 @@ jobs:
matrix:
include:
- label: macOS arm64
runner: macos-14
runner: blacksmith-12vcpu-macos-26
Comment thread
capy-ai[bot] marked this conversation as resolved.
Outdated
platform: mac
target: dmg
arch: arm64
- label: macOS x64
runner: macos-15-intel
runner: blacksmith-12vcpu-macos-26
platform: mac
target: dmg
arch: x64
- label: Linux x64
runner: ubuntu-24.04
runner: blacksmith-32vcpu-ubuntu-2404
platform: linux
target: AppImage
arch: x64
- label: Windows x64
runner: windows-2022
runner: windows-2022 # blacksmith-32vcpu-windows-2025
platform: win
target: nsis
arch: x64
# - label: Windows arm64
# runner: windows-11-arm
# platform: win
# target: nsis
# arch: arm64
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down Expand Up @@ -269,6 +274,18 @@ jobs:
done
fi

# Enable if Windows arm64 builds are enabled.
# Windows updater metadata is channel-specific (for example
# "latest.yml" or "nightly.yml"). Suffix each per-arch copy so the
# release job can merge matching arm64/x64 manifests back into one
# canonical manifest per channel.
# if [[ "${{ matrix.platform }}" == "win" ]]; then
# shopt -s nullglob
# for manifest in release-publish/*.yml; do
# mv "$manifest" "${manifest%.yml}-win-${{ matrix.arch }}.yml"
# done
# fi

- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
Expand Down Expand Up @@ -305,11 +322,40 @@ jobs:
for x64_manifest in release-assets/*-mac-x64.yml; do
arm64_manifest="${x64_manifest%-x64.yml}.yml"
if [[ -f "$arm64_manifest" ]]; then
node scripts/merge-mac-update-manifests.ts "$arm64_manifest" "$x64_manifest"
node scripts/merge-update-manifests.ts --platform mac "$arm64_manifest" "$x64_manifest"
Comment thread
capy-ai[bot] marked this conversation as resolved.
rm -f "$x64_manifest"
fi
done

# - name: Merge Windows updater manifests
# run: |
# shopt -s nullglob
# found_windows_manifest=false
# for x64_manifest in release-assets/*-win-x64.yml; do
# if [[ "$(basename "$x64_manifest")" == builder-debug-* ]]; then
# continue
# fi

# arm64_manifest="${x64_manifest/-x64.yml/-arm64.yml}"
# output_manifest="${x64_manifest/-win-x64.yml/.yml}"
# if [[ ! -f "$arm64_manifest" ]]; then
# echo "Missing matching arm64 Windows manifest for $x64_manifest" >&2
# exit 1
# fi

# found_windows_manifest=true
# node scripts/merge-update-manifests.ts --platform win \
# "$arm64_manifest" \
# "$x64_manifest" \
# "$output_manifest"
# rm -f "$arm64_manifest" "$x64_manifest"
# done

# if [[ "$found_windows_manifest" != true ]]; then
# echo "No Windows updater manifests found to merge." >&2
# exit 1
# fi

- name: Publish release
if: needs.preflight.outputs.previous_tag != ''
uses: softprops/action-gh-release@v2
Expand Down Expand Up @@ -392,6 +438,9 @@ jobs:
with:
node-version-file: package.json

- name: Install dependencies
run: bun install --frozen-lockfile

- id: update_versions
name: Update version strings
env:
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,3 @@ apps/web/src/components/__screenshots__
__screenshots__/
.tanstack
squashfs-root/
core.*
9 changes: 5 additions & 4 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"name": "@t3tools/desktop",
"version": "0.0.17",
"version": "0.0.20",
"private": true,
"main": "dist-electron/main.js",
"type": "module",
"main": "dist-electron/main.cjs",
"scripts": {
"dev": "bun run --parallel dev:bundle dev:electron",
"dev:bundle": "tsdown --watch",
"dev:electron": "bun run scripts/dev-electron.mjs",
"dev:electron": "node scripts/dev-electron.mjs",
"build": "tsdown",
"start": "bun run scripts/start-electron.mjs",
"start": "node scripts/start-electron.mjs",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"smoke-test": "node scripts/smoke-test.mjs"
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/scripts/dev-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ if (!Number.isInteger(port) || port <= 0) {
}

const requiredFiles = [
"dist-electron/main.js",
"dist-electron/preload.js",
"dist-electron/main.cjs",
"dist-electron/preload.cjs",
"../server/dist/bin.mjs",
];
const watchedDirectories = [
{ directory: "dist-electron", files: new Set(["main.js", "preload.js"]) },
{ directory: "dist-electron", files: new Set(["main.cjs", "preload.cjs"]) },
{ directory: "../server/dist", files: new Set(["bin.mjs"]) },
];
const forcedShutdownTimeoutMs = 1_500;
Expand Down Expand Up @@ -69,7 +69,7 @@ function startApp() {

const app = spawn(
resolveElectronPath(),
[`--t3code-dev-root=${desktopDir}`, "dist-electron/main.js"],
[`--t3code-dev-root=${desktopDir}`, "dist-electron/main.cjs"],
{
cwd: desktopDir,
env: childEnv,
Expand Down
44 changes: 7 additions & 37 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
statSync,
writeFileSync,
Expand All @@ -20,7 +19,7 @@ import { fileURLToPath } from "node:url";
const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
const APP_BUNDLE_ID = isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code";
const LAUNCHER_VERSION = 1;
const LAUNCHER_VERSION = 2;

const __dirname = dirname(fileURLToPath(import.meta.url));
export const desktopDir = resolve(__dirname, "..");
Expand Down Expand Up @@ -121,40 +120,6 @@ function patchMainBundleInfoPlist(appBundlePath, iconPath) {
copyFileSync(iconPath, join(resourcesDir, "electron.icns"));
}

function patchHelperBundleInfoPlists(appBundlePath) {
const frameworksDir = join(appBundlePath, "Contents", "Frameworks");
if (!existsSync(frameworksDir)) {
return;
}

for (const entry of readdirSync(frameworksDir, { withFileTypes: true })) {
if (!entry.isDirectory() || !entry.name.endsWith(".app")) {
continue;
}
if (!entry.name.startsWith("Electron Helper")) {
continue;
}

const helperPlistPath = join(frameworksDir, entry.name, "Contents", "Info.plist");
if (!existsSync(helperPlistPath)) {
continue;
}

const suffix = entry.name.replace("Electron Helper", "").replace(".app", "").trim();
const helperName = suffix
? `${APP_DISPLAY_NAME} Helper ${suffix}`
: `${APP_DISPLAY_NAME} Helper`;
const helperIdSuffix = suffix.replace(/[()]/g, "").trim().toLowerCase().replace(/\s+/g, "-");
const helperBundleId = helperIdSuffix
? `${APP_BUNDLE_ID}.helper.${helperIdSuffix}`
: `${APP_BUNDLE_ID}.helper`;

setPlistString(helperPlistPath, "CFBundleDisplayName", helperName);
setPlistString(helperPlistPath, "CFBundleName", helperName);
setPlistString(helperPlistPath, "CFBundleIdentifier", helperBundleId);
}
}

function readJson(path) {
try {
return JSON.parse(readFileSync(path, "utf8"));
Expand Down Expand Up @@ -192,7 +157,6 @@ function buildMacLauncher(electronBinaryPath) {
rmSync(targetAppBundlePath, { recursive: true, force: true });
cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true });
patchMainBundleInfoPlist(targetAppBundlePath, iconPath);
patchHelperBundleInfoPlists(targetAppBundlePath);
writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);

return targetBinaryPath;
Expand All @@ -206,5 +170,11 @@ export function resolveElectronPath() {
return electronBinaryPath;
}

// Dev launches do not need a renamed app bundle badly enough to risk breaking
// Electron helper resource lookup on macOS.
if (isDevelopment) {
return electronBinaryPath;
}

return buildMacLauncher(electronBinaryPath);
}
2 changes: 1 addition & 1 deletion apps/desktop/scripts/smoke-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const desktopDir = resolve(__dirname, "..");
const electronBin = resolve(desktopDir, "node_modules/.bin/electron");
const mainJs = resolve(desktopDir, "dist-electron/main.js");
const mainJs = resolve(desktopDir, "dist-electron/main.cjs");

console.log("\nLaunching Electron smoke test...");

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/scripts/start-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs";
const childEnv = { ...process.env };
delete childEnv.ELECTRON_RUN_AS_NODE;

const child = spawn(resolveElectronPath(), ["dist-electron/main.js"], {
const child = spawn(resolveElectronPath(), ["dist-electron/main.cjs"], {
stdio: "inherit",
cwd: desktopDir,
env: childEnv,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/appBranding.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";

import { resolveDesktopAppBranding, resolveDesktopAppStageLabel } from "./appBranding";
import { resolveDesktopAppBranding, resolveDesktopAppStageLabel } from "./appBranding.ts";

describe("resolveDesktopAppStageLabel", () => {
it("uses Dev in desktop development", () => {
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/appBranding.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { DesktopAppBranding, DesktopAppStageLabel } from "@t3tools/contracts";

import { isNightlyDesktopVersion } from "./updateChannels.ts";

const APP_BASE_NAME = "T3 Code";
const NIGHTLY_VERSION_PATTERN = /-nightly\.\d{8}\.\d+$/;

export function resolveDesktopAppStageLabel(input: {
readonly isDevelopment: boolean;
Expand All @@ -11,7 +12,7 @@ export function resolveDesktopAppStageLabel(input: {
return "Dev";
}

return NIGHTLY_VERSION_PATTERN.test(input.appVersion) ? "Nightly" : "Alpha";
return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Alpha";
}

export function resolveDesktopAppBranding(input: {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/backendPort.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";

import { resolveDesktopBackendPort } from "./backendPort";
import { resolveDesktopBackendPort } from "./backendPort.ts";

describe("resolveDesktopBackendPort", () => {
it("returns the starting port when it is available", async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/backendReadiness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
BackendReadinessAbortedError,
isBackendReadinessAborted,
waitForHttpReady,
} from "./backendReadiness";
} from "./backendReadiness.ts";

describe("waitForHttpReady", () => {
it("returns once the backend serves the requested readiness path", async () => {
Expand Down
24 changes: 5 additions & 19 deletions apps/desktop/src/clientPersistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
writeSavedEnvironmentRegistry,
writeSavedEnvironmentSecret,
type DesktopSecretStorage,
} from "./clientPersistence";
} from "./clientPersistence.ts";

const tempDirectories: string[] = [];

Expand Down Expand Up @@ -52,6 +52,10 @@ const clientSettings: ClientSettings = {
confirmThreadArchive: true,
confirmThreadDelete: false,
diffWordWrap: true,
sidebarProjectGroupingMode: "repository_path",
sidebarProjectGroupingOverrides: {
"environment-1:/tmp/project-a": "separate",
},
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
timestampFormat: "24-hour",
Expand All @@ -75,24 +79,6 @@ describe("clientPersistence", () => {
expect(readClientSettings(settingsPath)).toEqual(clientSettings);
});

it("migrates partial persisted client settings with schema defaults", () => {
const settingsPath = makeTempPath("client-settings.json");
fs.writeFileSync(
settingsPath,
`${JSON.stringify({ settings: { confirmThreadArchive: true, timestampFormat: "24-hour" } })}\n`,
"utf8",
);

expect(readClientSettings(settingsPath)).toEqual({
confirmThreadArchive: true,
confirmThreadDelete: true,
diffWordWrap: false,
sidebarProjectSortOrder: "updated_at",
sidebarThreadSortOrder: "updated_at",
timestampFormat: "24-hour",
});
});

it("persists and reloads saved environment metadata", () => {
const registryPath = makeTempPath("saved-environments.json");

Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/clientPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
type ClientSettings,
type PersistedSavedEnvironmentRecord,
} from "@t3tools/contracts";
import * as Schema from "effect/Schema";
import { Predicate } from "effect";
import * as Schema from "effect/Schema";

interface ClientSettingsDocument {
readonly settings: ClientSettings;
Expand Down Expand Up @@ -88,12 +88,12 @@ function toPersistedSavedEnvironmentRecord(
}

export function readClientSettings(settingsPath: string): ClientSettings | null {
const settings = readJsonFile<ClientSettingsDocument>(settingsPath)?.settings;
if (!settings) {
const raw = readJsonFile<ClientSettingsDocument>(settingsPath)?.settings;
if (!raw) {
return null;
}
try {
return Schema.decodeSync(ClientSettingsSchema)(settings);
return Schema.decodeUnknownSync(ClientSettingsSchema)(raw);
} catch {
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/confirmDialog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ vi.mock("electron", () => ({
},
}));

import { showDesktopConfirmDialog } from "./confirmDialog";
import { showDesktopConfirmDialog } from "./confirmDialog.ts";

describe("showDesktopConfirmDialog", () => {
beforeEach(() => {
Expand Down
Loading
Loading