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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
29 changes: 29 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Third-party notices

## Synara

The workspace design, icon assets, ASCII background, and native window blur include adaptations from
[Synara](https://github.com/Emanuele-web04/synara), revision 299fd573b1d6520c323a87c78d30be74e87e53e1.

MIT License

Copyright (c) 2026 T3 Tools Inc.
Copyright (c) 2026 Emanuele Di Pietro

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
105 changes: 105 additions & 0 deletions apps/desktop/native/window-material/WindowMaterial.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// FILE: WindowMaterial.m
// Purpose: Node-API addon that sets the macOS window-server background blur radius for a
// BrowserWindow, so the translucent shell can show the desktop at any blur amount.
// Layer: Desktop native (macOS only, loaded in the Electron main process)
// Exports: setBackgroundBlurRadius(nativeWindowHandle: Buffer, radius: number) => boolean
//
// Electron only exposes vibrancy on/off. The adjustable blur behind a window comes from
// the private CGSSetWindowBackgroundBlurRadius call (the one iTerm2 and Ghostty use).
// WindowServer only accepts it from the connection that owns the window, so it has to
// run in-process rather than in the AppSnap helper. Both symbols are resolved with dlsym:
// if a future macOS drops them the addon still loads and reports failure, and the caller
// falls back to plain vibrancy.

#import <AppKit/AppKit.h>
#include <dlfcn.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

// ─── Node-API (ABI-stable C surface, declared here to avoid a headers dependency) ───

typedef struct napi_env__* napi_env;
typedef struct napi_value__* napi_value;
typedef struct napi_callback_info__* napi_callback_info;
typedef int napi_status;
typedef napi_value (*napi_callback)(napi_env env, napi_callback_info info);

extern napi_status napi_get_cb_info(napi_env env, napi_callback_info info, size_t* argc,
napi_value* argv, napi_value* this_arg, void** data);
extern napi_status napi_get_buffer_info(napi_env env, napi_value value, void** data,
size_t* length);
extern napi_status napi_get_value_int32(napi_env env, napi_value value, int32_t* result);
extern napi_status napi_get_boolean(napi_env env, bool value, napi_value* result);
extern napi_status napi_create_function(napi_env env, const char* utf8name, size_t length,
napi_callback cb, void* data, napi_value* result);
extern napi_status napi_set_named_property(napi_env env, napi_value object, const char* utf8name,
napi_value value);

// ─── Private window-server calls ───

typedef int32_t CGSConnectionID;
typedef int32_t (*CGSMainConnectionIDFn)(void);
typedef int32_t (*CGSSetWindowBackgroundBlurRadiusFn)(CGSConnectionID connection,
int32_t windowNumber, int32_t radius);

static const int32_t kMaxBlurRadius = 100;

static bool setBlurRadius(NSWindow* window, int32_t radius) {
static CGSMainConnectionIDFn mainConnectionID = NULL;
static CGSSetWindowBackgroundBlurRadiusFn setWindowBlurRadius = NULL;
static bool resolved = false;
if (!resolved) {
resolved = true;
mainConnectionID = (CGSMainConnectionIDFn)dlsym(RTLD_DEFAULT, "CGSMainConnectionID");
setWindowBlurRadius = (CGSSetWindowBackgroundBlurRadiusFn)dlsym(
RTLD_DEFAULT, "CGSSetWindowBackgroundBlurRadius");
}
if (mainConnectionID == NULL || setWindowBlurRadius == NULL) return false;

// The blur only shows through a non-opaque window with a clear background. A fully clear
// background (alpha 0) with a native shadow makes macOS draw a gap between the window
// border and its contents, so keep a sliver of alpha and have the shadow recomputed.
[window setOpaque:NO];
[window setBackgroundColor:[[NSColor clearColor] colorWithAlphaComponent:0.01]];
bool ok = setWindowBlurRadius(mainConnectionID(), (int32_t)[window windowNumber], radius) == 0;
[window setHasShadow:YES];
[window invalidateShadow];
return ok;
}

static napi_value SetBackgroundBlurRadius(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value argv[2] = {NULL, NULL};
bool ok = false;

void* handleData = NULL;
size_t handleLength = 0;
int32_t radius = 0;
if (napi_get_cb_info(env, info, &argc, argv, NULL, NULL) == 0 && argc == 2 &&
napi_get_buffer_info(env, argv[0], &handleData, &handleLength) == 0 &&
handleLength == sizeof(void*) && napi_get_value_int32(env, argv[1], &radius) == 0) {
// BrowserWindow.getNativeWindowHandle() on macOS is a buffer holding an NSView*.
NSView* view = (__bridge NSView*)(*(void**)handleData);
NSWindow* window = [view window];
if (window != nil) {
if (radius < 0) radius = 0;
if (radius > kMaxBlurRadius) radius = kMaxBlurRadius;
ok = setBlurRadius(window, radius);
}
}

napi_value result = NULL;
napi_get_boolean(env, ok, &result);
return result;
}

__attribute__((visibility("default"))) napi_value napi_register_module_v1(napi_env env,
napi_value exports) {
napi_value fn = NULL;
if (napi_create_function(env, "setBackgroundBlurRadius", (size_t)-1, SetBackgroundBlurRadius,
NULL, &fn) == 0) {
napi_set_named_property(env, exports, "setBackgroundBlurRadius", fn);
}
return exports;
}
84 changes: 84 additions & 0 deletions apps/desktop/scripts/build-window-material.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import * as NodeChildProcess from "node:child_process";
import * as NodeCrypto from "node:crypto";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as NodeURL from "node:url";
import * as NodeUtil from "node:util";

// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone native build targets the actual host.
const hostPlatform = NodeOS.platform();
// oxlint-disable-next-line t3code/no-global-process-runtime -- The compiler needs the host architecture as its default.
const hostArch = NodeOS.arch();
const { values } = NodeUtil.parseArgs({
options: { output: { type: "string" }, arch: { type: "string", default: hostArch } },
});

if (hostPlatform === "darwin") {
const architectures = { arm64: ["arm64"], x64: ["x86_64"], universal: ["arm64", "x86_64"] }[
values.arch
];
if (!architectures) throw new Error(`Unsupported macOS architecture: ${values.arch}`);
const desktopDirectory = NodeURL.fileURLToPath(new URL("../", import.meta.url));
const source = NodePath.join(desktopDirectory, "native/window-material/WindowMaterial.m");
const output =
values.output ??
NodePath.join(desktopDirectory, ".electron-runtime/window-material/otter-window-material.node");
const metadata = `${output}.build.json`;
const fingerprint = NodeCrypto.createHash("sha256")
.update(values.arch)
.update(NodeFS.readFileSync(source))
.update(NodeFS.readFileSync(NodeURL.fileURLToPath(import.meta.url)))
.digest("hex");
let current = false;
try {
current =
JSON.parse(NodeFS.readFileSync(metadata, "utf8")).fingerprint === fingerprint &&
NodeChildProcess.spawnSync("codesign", ["--verify", "--strict", output], {
stdio: "ignore",
}).status === 0;
} catch {}

if (!current) {
const directory = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "otter-window-material-"));
const temporary = NodePath.join(directory, "otter-window-material.node");
try {
NodeChildProcess.execFileSync(
"xcrun",
[
"clang",
"-x",
"objective-c",
"-fobjc-arc",
"-O2",
"-Wall",
"-Wextra",
"-Werror",
...architectures.flatMap((arch) => ["-arch", arch]),
"-mmacosx-version-min=12.3",
"-bundle",
"-undefined",
"dynamic_lookup",
"-framework",
"AppKit",
source,
"-o",
temporary,
],
{ stdio: "inherit", env: { ...process.env, CLANG_MODULE_CACHE_PATH: directory } },
);
NodeChildProcess.execFileSync(
"codesign",
["--force", "--sign", "-", "--timestamp=none", temporary],
{ stdio: "inherit" },
);
NodeFS.mkdirSync(NodePath.dirname(output), { recursive: true });
const pendingOutput = `${output}.${process.pid}.tmp`;
NodeFS.copyFileSync(temporary, pendingOutput);
NodeFS.renameSync(pendingOutput, output);
NodeFS.writeFileSync(metadata, `${JSON.stringify({ fingerprint })}\n`);
} finally {
NodeFS.rmSync(directory, { recursive: true, force: true });
}
}
}
5 changes: 5 additions & 0 deletions apps/desktop/scripts/dev-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ NodeChildProcess.execFileSync(
[NodePath.join(desktopDir, "scripts/build-browser-secret.mjs")],
{ stdio: "inherit" },
);
NodeChildProcess.execFileSync(
process.execPath,
[NodePath.join(desktopDir, "scripts/build-window-material.mjs")],
{ stdio: "inherit" },
);

