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
53 changes: 51 additions & 2 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as Path from "node:path";
import {
app,
BrowserWindow,
type BrowserWindowConstructorOptions,
clipboard,
dialog,
ipcMain,
Expand Down Expand Up @@ -118,6 +119,15 @@ const DESKTOP_UPDATE_CHANNEL = "latest";
const DESKTOP_UPDATE_ALLOW_PRERELEASE = false;
const DESKTOP_LOOPBACK_HOST = "127.0.0.1";
const DESKTOP_REQUIRED_PORT_PROBE_HOSTS = ["0.0.0.0", "::"] as const;
const TITLEBAR_HEIGHT = 40;
const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux
const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937";
const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc";

type WindowTitleBarOptions = Pick<
BrowserWindowConstructorOptions,
"titleBarOverlay" | "titleBarStyle" | "trafficLightPosition"
>;

type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"];
type LinuxDesktopNamedApp = Electron.App & {
Expand Down Expand Up @@ -1649,6 +1659,46 @@ function getInitialWindowBackgroundColor(): string {
return nativeTheme.shouldUseDarkColors ? "#0a0a0a" : "#ffffff";
}

function getWindowTitleBarOptions(): WindowTitleBarOptions {
if (process.platform === "darwin") {
return {
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 16, y: 18 },
};
}

return {
titleBarStyle: "hidden",
titleBarOverlay: {
color: TITLEBAR_COLOR,
height: TITLEBAR_HEIGHT,
symbolColor: nativeTheme.shouldUseDarkColors
? TITLEBAR_DARK_SYMBOL_COLOR
: TITLEBAR_LIGHT_SYMBOL_COLOR,
},
};
}

function syncWindowAppearance(window: BrowserWindow): void {
if (window.isDestroyed()) {
return;
}

window.setBackgroundColor(getInitialWindowBackgroundColor());
const { titleBarOverlay } = getWindowTitleBarOptions();
if (typeof titleBarOverlay === "object") {
window.setTitleBarOverlay(titleBarOverlay);
}
}
Comment on lines +1682 to +1692
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/main.ts:1682

syncWindowAppearance calls window.setTitleBarOverlay() whenever titleBarOverlay is an object, but this API is Windows-only and throws on Linux. Since getWindowTitleBarOptions() returns titleBarOverlay for both Windows and Linux in its else branch, Linux will crash when nativeTheme fires an updated event.

  window.setBackgroundColor(getInitialWindowBackgroundColor());
   const { titleBarOverlay } = getWindowTitleBarOptions();
   if (typeof titleBarOverlay === "object") {
+    if (process.platform === "win32") {
       window.setTitleBarOverlay(titleBarOverlay);
+    }
   }
 }
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/desktop/src/main.ts around lines 1682-1692:

`syncWindowAppearance` calls `window.setTitleBarOverlay()` whenever `titleBarOverlay` is an object, but this API is Windows-only and throws on Linux. Since `getWindowTitleBarOptions()` returns `titleBarOverlay` for both Windows and Linux in its else branch, Linux will crash when `nativeTheme` fires an `updated` event.

Copy link
Copy Markdown
Contributor Author

@tarik02 tarik02 Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.


function syncAllWindowAppearance(): void {
for (const window of BrowserWindow.getAllWindows()) {
syncWindowAppearance(window);
}
}

nativeTheme.on("updated", syncAllWindowAppearance);

