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
19 changes: 19 additions & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ syncShellEnvironment();
const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
const CONFIRM_CHANNEL = "desktop:confirm";
const SET_THEME_CHANNEL = "desktop:set-theme";
const SET_WINDOW_OPACITY_CHANNEL = "desktop:set-window-opacity";
const SET_SIDEBAR_OPACITY_CHANNEL = "desktop:set-sidebar-opacity";
const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
const MENU_ACTION_CHANNEL = "desktop:menu-action";
Expand Down Expand Up @@ -1151,6 +1153,23 @@ function registerIpcHandlers(): void {
nativeTheme.themeSource = theme;
});

ipcMain.removeHandler(SET_WINDOW_OPACITY_CHANNEL);
ipcMain.handle(SET_WINDOW_OPACITY_CHANNEL, async (event, rawOpacity: unknown) => {
if (typeof rawOpacity !== "number" || !Number.isFinite(rawOpacity)) return;
const opacity = Math.max(0.3, Math.min(1, rawOpacity));
const window = BrowserWindow.fromWebContents(event.sender);
if (window) {
window.setOpacity(opacity);
}
});

ipcMain.removeHandler(SET_SIDEBAR_OPACITY_CHANNEL);
ipcMain.handle(SET_SIDEBAR_OPACITY_CHANNEL, async (_event, rawOpacity: unknown) => {
// Sidebar opacity is handled purely on the renderer side via CSS.
// This channel exists so the bridge contract is satisfied; the renderer
// applies the value through a CSS custom-property.
});

