Skip to content
Open
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
4 changes: 1 addition & 3 deletions packages/desktop-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,8 @@ Available in all renderer code via the preload context bridge:
```ts
// Window chrome
window.electronAPI.windowControls.minimize()
window.electronAPI.windowControls.maximize()
window.electronAPI.windowControls.toggleWindowMode()
window.electronAPI.windowControls.close()
window.electronAPI.windowControls.isMaximized() // Promise<boolean>
window.electronAPI.windowControls.onMaximizedChange(cb) // returns unsubscribe fn

// Inter-app messaging
window.electronAPI.interApp.send(targetAppId, event, data)
Expand Down
8 changes: 1 addition & 7 deletions packages/desktop-app/shared/ipc-channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,10 @@
export const IPC = {
/** Window control channels (renderer → main) */
WINDOW_MINIMIZE: "window:minimize",
WINDOW_MAXIMIZE: "window:maximize",
WINDOW_TOGGLE_WINDOW_MODE: "window:toggle-window-mode",
WINDOW_CLOSE: "window:close",
WINDOW_NATIVE_BUTTONS_VISIBILITY: "window:native-buttons-visibility",

/** Window state query (renderer ↔ main) */
WINDOW_IS_MAXIMIZED: "window:is-maximized",

/** Window state broadcast (main → renderer) */
WINDOW_MAXIMIZED_CHANGED: "window:maximized-changed",

/** Inter-app message relay (renderer → main → renderer) */
INTER_APP_SEND: "inter-app:send",
INTER_APP_MESSAGE: "inter-app:message",
Expand Down Expand Up @@ -523,7 +517,7 @@
export interface CodeAgentModelSelection {
engine?: string;
model?: string;
effort?: CodeAgentReasoningEffort | string;

Check warning on line 520 in packages/desktop-app/shared/ipc-channels.ts

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

"\"auto\"" | "\"high\"" | "\"low\"" | "\"max\"" | "\"medium\"" | "\"minimal\"" | "\"none\"" | "\"xhigh\"" is overridden by string in this union type.
}

export interface CodeAgentModelOption {
Expand Down Expand Up @@ -612,7 +606,7 @@
export interface CodeAgentQueueMetadata {
queued: boolean;
queuedAt?: string;
queuedBy?: "desktop" | "cli" | "host" | string;

Check warning on line 609 in packages/desktop-app/shared/ipc-channels.ts

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

host is overridden by string in this union type.

Check warning on line 609 in packages/desktop-app/shared/ipc-channels.ts

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

cli is overridden by string in this union type.

Check warning on line 609 in packages/desktop-app/shared/ipc-channels.ts

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

desktop is overridden by string in this union type.
queueId?: string;
queuePosition?: number;
attempt?: number;
Expand All @@ -625,7 +619,7 @@
permissionMode?: CodeAgentPermissionMode;
engine?: string;
model?: string;
effort?: CodeAgentReasoningEffort | string;

Check warning on line 622 in packages/desktop-app/shared/ipc-channels.ts

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

"\"auto\"" | "\"high\"" | "\"low\"" | "\"max\"" | "\"medium\"" | "\"minimal\"" | "\"none\"" | "\"xhigh\"" is overridden by string in this union type.
attachments?: CodeAgentPromptAttachment[];
}

Expand Down Expand Up @@ -719,7 +713,7 @@
permissionMode?: CodeAgentPermissionMode;
engine?: string;
model?: string;
effort?: CodeAgentReasoningEffort | string;

Check warning on line 716 in packages/desktop-app/shared/ipc-channels.ts

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

"\"auto\"" | "\"high\"" | "\"low\"" | "\"max\"" | "\"medium\"" | "\"minimal\"" | "\"none\"" | "\"xhigh\"" is overridden by string in this union type.
attachments?: CodeAgentPromptAttachment[];
metadata?: Record<string, unknown>;
}
Expand Down Expand Up @@ -802,7 +796,7 @@
permissionMode?: CodeAgentPermissionMode;
engine?: string;
model?: string;
effort?: CodeAgentReasoningEffort | string;

Check warning on line 799 in packages/desktop-app/shared/ipc-channels.ts

View workflow job for this annotation

GitHub Actions / Lint & format

typescript(no-redundant-type-constituents)

"\"auto\"" | "\"high\"" | "\"low\"" | "\"max\"" | "\"medium\"" | "\"minimal\"" | "\"none\"" | "\"xhigh\"" is overridden by string in this union type.
attachments?: CodeAgentPromptAttachment[];
metadata?: Record<string, unknown>;
}
Expand Down
9 changes: 0 additions & 9 deletions packages/desktop-app/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8973,7 +8973,7 @@
function contentFilesSourcePrefixBase(name: string): string {
const prefix = name
.replace(/[\\/]/g, "-")
.replace(/\0/g, "")

Check warning on line 8976 in packages/desktop-app/src/main/index.ts

View workflow job for this annotation

GitHub Actions / Lint & format

eslint(no-control-regex)

Unexpected control character
.replace(/\s+/g, " ")
.trim();
if (!prefix || prefix === "." || prefix === "..") return "Local folder";
Expand Down Expand Up @@ -9312,7 +9312,7 @@
: [],
developmentOrigins: [
`http://localhost:${FRAME_PORT}`,
...(contentDevPort != null ? [`http://localhost:${contentDevPort}`] : []),