function createWindow(): BrowserWindow {
const window = new BrowserWindow({
width: 1100,
Expand All @@ -1660,8 +1710,7 @@ function createWindow(): BrowserWindow {
backgroundColor: getInitialWindowBackgroundColor(),
...getIconOption(),
title: APP_DISPLAY_NAME,
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 16, y: 18 },
...getWindowTitleBarOptions(),
webPreferences: {
preload: Path.join(__dirname, "preload.js"),
contextIsolation: true,
Expand Down
18 changes: 16 additions & 2 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,15 @@
environmentId: EnvironmentId;
threadId: ThreadId;
onDiffPanelOpen?: () => void;
reserveTitleBarControlInset?: boolean;
routeKind: "server";
draftId?: never;
}
| {
environmentId: EnvironmentId;
threadId: ThreadId;
onDiffPanelOpen?: () => void;
reserveTitleBarControlInset?: boolean;
routeKind: "draft";
draftId: DraftId;
};
Expand Down Expand Up @@ -573,7 +575,13 @@
});

export default function ChatView(props: ChatViewProps) {
const { environmentId, threadId, routeKind, onDiffPanelOpen } = props;
const {
environmentId,
threadId,
routeKind,
onDiffPanelOpen,
reserveTitleBarControlInset = true,
} = props;
const draftId = routeKind === "draft" ? props.draftId : null;
const routeThreadRef = useMemo(
() => scopeThreadRef(environmentId, threadId),
Expand Down Expand Up @@ -1516,7 +1524,7 @@
);

const activeTerminalGroup =
terminalState.terminalGroups.find(

Check warning on line 1527 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
(group) => group.id === terminalState.activeTerminalGroupId,
) ??
terminalState.terminalGroups.find((group) =>
Expand All @@ -1524,7 +1532,7 @@
) ??
null;
const hasReachedSplitLimit =
(activeTerminalGroup?.terminalIds.length ?? 0) >= MAX_TERMINALS_PER_GROUP;

Check warning on line 1535 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
const setThreadError = useCallback(
(targetThreadId: ThreadId | null, error: string | null) => {
if (!targetThreadId) return;
Expand Down Expand Up @@ -2675,7 +2683,7 @@
},
}
: {}),
...(baseBranchForWorktree

Check warning on line 2686 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
? {
prepareWorktree: {
projectCwd: activeProject.cwd,
Expand All @@ -2702,7 +2710,7 @@
titleSeed: title,
runtimeMode,
interactionMode,
...(bootstrap ? { bootstrap } : {}),

Check warning on line 2713 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
createdAt: messageCreatedAt,
});
turnStartSucceeded = true;
Expand Down Expand Up @@ -2765,7 +2773,7 @@
existing.includes(requestId) ? existing : [...existing, requestId],
);
await api.orchestration
.dispatchCommand({

Check warning on line 2776 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
type: "thread.approval.respond",
commandId: newCommandId(),
threadId: activeThreadId,
Expand Down Expand Up @@ -2900,7 +2908,7 @@
if (activePendingResolvedAnswers) {
void onRespondToUserInput(activePendingUserInput.requestId, activePendingResolvedAnswers);
}
return;

Check warning on line 2911 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'composerRef.current'
}
setActivePendingUserInputQuestionIndex(activePendingProgress.questionIndex + 1);
}, [
Expand Down Expand Up @@ -3294,7 +3302,13 @@
<header
className={cn(
"border-b border-border px-3 sm:px-5",
isElectron ? "drag-region flex h-[52px] items-center" : "py-2 sm:py-3",
isElectron
? cn(
"drag-region flex h-[52px] items-center wco:h-[env(titlebar-area-height)]",
reserveTitleBarControlInset &&
"wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
)
: "py-2 sm:py-3",
)}
>
<ChatHeader
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/DiffPanelShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export type DiffPanelMode = "inline" | "sheet" | "sidebar";
function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) {
const shouldUseDragRegion = isElectron && mode !== "sheet";
return cn(
"flex items-center justify-between gap-2 px-4",
shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12",
"flex items-center justify-between gap-2 px-4 wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
shouldUseDragRegion
? "drag-region h-[52px] border-b border-border wco:h-[env(titlebar-area-height)]"
: "h-12 wco:max-h-[env(titlebar-area-height)]",
);
}

Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/components/NoActiveThreadState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ export function NoActiveThreadState() {
<header
className={cn(
"border-b border-border px-3 sm:px-5",
isElectron ? "drag-region flex h-[52px] items-center" : "py-2 sm:py-3",
isElectron
? "drag-region flex h-[52px] items-center wco:h-[env(titlebar-area-height)]"
: "py-2 sm:py-3",
)}
>
{isElectron ? (
<span className="text-xs text-muted-foreground/50">No active thread</span>
<span className="text-xs text-muted-foreground/50 wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]">
No active thread
</span>
) : (
<div className="flex items-center gap-2">
<SidebarTrigger className="size-7 shrink-0 md:hidden" />
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1974,7 +1974,7 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({
);

return isElectron ? (
<SidebarHeader className="drag-region h-[52px] flex-row items-center gap-2 px-4 py-0 pl-[90px]">
<SidebarHeader className="drag-region h-[52px] flex-row items-center gap-2 px-4 py-0 pl-[90px] wco:h-[env(titlebar-area-height)] wco:pl-[calc(env(titlebar-area-x)+1em)]">
{wordmark}
</SidebarHeader>
) : (
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@import "tailwindcss";

@custom-variant dark (&:is(.dark, .dark *));
/* Window Controls Overlay: active when Electron exposes native titlebar control geometry. */
@custom-variant wco (&:is(.wco, .wco *));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

descriptive comment would be nice, not an acronym that immediately pops into mind


@theme inline {
--animate-skeleton: skeleton 2s -1s infinite linear;
Expand Down
40 changes: 40 additions & 0 deletions apps/web/src/lib/windowControlsOverlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const WCO_CLASS_NAME = "wco";

interface WindowControlsOverlayLike {
readonly visible: boolean;
addEventListener(type: "geometrychange", listener: EventListener): void;
removeEventListener(type: "geometrychange", listener: EventListener): void;
}

interface NavigatorWithWindowControlsOverlay extends Navigator {
readonly windowControlsOverlay?: WindowControlsOverlayLike;
}

function getWindowControlsOverlay(): WindowControlsOverlayLike | null {
if (typeof navigator === "undefined") {
return null;
}

return (navigator as NavigatorWithWindowControlsOverlay).windowControlsOverlay ?? null;
}

export function syncDocumentWindowControlsOverlayClass(): () => void {
if (typeof document === "undefined") {
return () => {};
}

const overlay = getWindowControlsOverlay();
const update = () => {
document.documentElement.classList.toggle(WCO_CLASS_NAME, overlay !== null && overlay.visible);
};

update();
if (!overlay) {
return () => {};
}

overlay.addEventListener("geometrychange", update);
return () => {
overlay.removeEventListener("geometrychange", update);
};
}
5 changes: 5 additions & 0 deletions apps/web/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ import "./index.css";
import { isElectron } from "./env";
import { getRouter } from "./router";
import { APP_DISPLAY_NAME } from "./branding";
import { syncDocumentWindowControlsOverlayClass } from "./lib/windowControlsOverlay";

// Electron loads the app from a file-backed shell, so hash history avoids path resolution issues.
const history = isElectron ? createHashHistory() : createBrowserHistory();

const router = getRouter(history);

if (isElectron) {
syncDocumentWindowControlsOverlayClass();
}

document.title = APP_DISPLAY_NAME;

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/routes/_chat.$environmentId.$threadId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ function ChatThreadRouteView() {
environmentId={threadRef.environmentId}
threadId={threadRef.threadId}
onDiffPanelOpen={markDiffOpened}
reserveTitleBarControlInset={!diffOpen}
routeKind="server"
/>
</SidebarInset>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/routes/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function SettingsContentLayout() {
)}

{isElectron && (
<div className="drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5">
<div className="drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5 wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]">
<span className="text-xs font-medium tracking-wide text-muted-foreground/70">
Settings
</span>
Expand Down
Loading