ipcMain.removeHandler(CONTEXT_MENU_CHANNEL);
ipcMain.handle(
CONTEXT_MENU_CHANNEL,
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { DesktopBridge } from "@okcode/contracts";
const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
const CONFIRM_CHANNEL = "desktop:confirm";
const SET_THEME_CHANNEL = "desktop:set-theme";
const SET_WINDOW_OPACITY_CHANNEL = "desktop:set-window-opacity";
const SET_SIDEBAR_OPACITY_CHANNEL = "desktop:set-sidebar-opacity";
const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
const MENU_ACTION_CHANNEL = "desktop:menu-action";
Expand All @@ -25,6 +27,8 @@ contextBridge.exposeInMainWorld("desktopBridge", {
pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL),
confirm: (message) => ipcRenderer.invoke(CONFIRM_CHANNEL, message),
setTheme: (theme) => ipcRenderer.invoke(SET_THEME_CHANNEL, theme),
setWindowOpacity: (opacity) => ipcRenderer.invoke(SET_WINDOW_OPACITY_CHANNEL, opacity),
setSidebarOpacity: (opacity) => ipcRenderer.invoke(SET_SIDEBAR_OPACITY_CHANNEL, opacity),
showContextMenu: (items, position) => ipcRenderer.invoke(CONTEXT_MENU_CHANNEL, items, position),
openExternal: (url: string) => ipcRenderer.invoke(OPEN_EXTERNAL_CHANNEL, url),
onMenuAction: (listener) => {
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/appSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export const AppSettingsSchema = Schema.Struct({
withDefaults(() => DEFAULT_SIDEBAR_THREAD_SORT_ORDER),
),
timestampFormat: TimestampFormat.pipe(withDefaults(() => DEFAULT_TIMESTAMP_FORMAT)),
windowOpacity: Schema.Number.pipe(withDefaults(() => 1)),
sidebarOpacity: Schema.Number.pipe(withDefaults(() => 1)),
customCodexModels: Schema.Array(Schema.String).pipe(withDefaults(() => [])),
customClaudeModels: Schema.Array(Schema.String).pipe(withDefaults(() => [])),
textGenerationModel: Schema.optional(TrimmedNonEmptyString),
Expand Down Expand Up @@ -136,9 +138,15 @@ export function normalizeCustomModelSlugs(
return normalizedModels;
}

function clampOpacity(value: number): number {
return Math.max(0.3, Math.min(1, value));
}

function normalizeAppSettings(settings: AppSettings): AppSettings {
return {
...settings,
windowOpacity: clampOpacity(settings.windowOpacity),
sidebarOpacity: clampOpacity(settings.sidebarOpacity),
customCodexModels: normalizeCustomModelSlugs(settings.customCodexModels, "codex"),
customClaudeModels: normalizeCustomModelSlugs(settings.customClaudeModels, "claudeAgent"),
};
Expand Down
74 changes: 74 additions & 0 deletions apps/web/src/routes/_chat.settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,80 @@ function SettingsRouteView() {
}
/>

<SettingsRow
title="Window opacity"
description="Adjust the transparency of the entire application window."
resetAction={
settings.windowOpacity !== defaults.windowOpacity ? (
<SettingResetButton
label="window opacity"
onClick={() => {
updateSettings({ windowOpacity: defaults.windowOpacity });
if (isElectron && window.desktopBridge) {
void window.desktopBridge.setWindowOpacity(defaults.windowOpacity);
}
}}
/>
) : null
}
control={
<div className="flex items-center gap-2">
<input
type="range"
min={30}
max={100}
value={Math.round(settings.windowOpacity * 100)}
onChange={(e) => {
const value = Number(e.target.value) / 100;
updateSettings({ windowOpacity: value });
if (isElectron && window.desktopBridge) {
void window.desktopBridge.setWindowOpacity(value);
}
}}
className="h-1.5 w-24 cursor-pointer appearance-none rounded-full bg-muted accent-foreground sm:w-28"
aria-label="Window opacity"
/>
<span className="w-9 text-right text-xs tabular-nums text-muted-foreground">
{Math.round(settings.windowOpacity * 100)}%
</span>
</div>
}
/>

<SettingsRow
title="Sidebar opacity"
description="Adjust the transparency of the side panel and project list."
resetAction={
settings.sidebarOpacity !== defaults.sidebarOpacity ? (
<SettingResetButton
label="sidebar opacity"
onClick={() =>
updateSettings({ sidebarOpacity: defaults.sidebarOpacity })
}
/>
) : null
}
control={
<div className="flex items-center gap-2">
<input
type="range"
min={30}
max={100}
value={Math.round(settings.sidebarOpacity * 100)}
onChange={(e) => {
const value = Number(e.target.value) / 100;
updateSettings({ sidebarOpacity: value });
}}
className="h-1.5 w-24 cursor-pointer appearance-none rounded-full bg-muted accent-foreground sm:w-28"
aria-label="Sidebar opacity"
/>
<span className="w-9 text-right text-xs tabular-nums text-muted-foreground">
{Math.round(settings.sidebarOpacity * 100)}%
</span>
</div>
}
/>

<SettingsRow
title="Time format"
description="System default follows your browser or OS clock preference."
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/routes/_chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ function ChatRouteGlobalShortcuts() {

function ChatRouteLayout() {
const navigate = useNavigate();
const { settings } = useAppSettings();

useEffect(() => {
const onMenuAction = window.desktopBridge?.onMenuAction;
Expand All @@ -112,13 +113,21 @@ function ChatRouteLayout() {
};
}, [navigate]);

// Apply window opacity via the desktop bridge when the setting changes
useEffect(() => {
if (window.desktopBridge) {
void window.desktopBridge.setWindowOpacity(settings.windowOpacity);
}
}, [settings.windowOpacity]);

return (
<SidebarProvider defaultOpen>
<ChatRouteGlobalShortcuts />
<Sidebar
side="left"
collapsible="offcanvas"
className="border-r border-border bg-card text-foreground"
style={{ opacity: settings.sidebarOpacity }}
resizable={{
minWidth: THREAD_SIDEBAR_MIN_WIDTH,
shouldAcceptWidth: ({ nextWidth, wrapper }) =>
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export interface DesktopBridge {
pickFolder: () => Promise<string | null>;
confirm: (message: string) => Promise<boolean>;
setTheme: (theme: DesktopTheme) => Promise<void>;
setWindowOpacity: (opacity: number) => Promise<void>;
setSidebarOpacity: (opacity: number) => Promise<void>;
showContextMenu: <T extends string>(
items: readonly ContextMenuItem<T>[],
position?: { x: number; y: number },
Expand Down
Loading