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
34 changes: 29 additions & 5 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ import {
getInstallationStatus,
PromiseReturnType,
} from './install-deps';
import {
setRoundedCorners,
setTransparentTitlebar,
setVibrancy,
} from './native/macos-window';
import { registerUpdateIpcHandlers, update } from './update';
import {
getEmailFolderPath,
Expand Down Expand Up @@ -2683,15 +2688,12 @@ async function createWindow() {
show: false, // Don't show until content is ready to avoid white screen
// Only use transparency on macOS and Linux (not supported well on Windows)
transparent: !isWindows,
// macOS-only visual effects
vibrancy: isMac ? 'sidebar' : undefined,
visualEffectState: isMac ? 'active' : undefined,
// Solid background on Windows (respect dark/light mode), semi-transparent on macOS/Linux
// Solid background on Windows (respect dark/light mode), fully transparent on macOS for native vibrancy
backgroundColor: isWindows
? nativeTheme.shouldUseDarkColors
? '#1e1e1e'
: '#ffffff'
: '#f5f5f580',
: '#00000000',
// macOS-specific title bar styling
titleBarStyle: isMac ? 'hidden' : undefined,
trafficLightPosition: isMac ? { x: 10, y: 10 } : undefined,
Expand All @@ -2715,6 +2717,28 @@ async function createWindow() {
},
});

// Apply native macOS effects
if (process.platform === 'darwin') {
win.once('ready-to-show', () => {
if (win && !win.isDestroyed()) {
try {
// Apply vibrancy with HUDWindow material (or others like 'Sidebar', 'UnderWindowBackground')
setVibrancy(win, 'HUDWindow');

// Apply rounded corners
setRoundedCorners(win, 20);

// Make titlebar transparent
setTransparentTitlebar(win);

log.info('[MacOS] Applied native visual effects');
} catch (error) {
log.error('[MacOS] Failed to apply native visual effects:', error);
}
}
});
}

