Skip to content
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
80 changes: 74 additions & 6 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
readdirSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { createRequire } from "node:module";
Expand All @@ -18,8 +19,8 @@ import { fileURLToPath } from "node:url";

const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
const APP_DISPLAY_NAME = isDevelopment ? "Kodo Code (Dev)" : "Kodo Code (Alpha)";
const APP_BUNDLE_ID = isDevelopment ? "com.kodo.code.dev" : "com.kodo.code";
const LAUNCHER_VERSION = 2;
const APP_BUNDLE_ID = isDevelopment ? "app.kodocode.dev" : "app.kodocode";
const LAUNCHER_VERSION = 4;
const ELECTRON_INSTALL_CANDIDATES = ["node", "bun"];

const __dirname = dirname(fileURLToPath(import.meta.url));
Expand All @@ -46,8 +47,33 @@ function setPlistString(plistPath, key, value) {
throw new Error(`Failed to update plist key "${key}" at ${plistPath}: ${details}`.trim());
}

function ensureMainBundleInfoPlist(appBundlePath) {
const contentsDir = join(appBundlePath, "Contents");
const infoPlistPath = join(contentsDir, "Info.plist");
if (existsSync(infoPlistPath)) {
return infoPlistPath;
}

mkdirSync(contentsDir, { recursive: true });
writeFileSync(
infoPlistPath,
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>Electron</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
</dict>
</plist>
`,
);
return infoPlistPath;
}

function patchMainBundleInfoPlist(appBundlePath, iconPath) {
const infoPlistPath = join(appBundlePath, "Contents", "Info.plist");
const infoPlistPath = ensureMainBundleInfoPlist(appBundlePath);
setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);
setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME);
setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID);
Expand Down Expand Up @@ -92,6 +118,45 @@ function patchHelperBundleInfoPlists(appBundlePath) {
}
}

function repairFrameworkVersionSymlinks(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(".framework")) {
continue;
}

const frameworkPath = join(frameworksDir, entry.name);
const frameworkExecutable = entry.name.replace(/\.framework$/, "");
const versionsDir = join(frameworkPath, "Versions");
const versionAPath = join(versionsDir, "A");
const currentPath = join(versionsDir, "Current");
if (!existsSync(versionAPath)) {
continue;
}

if (!existsSync(currentPath)) {
rmSync(currentPath, { force: true });
symlinkSync("A", currentPath);
}

const executablePath = join(frameworkPath, frameworkExecutable);
if (existsSync(join(versionAPath, frameworkExecutable))) {
rmSync(executablePath, { force: true });
symlinkSync(`Versions/Current/${frameworkExecutable}`, executablePath);
}

const resourcesPath = join(frameworkPath, "Resources");
if (existsSync(join(versionAPath, "Resources"))) {
rmSync(resourcesPath, { force: true });
symlinkSync("Versions/Current/Resources", resourcesPath);
}
}
}

function readJson(path) {
try {
return JSON.parse(readFileSync(path, "utf8"));
Expand Down Expand Up @@ -198,32 +263,35 @@ function buildMacLauncher(electronBinaryPath) {
const runtimeDir = join(desktopDir, ".electron-runtime");
const targetAppBundlePath = join(runtimeDir, `${APP_DISPLAY_NAME}.app`);
const targetBinaryPath = join(targetAppBundlePath, "Contents", "MacOS", "Electron");
const targetInfoPlistPath = join(targetAppBundlePath, "Contents", "Info.plist");
const iconPath = resolveMacLauncherIconPath();
const metadataPath = join(runtimeDir, "metadata.json");

mkdirSync(runtimeDir, { recursive: true });

const expectedMetadata = {
launcherVersion: LAUNCHER_VERSION,
appDisplayName: APP_DISPLAY_NAME,
appBundleId: APP_BUNDLE_ID,
sourceAppBundlePath,
sourceAppMtimeMs: statSync(sourceAppBundlePath).mtimeMs,
appBundleId: APP_BUNDLE_ID,
appDisplayName: APP_DISPLAY_NAME,
iconPath,
iconMtimeMs: statSync(iconPath).mtimeMs,
};

const currentMetadata = readJson(metadataPath);
if (
existsSync(targetBinaryPath) &&
existsSync(targetInfoPlistPath) &&
currentMetadata &&
JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata)
) {
return targetBinaryPath;
}

rmSync(targetAppBundlePath, { recursive: true, force: true });
cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true });
cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true, verbatimSymlinks: true });
repairFrameworkVersionSymlinks(targetAppBundlePath);
patchMainBundleInfoPlist(targetAppBundlePath, iconPath);
patchHelperBundleInfoPlists(targetAppBundlePath);
writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
Expand Down
35 changes: 3 additions & 32 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const LEGACY_DESKTOP_SCHEME = "t3";
const ROOT_DIR = Path.resolve(__dirname, "../../..");
const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
const APP_DISPLAY_NAME = isDevelopment ? "Kodo Code (Dev)" : "Kodo Code (Alpha)";
const APP_USER_MODEL_ID = "com.kodo.code";
const APP_USER_MODEL_ID = "app.kodocode";
const LINUX_DESKTOP_ENTRY_NAME = isDevelopment ? "kodo-code-dev.desktop" : "kodo-code.desktop";
const LINUX_WM_CLASS = isDevelopment ? "kodo-code-dev" : "kodo-code";
const USER_DATA_DIR_NAME = isDevelopment ? "kodo-code-dev" : "kodo-code";
Expand Down Expand Up @@ -803,32 +803,7 @@ function resolveIconPath(ext: "ico" | "icns" | "png"): string | null {
return resolveResourcePath(`icon.${ext}`);
}

function resolveMacDockIcon(): Electron.NativeImage | null {
const iconPath = resolveIconPath("icns");
if (!iconPath) {
return null;
}

const icon = nativeImage.createFromPath(iconPath);
return icon.isEmpty() ? null : icon;
}

function resolveVectorIconPath(): string | null {
const candidate = isDevelopment
? Path.join(ROOT_DIR, "assets/dev/blueprint.svg")
: Path.join(ROOT_DIR, "assets/prod/logo.svg");
return FS.existsSync(candidate) ? candidate : null;
}

function resolveNativeAppIcon(ext: "ico" | "icns" | "png"): Electron.NativeImage | null {
const vectorIconPath = resolveVectorIconPath();
if (vectorIconPath) {
const vectorIcon = nativeImage.createFromPath(vectorIconPath);
if (!vectorIcon.isEmpty()) {
return vectorIcon;
}
}

const rasterIconPath = resolveIconPath(ext);
if (!rasterIconPath) {
return null;
Expand Down Expand Up @@ -885,12 +860,8 @@ function configureAppIdentity(): void {
(app as LinuxDesktopNamedApp).setDesktopName?.(LINUX_DESKTOP_ENTRY_NAME);
}

if (process.platform === "darwin" && app.dock) {
const icon = resolveMacDockIcon();
if (icon) {
app.dock.setIcon(icon);
}
}
// macOS uses CFBundleIconFile from the app bundle. Calling app.dock.setIcon()
// renders the raw image payload and bypasses the bundle icon presentation.
}

function clearUpdatePollTimer(): void {
Expand Down
10 changes: 7 additions & 3 deletions apps/server/src/codexAppServerManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { ApprovalRequestId, ThreadId } from "@t3tools/contracts";
import { ApprovalRequestId, DEFAULT_MODEL_BY_PROVIDER, ThreadId } from "@t3tools/contracts";

import {
buildCodexInitializeParams,
Expand Down Expand Up @@ -249,6 +249,10 @@ describe("normalizeCodexModelSlug", () => {
expect(normalizeCodexModelSlug("gpt-5.3")).toBe("gpt-5.3-codex");
});

it("maps 5.5 aliases to gpt-5.5", () => {
expect(normalizeCodexModelSlug("5.5")).toBe("gpt-5.5");
});

it("prefers codex id when model differs", () => {
expect(normalizeCodexModelSlug("gpt-5.3", "gpt-5.3-codex")).toBe("gpt-5.3-codex");
});
Expand Down Expand Up @@ -381,7 +385,7 @@ describe("resolveCodexModelForAccount", () => {
planType: "plus",
sparkEnabled: false,
}),
).toBe("gpt-5.3-codex");
).toBe(DEFAULT_MODEL_BY_PROVIDER.codex);
});

it("keeps spark for supported plans", () => {
Expand All @@ -401,7 +405,7 @@ describe("resolveCodexModelForAccount", () => {
planType: null,
sparkEnabled: false,
}),
).toBe("gpt-5.3-codex");
).toBe(DEFAULT_MODEL_BY_PROVIDER.codex);
});
});

Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/codexAppServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
parseCodexCliVersion,
} from "./provider/codexCliVersion";
import {
CODEX_DEFAULT_MODEL,
readCodexAccountSnapshot,
resolveCodexModelForAccount,
type CodexAccountSnapshot,
Expand Down Expand Up @@ -384,7 +385,7 @@ function buildCodexCollaborationMode(input: {
}
const codexMode =
input.interactionMode === "plan" || input.interactionMode === "review" ? "plan" : "default";
const model = normalizeCodexModelSlug(input.model) ?? "gpt-5.3-codex";
const model = normalizeCodexModelSlug(input.model) ?? CODEX_DEFAULT_MODEL;
return {
mode: codexMode,
settings: {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/git/Layers/CodexTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => {
subject: "Add important change",
body: "",
}),
requireModel: "gpt-5.4",
requireModel: "gpt-5.4-mini",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;
Expand Down
17 changes: 13 additions & 4 deletions apps/server/src/git/Layers/CodexTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { randomUUID } from "node:crypto";
import { Effect, FileSystem, Layer, Option, Path, Schema, Scope, Stream } from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { CodexModelSelection } from "@t3tools/contracts";
import {
CodexModelSelection,
DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER,
} from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";

import { resolveAttachmentPath } from "../../attachmentStore.ts";
Expand Down Expand Up @@ -163,9 +166,15 @@ const makeCodexTextGeneration = Effect.gen(function* () {

const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () {
// Resolve frontend sentinel models like `auto` before invoking Codex CLI.
const resolvedModelSelection = resolveModelSelectionDefault(
modelSelection,
) as CodexModelSelection;
// Utility text generation should use the cheaper utility default, not the
// interactive composer default.
const resolvedModelSelection =
modelSelection.model.trim().toLowerCase() === "auto"
? ({
...modelSelection,
model: DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER.codex,
} satisfies CodexModelSelection)
: (resolveModelSelectionDefault(modelSelection) as CodexModelSelection);
const normalizedOptions = normalizeCodexModelOptionsWithCapabilities(
getCodexModelCapabilities(resolvedModelSelection.model),
resolvedModelSelection.options,
Expand Down
6 changes: 3 additions & 3 deletions apps/server/src/os-jank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ export function fixPath(
}
}

export const expandHomePath = Effect.fn(function* (input: string) {
return expandHomePathValue(input);
});
export const expandHomePath = Effect.fn((input: string) =>
Effect.succeed(expandHomePathValue(input)),
);

export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) {
const { join, resolve } = yield* Path.Path;
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/persistence/Layers/ProjectionThreads.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as SqlClient from "effect/unstable/sql/SqlClient";
import * as SqlSchema from "effect/unstable/sql/SqlSchema";
import { Effect, Layer, Option, Schema, Struct } from "effect";
import { Effect, Layer, Option, Schema } from "effect";

import { toPersistenceSqlError } from "../Errors.ts";
import {
Expand Down
Loading