await waitForResources({
baseDir: desktopDir,
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/scripts/start-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ NodeChildProcess.execFileSync(
[NodePath.join(desktopDir, "scripts/build-browser-secret.mjs")],
{ stdio: "inherit" },
);
NodeChildProcess.execFileSync(
process.execPath,
[NodePath.join(desktopDir, "scripts/build-window-material.mjs")],
{ stdio: "inherit" },
);

const childEnv = { ...process.env };
delete childEnv.ELECTRON_RUN_AS_NODE;
Expand Down
84 changes: 61 additions & 23 deletions apps/desktop/src/app/DesktopAppIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const withIdentity = <A, E, R>(
readonly calls?: ElectronAppCalls;
readonly environment?: TestEnvironmentInput;
readonly legacyPathExists?: boolean;
readonly legacyPathProbeError?: PlatformError.PlatformError;
readonly pathProbeError?: PlatformError.PlatformError;
readonly packageJson?: string;
readonly pngIconPath?: Option.Option<string>;
} = {},
Expand All @@ -131,8 +131,8 @@ const withIdentity = <A, E, R>(
Layer.provideMerge(
FileSystem.layerNoop({
exists: (path) =>
input.legacyPathProbeError
? Effect.fail(input.legacyPathProbeError)
input.pathProbeError
? Effect.fail(input.pathProbeError)
: Effect.succeed(
input.legacyPathExists === true && /Otter Code( \(Dev\))?$/.test(path),
),
Expand Down Expand Up @@ -161,30 +161,68 @@ describe("DesktopAppIdentity", () => {
),
);

it.effect("keeps using the legacy development profile", () =>
withIdentity(
Effect.gen(function* () {
const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
assert.equal(
yield* identity.resolveUserDataPath,
"/Users/alice/Library/Application Support/Otter Code (Dev)",
);
}),
{
legacyPathExists: true,
environment: { env: { VITE_DEV_SERVER_URL: "http://localhost:5173" } },
it.effect.each([
{
label: "default development state",
env: { VITE_DEV_SERVER_URL: "http://localhost:5173" },
expectedPath: "/Users/alice/.otter-code/dev/electron",
},
{
label: "an isolated worktree",
env: {
VITE_DEV_SERVER_URL: "http://localhost:5173",
T3CODE_HOME: "/worktree/.t3",
},
),
expectedPath: "/worktree/.t3/userdata/electron",
},
])(
"uses the isolated Chromium profile for $label even when the legacy profile exists",
({ env, expectedPath }) =>
withIdentity(
Effect.gen(function* () {
const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
assert.equal(yield* identity.resolveUserDataPath, expectedPath);
}),
{
legacyPathExists: true,
environment: { env },
},
),
);

it.effect(
"does not inspect the legacy development profile when its permissions are denied",
() => {
const cause = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "exists",
pathOrDescriptor: "/Users/alice/Library/Application Support/Otter Code (Dev)",
});
return withIdentity(
Effect.gen(function* () {
const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
assert.equal(
yield* identity.resolveUserDataPath,
"/Users/alice/.otter-code/dev/electron",
);
}),
{
pathProbeError: cause,
environment: { env: { VITE_DEV_SERVER_URL: "http://localhost:5173" } },
},
);
},
);

it.effect("preserves failures while inspecting the legacy userData path", () => {
const legacyPath = "/Users/alice/Library/Application Support/Otter Code (Dev)";
it.effect("preserves failures while inspecting production Windows credential state", () => {
const statePath = "/Users/alice/AppData/Roaming/otter-code/Local State";
const cause = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "exists",
description: "permission denied",
pathOrDescriptor: legacyPath,
pathOrDescriptor: statePath,
});

return withIdentity(
Expand All @@ -193,16 +231,16 @@ describe("DesktopAppIdentity", () => {
const error = yield* identity.resolveUserDataPath.pipe(Effect.flip);

assert.instanceOf(error, DesktopUserData.DesktopUserDataInitializationError);
assert.equal(error.resourcePath, legacyPath);
assert.equal(error.resourcePath, statePath);
assert.strictEqual(error.cause, cause);
assert.equal(
error.message,
`Could not initialize Electron user data during inspect at ${legacyPath} (PermissionDenied).`,
`Could not initialize Electron user data during inspect at ${statePath} (PermissionDenied).`,
);
}),
{
legacyPathProbeError: cause,
environment: { env: { VITE_DEV_SERVER_URL: "http://localhost:5173" } },
pathProbeError: cause,
environment: { platform: "win32" },
},
);
});
Expand Down
Loading
Loading