// ==================== Handle renderer crashes and failed loads ====================
win.webContents.on('render-process-gone', (event, details) => {
log.error('[RENDERER] Process gone:', details.reason, details.exitCode);
Expand Down
245 changes: 245 additions & 0 deletions electron/main/native/macos-window.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========

import { BrowserWindow } from 'electron';
import koffi from 'koffi';
import os from 'os';

// NSVisualEffectView material constants (enum values)
export const NSVisualEffectMaterial = {
Titlebar: 3,
Selection: 4,
Menu: 5,
Popover: 6,
Sidebar: 7,
HeaderView: 10,
Sheet: 11,
WindowBackground: 12,
HUDWindow: 13,
FullScreenUI: 15,
ToolTip: 17,
ContentBackground: 18,
UnderWindowBackground: 21,
UnderPageBackground: 22,
} as const;

export type MaterialType = keyof typeof NSVisualEffectMaterial;

// Interface for our module functions
interface MacWindowUtils {
setVibrancy: (window: BrowserWindow, material?: MaterialType) => void;
setRoundedCorners: (window: BrowserWindow, radius?: number) => void;
setTransparentTitlebar: (window: BrowserWindow) => void;
}

let utils: MacWindowUtils;

if (os.platform() === 'darwin') {
try {
const objc = koffi.load('libobjc.A.dylib');

// Types
const Ptr = 'size_t';

const objc_getClass = objc.func('objc_getClass', Ptr, ['string']);
const sel_registerName = objc.func('sel_registerName', Ptr, ['string']);
const objc_msgSend = objc.func('objc_msgSend', Ptr, [Ptr, Ptr]);
const objc_msgSend_long = objc.func('objc_msgSend', Ptr, [
Ptr,
Ptr,
'long',
]);
const objc_msgSend_double = objc.func('objc_msgSend', Ptr, [
Ptr,
Ptr,
'double',
]);
const objc_msgSend_bool = objc.func('objc_msgSend', Ptr, [
Ptr,
Ptr,
'bool',
]);

const NSRect = koffi.struct('NSRect', {
x: 'double',
y: 'double',
width: 'double',
height: 'double',
});

const NSVisualEffectBlendingMode = {
BehindWindow: 0,
WithinWindow: 1,
};

utils = {
setVibrancy: (
window: BrowserWindow,
material: MaterialType = 'HUDWindow'
) => {
try {
const windowHandle = window.getNativeWindowHandle();
if (windowHandle.length === 0) return;

// Electron calls valid native handle returns the NSView (BridgedContentView) on macOS
const nsViewPtr = windowHandle.readBigUInt64LE();
if (!nsViewPtr) return;

// Selectors
const selAlloc = sel_registerName('alloc');
const selInit = sel_registerName('init');
const selSetMaterial = sel_registerName('setMaterial:');
const selSetBlendingMode = sel_registerName('setBlendingMode:');
const selSetState = sel_registerName('setState:');
const selSetAutoresizingMask = sel_registerName(
'setAutoresizingMask:'
);
const selSetFrame = sel_registerName('setFrame:');
const selAddSubview = sel_registerName(
'addSubview:positioned:relativeTo:'
);

const NSVisualEffectViewClass = objc_getClass('NSVisualEffectView');
if (!NSVisualEffectViewClass) return;

// Allocation
const visualEffectView = objc_msgSend(
NSVisualEffectViewClass,
selAlloc
);
objc_msgSend(visualEffectView, selInit);

const materialValue =
NSVisualEffectMaterial[material] ||
NSVisualEffectMaterial.HUDWindow;

// Configuration
objc_msgSend_long(visualEffectView, selSetMaterial, materialValue);
objc_msgSend_long(
visualEffectView,
selSetBlendingMode,
NSVisualEffectBlendingMode.BehindWindow
);
objc_msgSend_long(visualEffectView, selSetState, 1);
objc_msgSend_long(visualEffectView, selSetAutoresizingMask, 18);

// Frame
const bounds = window.getBounds();
const viewFrame = {
x: 0,
y: 0,
width: bounds.width,
height: bounds.height,
};

const objc_msgSend_frame = objc.func('objc_msgSend', 'void', [
Ptr,
Ptr,
NSRect,
]);
objc_msgSend_frame(visualEffectView, selSetFrame, viewFrame);

// Add Subview to the CONTENT VIEW (which we already have as nsViewPtr)
const objc_msgSend_positioned = objc.func('objc_msgSend', 'void', [
Ptr,
Ptr,
Ptr,
'long',
Ptr,
]);
objc_msgSend_positioned(
nsViewPtr,
selAddSubview,
visualEffectView,
-1,
0
); // -1 = NSWindowBelow

console.log(`[MacOS] Vibrancy applied successfully`);
} catch (error) {
console.error('[MacOS] Error applying vibrancy:', error);
}
},

setRoundedCorners: (window: BrowserWindow, radius = 20) => {
try {
const windowHandle = window.getNativeWindowHandle();
const nsViewPtr = windowHandle.readBigUInt64LE();

const selLayer = sel_registerName('layer');
const selSetWantsLayer = sel_registerName('setWantsLayer:');
const selSetCornerRadius = sel_registerName('setCornerRadius:');
const selSetMasksToBounds = sel_registerName('setMasksToBounds:');

// Ensure layer-backing
objc_msgSend_bool(nsViewPtr, selSetWantsLayer, true);

// Get layer
const nsLayer = objc_msgSend(nsViewPtr, selLayer);
if (!nsLayer) return console.error('[MacOS] Failed to get layer');

// Apply Corner Radius
objc_msgSend_double(nsLayer, selSetCornerRadius, radius);
objc_msgSend_bool(nsLayer, selSetMasksToBounds, true);

console.log(`[MacOS] Rounded corners applied: ${radius}`);
} catch (error) {
console.error('[MacOS] Error applying rounded corners:', error);
}
},

setTransparentTitlebar: (window: BrowserWindow) => {
try {
const windowHandle = window.getNativeWindowHandle();
const nsViewPtr = windowHandle.readBigUInt64LE();

// We have the View, we need the Window
const selWindow = sel_registerName('window');
const nsWindowPtr = objc_msgSend(nsViewPtr, selWindow);

if (!nsWindowPtr)
return console.error('[MacOS] Failed to get NSWindow from NSView');

const selSetTitlebarAppearsTransparent = sel_registerName(
'setTitlebarAppearsTransparent:'
);
objc_msgSend_bool(
nsWindowPtr,
selSetTitlebarAppearsTransparent,
true
);

console.log('[MacOS] Transparent titlebar applied');
} catch (error) {
console.error('[MacOS] Error setting transparent titlebar:', error);
}
},
};
} catch (e) {
console.error('[MacOS] Failed to load native libraries:', e);
utils = {
setVibrancy: () => {},
setRoundedCorners: () => {},
setTransparentTitlebar: () => {},
};
}
} else {
utils = {
setVibrancy: () => {},
setRoundedCorners: () => {},
setTransparentTitlebar: () => {},
};
}

export const { setVibrancy, setRoundedCorners, setTransparentTitlebar } = utils;
22 changes: 11 additions & 11 deletions src/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const SelectTrigger = React.forwardRef<
return (
<div className={cn('w-fit', stateCls.wrapper)}>
{title ? (
<div className="mb-1.5 flex items-center gap-1 text-body-sm font-bold text-text-heading">
<div className="mb-1.5 gap-1 text-body-sm font-bold text-text-heading flex items-center">
<span>{title}</span>
{required && <span className="text-text-body">*</span>}
{tooltip && (
Expand All @@ -116,16 +116,16 @@ const SelectTrigger = React.forwardRef<
disabled={disabled}
className={cn(
// Base styles
'relative flex w-full items-center justify-between gap-2 rounded-lg border border-solid px-3 text-text-body outline-none transition-all',
'gap-2 rounded-lg px-3 text-text-body relative flex w-full items-center justify-between border border-solid transition-all outline-none',
sizeClasses[size],
'whitespace-nowrap [&>span]:line-clamp-1',
// Default state (when no error/success)
!state && 'bg-input-bg-default',
// Interactive states (only when no error/success state)
state !== 'error' &&
state !== 'success' && [
'hover:bg-input-bg-hover hover:ring-1 hover:ring-input-border-hover hover:ring-offset-0',
'focus-visible:ring-1 focus-visible:ring-input-border-focus focus-visible:ring-offset-0 data-[state=open]:bg-input-bg-input data-[state=open]:ring-1 data-[state=open]:ring-input-border-focus data-[state=open]:ring-offset-0',
'hover:bg-input-bg-hover hover:ring-input-border-hover hover:ring-1 hover:ring-offset-0',
'focus-visible:ring-input-border-focus data-[state=open]:bg-input-bg-input data-[state=open]:ring-input-border-focus focus-visible:ring-1 focus-visible:ring-offset-0 data-[state=open]:ring-1 data-[state=open]:ring-offset-0',
],
// Validation states (override defaults)
stateCls.trigger,
Expand Down Expand Up @@ -156,7 +156,7 @@ const SelectScrollUpButton = React.forwardRef<
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
'py-1 flex cursor-default items-center justify-center',
className
)}
{...props}
Expand All @@ -173,7 +173,7 @@ const SelectScrollDownButton = React.forwardRef<
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
'py-1 flex cursor-default items-center justify-center',
className
)}
{...props}
Expand All @@ -192,7 +192,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
'text-popover-foreground relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] origin-[--radix-select-content-transform-origin] overflow-y-auto overflow-x-hidden rounded-lg border border-solid border-transparent bg-input-bg-default shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'text-popover-foreground rounded-xl bg-input-bg-default backdrop-blur-md shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] origin-[--radix-select-content-transform-origin] overflow-x-hidden overflow-y-auto border border-solid border-transparent',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
Expand Down Expand Up @@ -235,12 +235,12 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-pointer select-none items-center rounded-lg py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-menutabs-fill-hover data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'focus:bg-accent focus:text-accent-foreground rounded-lg py-1.5 pl-2 pr-8 text-sm hover:bg-menutabs-fill-hover relative flex w-full cursor-pointer items-center outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<span className="right-2 h-3.5 w-3.5 absolute flex items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
Expand Down Expand Up @@ -292,12 +292,12 @@ const SelectItemWithButton = React.forwardRef<
value={value}
disabled={!enabled}
className={cn(
'focus:bg-accent focus:text-accent-foreground group relative flex w-full cursor-pointer select-none items-center rounded-lg py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-menutabs-fill-hover data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'focus:bg-accent focus:text-accent-foreground group rounded-lg py-1.5 pl-2 pr-8 text-sm hover:bg-menutabs-fill-hover relative flex w-full cursor-pointer items-center outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<span className="right-2 h-3.5 w-3.5 absolute flex items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
Expand Down
2 changes: 1 addition & 1 deletion src/style/token.css
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@
--text-success-default: var(--text-success);
--text-caution: var(--text-cuation);
--text-cuation-default: var(--text-cuation);
--surface-primary: var(--colors-off-white-80);
--surface-primary: var(--colors-off-white-50);
--surface-secondary: var(--colors-off-white-50);
--surface-success: var(--colors-green-50);
--surface-information: var(--colors-blue-50);
Expand Down
Loading