diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..dfd9771 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,76 @@ +name: Build & Release + +on: + push: + branches: [ main, development ] + tags: [ "v*" ] + pull_request: + branches: [ main, development ] + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + name: Build (${{ matrix.platform }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-14 + platform: mac + builder_args: "--mac --arm64" + - os: windows-latest + platform: windows + builder_args: "--win --x64" + - os: ubuntu-latest + platform: linux + builder_args: "--linux deb AppImage" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build ${{ matrix.platform }} artifacts + run: npx --yes electron-builder@24.13.3 ${{ matrix.builder_args }} --publish never + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: grok-${{ matrix.platform }} + if-no-files-found: warn + retention-days: 14 + path: | + build/*.dmg + build/*.zip + build/*.exe + build/*.msi + build/*.deb + build/*.AppImage + build/*.rpm + + - name: Publish to GitHub Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: | + build/*.dmg + build/*.zip + build/*.exe + build/*.msi + build/*.deb + build/*.AppImage + build/*.rpm diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a3d2a..3499a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,37 @@ All notable changes to this project will be documented in this file. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. +## [1.3.1] - 2026-06-06 + +### Added + +- **Drag ghost improvements** — padded transparent window so tab shadows and rounded corners are no longer clipped +- **Holographic merge hint** — ghost becomes translucent when dragged over another window's tab strip +- **In-strip reorder mode** — ghost hides while over the source window's tab bar so live tab reordering is visible + +### Changed + +- **App renamed to Grok** — builds now produce `Grok.app` (macOS) and `Grok.exe` (Windows) instead of "Grok Desktop Universal" +- Simplified release artifact names (`Grok-v1.3.1-arm64.dmg`, `Grok_Installer-v1.3.1.exe`, etc.) +- Updated window title, About page, Linux metainfo, and Windows build scripts for the new name +- Bump version to 1.3.1 + +## [1.3.0] - 2026-06-06 + +This release marks the **Grok Desktop Universal** fork of AnRkey/Grok-Desktop. + +### Added + +- **macOS support** (Apple Silicon / arm64) with a native `.icns` icon and DMG/ZIP packaging +- **Browser-style tearable tabs** - drag a tab out of the window to create a separate window +- **Multi-window support** - drag tabs back onto another window's tab strip to merge them +- **Multi-select tabs** - `Cmd/Ctrl`-click and `Shift`-click to drag multiple tabs at once +- **Cross-platform CI** - GitHub Actions workflow that builds and publishes macOS, Windows, and Linux release artifacts on `v*` tags + +### Changed + +- Rebranded to `grok-desktop-universal` (product name "Grok Desktop Universal") +- Bump version to 1.3.0 ## [1.2.4] - 2025-12-14 diff --git a/README.md b/README.md index 76bb06d..9cb712a 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,57 @@ -# Grok-Desktop v1.2.4 +# Grok v1.3.1 + +> A cross-platform fork of [AnRkey/Grok-Desktop](https://github.com/AnRkey/Grok-Desktop) with native **macOS** builds and **browser-style tearable tabs**. ## Description -Grok-Desktop is an Electron-based desktop application for Windows 10/11 and Linux that wraps `grok.com`, providing desktop-application-like access to Grok with real-time API usage monitoring, multi-tab support, and seamless authentication for xAI, Google, and Apple accounts. + +Grok is an Electron-based desktop application for **macOS, Windows 10/11, and Linux** that wraps `grok.com`, providing desktop-application-like access to Grok with real-time API usage monitoring, multi-tab support, and seamless authentication for xAI, Google, and Apple accounts. ## Screenshot + ![Screenshot](screenshot.png) +## What's new in this fork + +- **Native macOS support** (Apple Silicon / arm64) with proper `.icns` icon and DMG/ZIP packaging +- **Browser-style tearable tabs** - drag a tab out of the window to pop it into its own window, just like Chrome/Firefox +- **Multi-window support** - drag tabs back onto another window's tab strip to merge them +- **Multi-select tabs** - `Cmd/Ctrl`-click and `Shift`-click to move several tabs at once +- **Automated cross-platform release builds** via GitHub Actions (macOS, Windows, and Linux artifacts) + ## Features + - **Desktop application wrapper** for grok.com - **Tabs functionality** for multiple Grok conversations +- **Tearable / dockable tabs** - pull a tab into its own window or merge it back - **Real-time Usage Monitoring** - Track your Grok usage limits: - **Low Effort**: Basic query limits and remaining tokens - **High Effort**: Advanced feature usage tracking - **Grok 4 Heavy**: Specialized model usage limits - **Refill Timer**: Shows when limits reset - **Keyboard shortcuts**: - - `Ctrl+T`: Open a new tab - - `Ctrl+Tab` / `Ctrl+Shift+Tab`: Cycle through open tabs (next/previous) - - `Ctrl+R`: Reload the active tab - - `Ctrl+I`: Show information/about dialog + - `Ctrl/Cmd+T`: Open a new tab + - `Ctrl/Cmd+Tab` / `Ctrl/Cmd+Shift+Tab`: Cycle through open tabs (next/previous) + - `Ctrl/Cmd+R`: Reload the active tab + - `Ctrl/Cmd+I`: Show information/about dialog - **Authentication support** for xAI, Google, and Apple accounts - **Clean interface** with no menu bar for distraction-free usage -- **Always-on-top function** with cross-platform support (Windows & Linux) +- **Always-on-top function** with cross-platform support (macOS, Windows & Linux) - **Dark/Light mode support** with system theme detection - **Grok speech mode** support - **Enhanced security** with domain validation and OAuth protection ## Download -[Download Grok-Desktop_Installer-v1.2.4.exe](https://github.com/AnRkey/Grok-Desktop/releases/download/v1.2.4/Grok-Desktop_Installer-v1.2.4.exe) + +Pre-built installers for macOS, Windows, and Linux are published automatically on the [Releases page](https://github.com/perlytiara/Grok-Desktop-Universal/releases) of this fork. + +- **macOS (Apple Silicon)**: `Grok-v1.3.1-arm64.dmg` (installs `Grok.app`) +- **Windows**: `Grok_Installer-v1.3.1.exe` (installs `Grok.exe`) +- **Linux**: `Grok-v1.3.1.deb` or the `.AppImage` ## System Requirements ### For Using the Application -- **Operating System**: Windows 10/11 or Linux (Rocky Linux 9/10, RHEL 9, Ubuntu, Fedora, etc.) +- **Operating System**: macOS 12+ (Apple Silicon), Windows 10/11, or Linux (Rocky Linux 9/10, RHEL 9, Ubuntu, Fedora, etc.) - **Internet connection** for accessing grok.com - **Grok account** (sign up in-app or use Google/Apple/xAI authentication) - **Linux AOT (Always-on-Top) requirement**: Install `wmctrl` for Always-on-Top functionality: @@ -40,10 +59,27 @@ Grok-Desktop is an Electron-based desktop application for Windows 10/11 and Linu - Ubuntu/Debian: `sudo apt install wmctrl` ### For Building from Source -- **Operating System**: Windows 10/11 or Linux +- **Operating System**: macOS, Windows 10/11, or Linux - **Node.js**: LTS version (20.x recommended) - **Internet connection** for downloading dependencies +### Building locally + +```bash +npm install + +# macOS (Apple Silicon) +npm run build-mac-arm64 + +# Windows +npm run build + +# Linux (deb + rpm) +npm run build-rpm-deb +``` + +Built installers are written to the `build/` directory. Cross-platform release artifacts are also produced automatically by the GitHub Actions workflow in `.github/workflows/build.yml` whenever a `v*` tag is pushed. + ## Project Structure ``` Grok-Desktop/ @@ -118,7 +154,7 @@ npm run build-all ### Windows 1. Download and run `Grok-Desktop_Installer-v1.2.4.exe` from the releases page 2. Follow the installation wizard -3. Launch "Grok Desktop" from the Start Menu +3. Launch **Grok** from the Start Menu ### Linux diff --git a/about.html b/about.html index 7a7c6c9..a4cc844 100644 --- a/about.html +++ b/about.html @@ -94,7 +94,7 @@ } } window.addEventListener('DOMContentLoaded', function() { - const name = getParam('name', 'Grok Desktop'); + const name = getParam('name', 'Grok'); const version = getParam('version', '0.0.0'); const repo = getParam('repo', 'https://github.com/AnRkey/Grok-Desktop'); const developer = getParam('developer', ''); @@ -146,7 +146,7 @@
-

Grok Desktop

+

Grok

Version: 0.0.0

Developer: AnRkey

GitHub Repository

diff --git a/build-resources/com.grok.desktop.metainfo.xml b/build-resources/com.grok.desktop.metainfo.xml index 5c076ee..ceb5245 100644 --- a/build-resources/com.grok.desktop.metainfo.xml +++ b/build-resources/com.grok.desktop.metainfo.xml @@ -3,7 +3,7 @@ com.grok.desktop MIT GPL-2.0 - Grok Desktop + Grok Desktop application for Grok.com with tab support

@@ -26,7 +26,7 @@ AnRkey - Grok Desktop main interface + Grok main interface https://raw.githubusercontent.com/AnRkey/Grok-Desktop/main/screenshot.png diff --git a/build-resources/grok.icns b/build-resources/grok.icns new file mode 100644 index 0000000..b3d9683 Binary files /dev/null and b/build-resources/grok.icns differ diff --git a/build.bat b/build.bat index 258fe4a..3aa225e 100644 --- a/build.bat +++ b/build.bat @@ -7,7 +7,7 @@ set BUILD_TARGET=%1 if "%BUILD_TARGET%"=="" set BUILD_TARGET=win echo =================================================== -echo Building Grok Desktop - Target: %BUILD_TARGET% +echo Building Grok - Target: %BUILD_TARGET% echo =================================================== :: Validate build target @@ -32,9 +32,9 @@ echo. :: Clean previous build files echo Cleaning previous build files... rem Try to stop any running instances that may lock files -echo Stopping running Grok Desktop instances ^(if any^)... -taskkill /IM "Grok Desktop.exe" /F >nul 2>&1 -taskkill /IM "Grok Desktop.exe" /T /F >nul 2>&1 +echo Stopping running Grok instances ^(if any^)... +taskkill /IM "Grok.exe" /F >nul 2>&1 +taskkill /IM "Grok.exe" /T /F >nul 2>&1 taskkill /IM "electron.exe" /F >nul 2>&1 taskkill /IM "electron.exe" /T /F >nul 2>&1 diff --git a/index.html b/index.html index 8d12e86..43ce623 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Grok Desktop + Grok

${safeTitle}${badge}
`; +} + +function positionGhostAtCursor() { + if (!dragGhostWindow || dragGhostWindow.isDestroyed()) return; + try { + const p = screen.getCursorScreenPoint(); + // Anchor the chip just below-right of the cursor. Subtract the transparent + // padding so the visible chip (not the window edge) tracks the cursor. + dragGhostWindow.setPosition( + Math.round(p.x - 12 - GHOST_PAD), + Math.round(p.y + 14 - GHOST_PAD) + ); + } catch (_) {} +} + +// Switch the ghost between: 'normal' (floating chip), 'ghost' (holographic / +// translucent, shown while hovering another window to signal a merge), and +// 'hidden' (over the source window's own strip, so the live reorder shows). +function setGhostState(state) { + if (!dragGhostWindow || dragGhostWindow.isDestroyed()) return; + if (state === dragGhostState) return; + dragGhostState = state; + try { + if (state === 'hidden') { + dragGhostWindow.hide(); + return; + } + dragGhostWindow.setOpacity(state === 'ghost' ? 0.4 : 1); + if (!dragGhostWindow.isVisible()) dragGhostWindow.showInactive(); + } catch (_) {} +} + +function destroyDragGhost() { + if (dragGhostTimer) { clearInterval(dragGhostTimer); dragGhostTimer = null; } + if (dragGhostWatchdog) { clearTimeout(dragGhostWatchdog); dragGhostWatchdog = null; } + if (dragGhostWindow && !dragGhostWindow.isDestroyed()) { + try { dragGhostWindow.destroy(); } catch (_) {} + } + dragGhostWindow = null; + dragGhostState = 'normal'; +} + +function createDragGhost(info) { + destroyDragGhost(); + dragGhostState = 'normal'; + + dragGhostWindow = new BrowserWindow({ + width: GHOST_WIDTH, + height: GHOST_HEIGHT, + frame: false, + transparent: true, + resizable: false, + movable: false, + minimizable: false, + maximizable: false, + fullscreenable: false, + skipTaskbar: true, + focusable: false, + hasShadow: false, + acceptFirstMouse: false, + alwaysOnTop: true, + show: false, + webPreferences: { nodeIntegration: false, contextIsolation: true } + }); + + try { dragGhostWindow.setIgnoreMouseEvents(true); } catch (_) {} + try { dragGhostWindow.setAlwaysOnTop(true, 'screen-saver'); } catch (_) {} + if (typeof dragGhostWindow.setVisibleOnAllWorkspaces === 'function') { + try { dragGhostWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); } catch (_) {} + } + + const html = buildGhostHtml(info && info.title, (info && info.count) || 1); + dragGhostWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html)); + + positionGhostAtCursor(); + try { dragGhostWindow.showInactive(); } catch (_) {} + + dragGhostTimer = setInterval(positionGhostAtCursor, 16); + // Safety net: never let the ghost outlive a drag even if a mouseup is missed + dragGhostWatchdog = setTimeout(destroyDragGhost, 30000); +} // Allow autoplay without user gesture (for seamless audio playback) try { app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required'); } catch (_) {} @@ -278,7 +395,7 @@ function toggleAlwaysOnTopLinux(mainWindow) { return new Promise((resolve) => { // Get the window title to target it specifically - const windowTitle = mainWindow.getTitle() || 'Grok Desktop'; + const windowTitle = mainWindow.getTitle() || 'Grok'; // First focus the window, then toggle always-on-top const commands = [ @@ -298,32 +415,31 @@ function toggleAlwaysOnTopLinux(mainWindow) { }); } -function createWindow() { - // Create the browser window - mainWindow = new BrowserWindow({ - width: 1200, - height: 800, - minWidth: 800, - minHeight: 600, - webPreferences: { - nodeIntegration: true, // Enable Node.js integration - contextIsolation: false, // Disable context isolation for this use case - webviewTag: true, // Enable webview tag for tabs - spellcheck: true - }, - icon: path.join(__dirname, 'grok.png') +function broadcastThemeUpdate() { + const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; + BrowserWindow.getAllWindows().forEach((win) => { + if (!win.isDestroyed()) { + win.webContents.send('system-theme-updated', theme); + } }); +} - // Disable the menu bar - Menu.setApplicationMenu(null); - - // Ensure shortcuts work when focus is on the main window UI - try { attachShortcutHandlers(mainWindow.webContents); } catch (_) {} - - // Load the index.html file - mainWindow.loadFile(path.join(__dirname, '../index.html')); +function applyColorSchemeToAll() { + const scheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; + try { + webContents.getAllWebContents().forEach((wc) => { + if (typeof wc.setColorScheme === 'function') { + if (forcedLightWebContentsIds.has(wc.id)) { + wc.setColorScheme('light'); + } else { + wc.setColorScheme(scheme); + } + } + }); + } catch (_) {} +} - // Configure spellchecker languages for default session and webview partition +function configureSpellcheck() { try { const locale = (typeof app.getLocale === 'function' && app.getLocale()) || 'en-US'; const languages = Array.isArray(locale) ? locale : [locale]; @@ -348,60 +464,28 @@ function createWindow() { } } } catch (_) {} +} - // Send initial theme and listen for OS theme changes - const sendTheme = () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('system-theme-updated', nativeTheme.shouldUseDarkColors ? 'dark' : 'light'); - } - }; - sendTheme(); - // Apply color scheme to all web contents (main and webviews) - const applyColorSchemeToAll = () => { - const scheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; - try { - webContents.getAllWebContents().forEach((wc) => { - if (typeof wc.setColorScheme === 'function') { - if (forcedLightWebContentsIds.has(wc.id)) { - wc.setColorScheme('light'); - } else { - wc.setColorScheme(scheme); - } - } - }); - } catch (_) {} - }; - applyColorSchemeToAll(); - - nativeTheme.on('updated', () => { - sendTheme(); - applyColorSchemeToAll(); - }); - - // Open DevTools in development mode - // mainWindow.webContents.openDevTools(); - - // Handle window closed event - mainWindow.on('closed', () => { - mainWindow = null; - }); +function initializeAppOnce() { + if (appInitialized) return; + appInitialized = true; - // Set up URL handling + Menu.setApplicationMenu(null); + configureSpellcheck(); setupUrlHandling(); - - // Set up IPC handlers setupIpcHandlers(); - - // Set up WebRTC/media permissions (allow across all domains) setupPermissions(); - - // Enable right-click context menus setupContextMenus(); - - // Set up keyboard shortcuts (Ctrl+T, Ctrl+Tab, Ctrl+R) setupKeyboardShortcuts(); - // Ensure newly created webContents/webviews get correct color scheme + broadcastThemeUpdate(); + applyColorSchemeToAll(); + + nativeTheme.on('updated', () => { + broadcastThemeUpdate(); + applyColorSchemeToAll(); + }); + app.on('web-contents-created', (_event, contents) => { const scheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; if (typeof contents.setColorScheme === 'function') { @@ -423,13 +507,76 @@ function createWindow() { }); } +// Height (in px) of the in-app tab strip; used to detect drops onto a window's tab bar +const TAB_BAR_HEIGHT = 50; + +function createWindow(options = {}) { + // Accept either a single initialTab or an array of initialTabs + const initialTabs = Array.isArray(options.initialTabs) + ? options.initialTabs + : (options.initialTab ? [options.initialTab] : []); + const position = options.position || null; + + const windowOptions = { + width: 1200, + height: 800, + minWidth: 800, + minHeight: 600, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + webviewTag: true, + spellcheck: true + }, + icon: path.join(__dirname, 'grok.png') + }; + + if (position && Number.isFinite(position.x) && Number.isFinite(position.y)) { + windowOptions.x = Math.round(position.x); + windowOptions.y = Math.round(position.y); + } + + const win = new BrowserWindow(windowOptions); + + if (!mainWindow) { + mainWindow = win; + } + + try { attachShortcutHandlers(win.webContents); } catch (_) {} + + const firstTab = initialTabs[0]; + const loadOptions = firstTab?.url + ? { query: { url: firstTab.url, title: firstTab.title || 'Grok' } } + : {}; + + win.loadFile(path.join(__dirname, '../index.html'), loadOptions); + + win.webContents.once('did-finish-load', () => { + broadcastThemeUpdate(); + // Open any additional tabs that came along in a multi-tab drag + const extraTabs = initialTabs.slice(1); + extraTabs.forEach((tab) => { + if (tab && tab.url) win.webContents.send('attach-tab', { url: tab.url, title: tab.title }); + }); + }); + + win.on('closed', () => { + if (mainWindow === win) { + const remaining = BrowserWindow.getAllWindows().filter((w) => !w.isDestroyed()); + mainWindow = remaining.length > 0 ? remaining[0] : null; + } + }); + + return win; +} + // Create window when Electron has finished initialization app.whenReady().then(() => { checkWmctrlAvailability(); + initializeAppOnce(); createWindow(); app.on('activate', () => { - // On macOS, re-create a window when the dock icon is clicked and no windows are open if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); @@ -457,25 +604,132 @@ function setupUrlHandling() { // Set up IPC handlers for renderer-to-main process communication function setupIpcHandlers() { + // ----- Browser-style tab drag / tear-off / re-attach ----- + + // Find a window whose tab strip is under the given screen point (for drop/merge) + const findTabBarWindowAt = (point, excludeId) => { + const wins = BrowserWindow.getAllWindows(); + for (const win of wins) { + if (win.isDestroyed()) continue; + if (excludeId != null && win.id === excludeId) continue; + if (dragGhostWindow && !dragGhostWindow.isDestroyed() && win.id === dragGhostWindow.id) continue; + let b; + try { b = win.getContentBounds(); } catch (_) { continue; } + const inX = point.x >= b.x && point.x <= b.x + b.width; + const inStrip = point.y >= b.y && point.y <= b.y + TAB_BAR_HEIGHT; + if (inX && inStrip) return win; + } + return null; + }; + + let lastHoverTargetId = null; + const clearDropIndicators = () => { + if (lastHoverTargetId == null) return; + const prev = BrowserWindow.getAllWindows().find((w) => w.id === lastHoverTargetId && !w.isDestroyed()); + if (prev) prev.webContents.send('drop-indicator', false); + lastHoverTargetId = null; + }; + + // Create the native, screen-roaming drag ghost when a drag begins + ipcMain.on('tab-drag-start', (_event, info) => { + try { createDragGhost(info || {}); } catch (_) {} + }); + + // Live highlight of a window's tab strip while a tab is dragged over it, + // plus driving the ghost's appearance (hidden / holographic / normal). + ipcMain.on('tab-drag-hover', (event) => { + const point = screen.getCursorScreenPoint(); + const sourceWin = BrowserWindow.fromWebContents(event.sender); + + // Over the source window's OWN strip => hide the ghost so the user sees the + // live in-strip reorder (drag a tab onto itself / its neighbours). + if (sourceWin && !sourceWin.isDestroyed()) { + let b = null; + try { b = sourceWin.getContentBounds(); } catch (_) {} + if (b) { + const overOwn = point.x >= b.x && point.x <= b.x + b.width && + point.y >= b.y && point.y <= b.y + TAB_BAR_HEIGHT; + if (overOwn) { + clearDropIndicators(); + setGhostState('hidden'); + return; + } + } + } + + const target = findTabBarWindowAt(point, sourceWin ? sourceWin.id : null); + const targetId = target ? target.id : null; + if (targetId !== lastHoverTargetId) { + clearDropIndicators(); + if (target) { + target.webContents.send('drop-indicator', true); + lastHoverTargetId = targetId; + } + } + // Over another window => holographic; over empty space => solid chip. + setGhostState(target ? 'ghost' : 'normal'); + }); + + ipcMain.on('tab-drag-end', () => { + clearDropIndicators(); + destroyDragGhost(); + }); + + // Decide what happens when a dragged tab (or group) is released outside its own strip + ipcMain.handle('tab-drop', (event, payload) => { + clearDropIndicators(); + destroyDragGhost(); + + const tabs = Array.isArray(payload?.tabs) ? payload.tabs.filter((t) => t && t.url) : []; + if (tabs.length === 0) return { attached: false, created: false }; + + const point = screen.getCursorScreenPoint(); + const sourceWin = BrowserWindow.fromWebContents(event.sender); + const target = findTabBarWindowAt(point, sourceWin ? sourceWin.id : null); + + if (target) { + // Merge the dragged tabs into the target window's tab strip + tabs.forEach((tab) => target.webContents.send('attach-tab', { url: tab.url, title: tab.title })); + if (target.isMinimized()) target.restore(); + target.focus(); + return { attached: true, created: false }; + } + + // Otherwise tear off into a brand-new window positioned under the cursor + createWindow({ + initialTabs: tabs, + position: { x: point.x - 140, y: point.y - 12 } + }); + return { attached: false, created: true }; + }); + + ipcMain.handle('close-current-window', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win && !win.isDestroyed()) { + win.close(); + return true; + } + return false; + }); + // Handle always-on-top toggle - ipcMain.handle('toggle-always-on-top', async () => { - if (!mainWindow) return false; + ipcMain.handle('toggle-always-on-top', async (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (!win || win.isDestroyed()) return false; // On Linux, use wmctrl if available for better GNOME compatibility if (os.platform() === 'linux') { if (wmctrlAvailable) { - const result = await toggleAlwaysOnTopLinux(mainWindow); + const result = await toggleAlwaysOnTopLinux(win); if (result) return true; } else { console.warn('Grok Desktop: wmctrl not available on Linux, AOT may not work'); } - // Fall back to Electron method if wmctrl fails or isn't available } - // Use Electron's built-in method (works on Windows/macOS, may not work reliably on Linux GNOME/Wayland) try { - const isAlwaysOnTop = mainWindow.isAlwaysOnTop(); - mainWindow.setAlwaysOnTop(!isAlwaysOnTop); + const isAlwaysOnTop = win.isAlwaysOnTop(); + win.setAlwaysOnTop(!isAlwaysOnTop); return !isAlwaysOnTop; } catch (error) { console.warn('Grok Desktop: Electron AOT toggle failed:', error.message); @@ -494,7 +748,7 @@ function setupIpcHandlers() { // Open About page in a new tab instead of a window ipcMain.handle('show-app-info', async () => { - const name = typeof app.getName === 'function' ? app.getName() : 'Grok Desktop'; + const name = typeof app.getName === 'function' ? app.getName() : 'Grok'; const version = typeof app.getVersion === 'function' ? app.getVersion() : '0.0.0'; const repoUrl = 'https://github.com/AnRkey/Grok-Desktop'; @@ -514,9 +768,9 @@ function setupIpcHandlers() { urlObj.searchParams.set('developer', developer); urlObj.searchParams.set('contact', contactUrl); - // Send the URL to the renderer to create a new tab - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('open-about-tab', urlObj.toString()); + const win = BrowserWindow.fromWebContents(_event.sender); + if (win && !win.isDestroyed()) { + win.webContents.send('open-about-tab', urlObj.toString()); } return { name, version };