Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
1,881 changes: 934 additions & 947 deletions apps/frontend/design.json

Large diffs are not rendered by default.

540 changes: 270 additions & 270 deletions apps/frontend/package.json

Large diffs are not rendered by default.

18 changes: 15 additions & 3 deletions apps/frontend/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,20 @@ function createWindow(): void {
const minWidth: number = Math.min(WINDOW_MIN_WIDTH, width);
const minHeight: number = Math.min(WINDOW_MIN_HEIGHT, height);

// Create the browser window
// Create the browser window with platform-specific frame configuration
// macOS: hiddenInset keeps native traffic lights with custom positioning
// Windows/Linux: frame:false removes native title bar; custom controls in TopNavBar
mainWindow = new BrowserWindow({
width,
height,
minWidth,
minHeight,
show: false,
autoHideMenuBar: true,
titleBarStyle: 'hiddenInset',
trafficLightPosition: { x: 15, y: 10 },
...(isMacOS()
? { titleBarStyle: 'hiddenInset' as const, trafficLightPosition: { x: 15, y: 10 } }
: { frame: false }
),
icon: getIconPath(),
webPreferences: {
preload: join(__dirname, '../preload/index.mjs'),
Expand All @@ -209,6 +213,14 @@ function createWindow(): void {
}
});

// Forward maximize state changes to renderer for custom window controls
mainWindow.on('maximize', () => {
mainWindow?.webContents.send(IPC_CHANNELS.WINDOW_MAXIMIZE_CHANGED, true);
});
mainWindow.on('unmaximize', () => {
mainWindow?.webContents.send(IPC_CHANNELS.WINDOW_MAXIMIZE_CHANGED, false);
});

// Show window when ready to avoid visual flash
mainWindow.on('ready-to-show', () => {
mainWindow?.show();
Expand Down
7 changes: 6 additions & 1 deletion apps/frontend/src/main/ipc-handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { registerClaudeCodeHandlers } from './claude-code-handlers';
import { registerMcpHandlers } from './mcp-handlers';
import { registerProfileHandlers } from './profile-handlers';
import { registerScreenshotHandlers } from './screenshot-handlers';
import { registerWindowHandlers } from './window-handlers';
import { registerTerminalWorktreeIpcHandlers } from './terminal';
import { notificationService } from '../notification-service';

Expand Down Expand Up @@ -122,6 +123,9 @@ export function setupIpcHandlers(
// Screenshot capture handlers
registerScreenshotHandlers();

// Window control handlers (minimize, maximize, close)
registerWindowHandlers(getMainWindow);

console.warn('[IPC] All handler modules registered successfully');
}

Expand Down Expand Up @@ -149,5 +153,6 @@ export {
registerClaudeCodeHandlers,
registerMcpHandlers,
registerProfileHandlers,
registerScreenshotHandlers
registerScreenshotHandlers,
registerWindowHandlers
};
45 changes: 45 additions & 0 deletions apps/frontend/src/main/ipc-handlers/window-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ipcMain, BrowserWindow } from 'electron';
import { IPC_CHANNELS } from '../../shared/constants/ipc';

export function registerWindowHandlers(getMainWindow: () => BrowserWindow | null): void {
ipcMain.handle(IPC_CHANNELS.WINDOW_MINIMIZE, () => {
getMainWindow()?.minimize();
});

ipcMain.handle(IPC_CHANNELS.WINDOW_MAXIMIZE, () => {
const win = getMainWindow();
if (!win) return false;
if (win.isMaximized()) win.unmaximize();
else win.maximize();
return win.isMaximized();
});

ipcMain.handle(IPC_CHANNELS.WINDOW_CLOSE, () => {
getMainWindow()?.close();
});

ipcMain.handle(IPC_CHANNELS.WINDOW_IS_MAXIMIZED, () => {
return getMainWindow()?.isMaximized() ?? false;
});

ipcMain.handle(IPC_CHANNELS.WINDOW_GET_BOUNDS, () => {
return getMainWindow()?.getBounds() ?? null;
});

// Uses ipcMain.on (fire-and-forget) instead of handle/invoke to avoid
// round-trip overhead during rapid resize/move events.
ipcMain.on(IPC_CHANNELS.WINDOW_SET_BOUNDS, (_event, bounds: { x: number; y: number; width: number; height: number }) => {
const win = getMainWindow();
if (win) {
const [minW, minH] = win.getMinimumSize();
win.setBounds({
x: bounds.x,
y: bounds.y,
width: Math.max(bounds.width, minW),
height: Math.max(bounds.height, minH),
});
}
});

console.warn('[IPC] Window control handlers registered');
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use console.debug instead of console.warn for informational startup log.

This is a normal startup event, not a warning. Per the ESLint config in this repo, console.warn should be reserved for actual warnings. Based on learnings, the ESLint configuration only allows console.warn and console.error — if console.debug is not permitted, consider removing this log or switching to electron-log.

Proposed fix
-  console.warn('[IPC] Window control handlers registered');
+  // If electron-log is available, use log.debug instead
+  console.debug('[IPC] Window control handlers registered');
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/ipc-handlers/window-handlers.ts` at line 63, Replace
the informational startup log that uses console.warn with a non-warning logger:
change the call that logs "[IPC] Window control handlers registered" (the
console.warn invocation in the window-handlers registration) to console.debug;
if console.debug is disallowed by the repo ESLint rules, instead remove the log
or route it through the project's approved logger (e.g., electron-log) so the
message is recorded as informational rather than a warning.

}
12 changes: 9 additions & 3 deletions apps/frontend/src/preload/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { McpAPI, createMcpAPI } from './modules/mcp-api';
import { ProfileAPI, createProfileAPI } from './profile-api';
import { ScreenshotAPI, createScreenshotAPI } from './screenshot-api';
import { QueueAPI, createQueueAPI } from './queue-api';
import { WindowAPI, createWindowAPI } from './window-api';

export interface ElectronAPI extends
ProjectAPI,
Expand All @@ -35,6 +36,8 @@ export interface ElectronAPI extends
github: GitHubAPI;
/** Queue routing API for rate limit recovery */
queue: QueueAPI;
/** Window control API (minimize, maximize, close) */
window: WindowAPI;
}

export const createElectronAPI = (): ElectronAPI => ({
Expand All @@ -51,7 +54,8 @@ export const createElectronAPI = (): ElectronAPI => ({
...createProfileAPI(),
...createScreenshotAPI(),
github: createGitHubAPI(),
queue: createQueueAPI() // Queue routing for rate limit recovery
queue: createQueueAPI(), // Queue routing for rate limit recovery
window: createWindowAPI() // Window controls (minimize, maximize, close)
});

// Export individual API creators for potential use in tests or specialized contexts
Expand All @@ -70,7 +74,8 @@ export {
createClaudeCodeAPI,
createMcpAPI,
createScreenshotAPI,
createQueueAPI
createQueueAPI,
createWindowAPI
};

export type {
Expand All @@ -90,5 +95,6 @@ export type {
ClaudeCodeAPI,
McpAPI,
ScreenshotAPI,
QueueAPI
QueueAPI,
WindowAPI
};
33 changes: 33 additions & 0 deletions apps/frontend/src/preload/api/window-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ipcRenderer } from 'electron';
import { IPC_CHANNELS } from '../../shared/constants/ipc';

export interface WindowBounds {
x: number;
y: number;
width: number;
height: number;
}

export interface WindowAPI {
minimize: () => Promise<void>;
maximize: () => Promise<boolean>;
close: () => Promise<void>;
isMaximized: () => Promise<boolean>;
getBounds: () => Promise<WindowBounds | null>;
setBounds: (bounds: WindowBounds) => void;
onMaximizeChanged: (callback: (isMaximized: boolean) => void) => () => void;
}

export const createWindowAPI = (): WindowAPI => ({
minimize: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_MINIMIZE),
maximize: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_MAXIMIZE),
close: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_CLOSE),
isMaximized: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_IS_MAXIMIZED),
getBounds: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_GET_BOUNDS),
setBounds: (bounds) => ipcRenderer.send(IPC_CHANNELS.WINDOW_SET_BOUNDS, bounds),
onMaximizeChanged: (callback) => {
const handler = (_event: Electron.IpcRendererEvent, isMaximized: boolean) => callback(isMaximized);
ipcRenderer.on(IPC_CHANNELS.WINDOW_MAXIMIZE_CHANGED, handler);
return () => ipcRenderer.removeListener(IPC_CHANNELS.WINDOW_MAXIMIZE_CHANGED, handler);
}
});
Loading