Check warning on line 9315 in packages/desktop-app/src/main/index.ts

View workflow job for this annotation

GitHub Actions / Lint & format

eslint(eqeqeq)

Expected !== and instead saw !=
],
development: IS_DEV,
});
Expand Down Expand Up @@ -14040,15 +14040,6 @@
}
});

// Broadcast window maximized state changes to the renderer
const broadcastMaximized = (isMaximized: boolean) =>
win.webContents.send(IPC.WINDOW_MAXIMIZED_CHANGED, isMaximized);

win.on("maximize", () => broadcastMaximized(true));
win.on("unmaximize", () => broadcastMaximized(false));
win.on("enter-full-screen", () => broadcastMaximized(true));
win.on("leave-full-screen", () => broadcastMaximized(false));

// macOS: restore/focus the window when dock icon is clicked
app.on("activate", () => {
if (isQuickPromptActive()) return;
Expand Down
96 changes: 96 additions & 0 deletions packages/desktop-app/src/main/ipc/window.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const electronState = vi.hoisted(() => {
const handlers = new Map<string, (event: { sender: unknown }) => void>();
return {
browserWindow: {
fromWebContents: vi.fn(),
},
ipcMain: {
on: vi.fn(
(channel: string, handler: (event: { sender: unknown }) => void) => {
handlers.set(channel, handler);
},
),
},
handlers,
};
});

vi.mock("electron", () => ({
BrowserWindow: electronState.browserWindow,
ipcMain: electronState.ipcMain,
}));

import { IPC } from "@shared/ipc-channels";

import { registerWindowIpc, toggleWindowMode } from "./window.js";

describe("desktop window controls", () => {
beforeEach(() => {
vi.clearAllMocks();
electronState.handlers.clear();
});

function createWindow({
fullscreen = false,
maximized = false,
}: {
fullscreen?: boolean;
maximized?: boolean;
} = {}) {
return {
isFullScreen: vi.fn(() => fullscreen),
setFullScreen: vi.fn(),
isMaximized: vi.fn(() => maximized),
maximize: vi.fn(),
restore: vi.fn(),
};
}

it("toggles fullscreen mode on macOS", () => {
const window = createWindow();

toggleWindowMode(window, "darwin");

expect(window.setFullScreen).toHaveBeenCalledWith(true);
expect(window.maximize).not.toHaveBeenCalled();
});

it("exits fullscreen mode on macOS", () => {
const window = createWindow({ fullscreen: true });

toggleWindowMode(window, "darwin");

expect(window.setFullScreen).toHaveBeenCalledWith(false);
});

it("maximizes the window on Windows and Linux", () => {
for (const platform of ["win32", "linux"] as const) {
const window = createWindow();

toggleWindowMode(window, platform);

expect(window.maximize).toHaveBeenCalledOnce();
expect(window.setFullScreen).not.toHaveBeenCalled();
}
});

it("restores an already-maximized window on Windows and Linux", () => {
for (const platform of ["win32", "linux"] as const) {
const window = createWindow({ maximized: true });

toggleWindowMode(window, platform);

expect(window.restore).toHaveBeenCalledOnce();
}
});

it("registers the platform-aware window mode command", () => {
registerWindowIpc();

expect(electronState.handlers.has(IPC.WINDOW_TOGGLE_WINDOW_MODE)).toBe(
true,
);
});
});
37 changes: 20 additions & 17 deletions packages/desktop-app/src/main/ipc/window.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import { IPC } from "@shared/ipc-channels";
import {
import { BrowserWindow, ipcMain, type IpcMainEvent } from "electron";

type WindowModeTarget = Pick<
BrowserWindow,
ipcMain,
type IpcMainEvent,
type IpcMainInvokeEvent,
} from "electron";
"isFullScreen" | "setFullScreen" | "isMaximized" | "maximize" | "restore"
>;

export function toggleWindowMode(
window: WindowModeTarget,
platform = process.platform,
): void {
if (platform === "darwin") {
window.setFullScreen(!window.isFullScreen());
Comment thread
shomix marked this conversation as resolved.
return;
}

window.isMaximized() ? window.restore() : window.maximize();

Check warning on line 18 in packages/desktop-app/src/main/ipc/window.ts

View workflow job for this annotation

GitHub Actions / Lint & format

eslint(no-unused-expressions)

Expected expression to be used
}

/** Registers the basic frameless-window control IPC handlers (minimize/maximize/close/is-maximized). */
/** Registers the basic frameless-window control IPC handlers. */
export function registerWindowIpc(): void {
ipcMain.on(IPC.WINDOW_MINIMIZE, (event: IpcMainEvent) => {
BrowserWindow.fromWebContents(event.sender)?.minimize();
});

ipcMain.on(IPC.WINDOW_MAXIMIZE, (event: IpcMainEvent) => {
ipcMain.on(IPC.WINDOW_TOGGLE_WINDOW_MODE, (event: IpcMainEvent) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) return;
win.isMaximized() ? win.restore() : win.maximize();
toggleWindowMode(win);
});

ipcMain.on(IPC.WINDOW_CLOSE, (event: IpcMainEvent) => {
Expand All @@ -33,13 +45,4 @@
);
},
);

ipcMain.handle(
IPC.WINDOW_IS_MAXIMIZED,
(event: IpcMainInvokeEvent): boolean => {
return (
BrowserWindow.fromWebContents(event.sender)?.isMaximized() ?? false
);
},
);
}
13 changes: 1 addition & 12 deletions packages/desktop-app/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,21 +143,10 @@ const electronAPI = {
/** Window chrome controls */
windowControls: {
minimize: () => ipcRenderer.send(IPC.WINDOW_MINIMIZE),
maximize: () => ipcRenderer.send(IPC.WINDOW_MAXIMIZE),
toggleWindowMode: () => ipcRenderer.send(IPC.WINDOW_TOGGLE_WINDOW_MODE),
close: () => ipcRenderer.send(IPC.WINDOW_CLOSE),
setNativeTrafficLightsVisible: (visible: boolean): void =>
ipcRenderer.send(IPC.WINDOW_NATIVE_BUTTONS_VISIBILITY, visible),
isMaximized: (): Promise<boolean> =>
ipcRenderer.invoke(IPC.WINDOW_IS_MAXIMIZED),

/** Subscribe to maximize/restore state changes. Returns an unsubscribe fn. */
onMaximizedChange: (cb: (isMaximized: boolean) => void): (() => void) => {
const handler = (_: Electron.IpcRendererEvent, value: boolean) =>
cb(value);
ipcRenderer.on(IPC.WINDOW_MAXIMIZED_CHANGED, handler);
return () =>
ipcRenderer.removeListener(IPC.WINDOW_MAXIMIZED_CHANGED, handler);
},
},

/** Shortcuts forwarded from the main process */
Expand Down
13 changes: 8 additions & 5 deletions packages/desktop-app/src/renderer/components/WindowControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ export default function WindowControls({
}: {
className?: string;
}) {
const windowModeLabel =
window.electronAPI?.platform === "darwin" ? "Full screen" : "Maximize";

return (
<div className={className}>
<button
Expand All @@ -20,8 +23,8 @@ export default function WindowControls({
<button
className="win-btn win-btn--maximize"
tabIndex={-1}
onClick={() => window.electronAPI?.windowControls.maximize()}
title="Maximize"
onClick={() => window.electronAPI?.windowControls.toggleWindowMode()}
title={windowModeLabel}
/>
</div>
);
Expand Down Expand Up @@ -59,9 +62,9 @@ export function CollapsedMacWindowControls({
<button
type="button"
className="win-btn win-btn--maximize"
onClick={() => window.electronAPI?.windowControls.maximize()}
aria-label="Zoom window"
title="Zoom"
onClick={() => window.electronAPI?.windowControls.toggleWindowMode()}
aria-label="Toggle full screen"
title="Full screen"
/>
</div>
);
Expand Down
4 changes: 1 addition & 3 deletions packages/desktop-app/src/renderer/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -838,11 +838,9 @@ interface ElectronAPI {

windowControls: {
minimize(): void;
maximize(): void;
toggleWindowMode(): void;
close(): void;
setNativeTrafficLightsVisible(visible: boolean): void;
isMaximized(): Promise<boolean>;
onMaximizedChange(cb: (isMaximized: boolean) => void): () => void;
};

shortcuts: {
Expand Down
Loading