From b01419663ebff5d9abf622e0c84455479b9d20b5 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 22:22:37 +0800 Subject: [PATCH 01/20] feat: normal app mode (dock visible, no auto-hide, draggable) - App Mode setting: Normal App (default) / Menu Bar - Normal mode: shows in Dock, no auto-hide on blur, window stays in place (draggable via title bar), minimize on hide - Menu Bar mode: current behavior (hidden dock, auto-hide, center on show) - Instant switching via IPC (no restart needed) - Title bar uses -webkit-app-region: drag for frameless window Co-Authored-By: Claude Opus 4.6 (1M context) --- src/electron-api.d.ts | 4 ++++ src/main.ts | 52 +++++++++++++++++++++++++++++++++++-------- src/popup.tsx | 19 ++++++++++++++++ src/preload.ts | 2 ++ src/switcher-ui.tsx | 5 ++++- 5 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/electron-api.d.ts b/src/electron-api.d.ts index 0a76efd..9d799fa 100644 --- a/src/electron-api.d.ts +++ b/src/electron-api.d.ts @@ -28,6 +28,10 @@ interface IElectronAPI { getUpdateStatus: () => Promise<{ status: string; releaseName?: string; error?: string } | null>; onUpdateStatus: (callback: IpcCallback) => void; + // App mode + getAppMode: () => Promise; + setAppMode: (mode: string) => void; + // Session terminal settings getSessionTerminalApp: () => Promise; setSessionTerminalApp: (app: string) => void; diff --git a/src/main.ts b/src/main.ts index d03fc9e..b6d8901 100644 --- a/src/main.ts +++ b/src/main.ts @@ -81,6 +81,9 @@ let serverProcess: any; const WIN_WIDTH = 800; const WIN_HEIGHT = 600; +// App mode: 'normal' (dock visible, no auto-hide) or 'menubar' (hidden dock, auto-hide on blur) +let appMode: 'normal' | 'menubar' = 'normal'; // default to normal for new users + const getWindowPosition = () => { const primaryDisplay = screen.getPrimaryDisplay(); const { width, height } = primaryDisplay.workAreaSize; @@ -95,19 +98,23 @@ const getWindowPosition = () => { // NOTE: setVisibleOnAllWorkspaces is needed ? const showSwitcherWindow = () => { let window = getSwitcherWindow(); - + if (!window) { // Recreate window if it has been destroyed switcherWindow = createSwitcherWindow(); window = switcherWindow; } - - const position = getWindowPosition(); - window.setPosition(position.x, position.y, false); + + // Menu bar mode: always center on screen. Normal mode: keep last position. + if (appMode === 'menubar') { + const position = getWindowPosition(); + window.setPosition(position.x, position.y, false); + } + if (window.isMinimized()) { + window.restore(); + } window.show(); - // mainWindow.setVisibleOnAllWorkspaces(true); window.focus(); - // mainWindow.setVisibleOnAllWorkspaces(false); }; const showAIAssistantWindow = () => { @@ -253,11 +260,17 @@ const getSwitcherWindow = () => { const hideSwitcherWindow = () => { const window = getSwitcherWindow(); if (window) { - window.hide(); + if (appMode === 'normal') { + window.minimize(); + } else { + window.hide(); + } } }; const onBlur = (event: any) => { + // Normal mode: don't auto-hide on blur + if (appMode === 'normal') return; hideSwitcherWindow(); }; @@ -1016,6 +1029,12 @@ const trayToggleEvtHandler = async () => { (async () => { await app.whenReady(); + // Load app mode setting early (before window creation) + appMode = ((await settings.get('app-mode')) as 'normal' | 'menubar') || 'normal'; + if (appMode === 'menubar') { + app.dock.hide(); + } + // Auto-update: check for updates via update.electronjs.org (non-MAS only) if (!isMAS()) { try { @@ -1293,7 +1312,7 @@ const trayToggleEvtHandler = async () => { } else { const window = getSwitcherWindow(); - if (window && window.isVisible()) { + if (window && window.isVisible() && !window.isMinimized()) { if (isDebug) { console.log('Switcher window visible, hiding it'); } @@ -1934,6 +1953,21 @@ ipcMain.handle('get-session-statuses', async () => { return obj; }); +ipcMain.handle('get-app-mode', async () => { + return appMode; +}); + +ipcMain.on('set-app-mode', async (_event, mode: string) => { + const newMode = mode === 'menubar' ? 'menubar' : 'normal'; + await settings.set('app-mode', newMode); + appMode = newMode; + if (newMode === 'menubar') { + app.dock.hide(); + } else { + app.dock.show(); + } +}); + ipcMain.handle('get-session-terminal-app', async () => { return (await settings.get('session-terminal-app')) || 'iterm2'; }); @@ -2077,4 +2111,4 @@ ipcMain.handle('detect-active-ide-projects', async () => { return Array.from(folderNames); }); -app.dock.hide(); +// app.dock.hide() moved to async init block (after settings loaded) diff --git a/src/popup.tsx b/src/popup.tsx index 0fc8d6d..e2b9de5 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -64,6 +64,7 @@ const PopupDefaultExample = ({ useState('switcher_window'); const [isMASBuild, setIsMASBuild] = useState(false); const [sessionStatusHooks, setSessionStatusHooks] = useState(true); + const [appModeState, setAppModeState] = useState('normal'); const [ideDataAccessGranted, setIdeDataAccessGranted] = useState(false); const [shortcuts, setShortcuts] = useState({ quickSwitcher: 'Command+Control+R', @@ -81,6 +82,9 @@ const PopupDefaultExample = ({ window.electronAPI.getAppVersion().then((version: string) => { setAppVersion(version); }); + window.electronAPI.getAppMode().then((mode: string) => { + setAppModeState(mode || 'normal'); + }); window.electronAPI.getSessionTerminalApp().then((app: string) => { setSessionTerminalApp(app || 'iterm2'); }); @@ -338,6 +342,21 @@ const PopupDefaultExample = ({ +
+ App Mode + +
Left-Click
)} - - - {/* Projects settings (only in Projects tab) */} - {switcherMode === 'projects' && ( -
-
- Projects -
-
- IDE - { + const ide = e.target.value; + setIdePreference(ide); + window.electronAPI.notifyIDEPreferenceChanged(ide); + if (isMASBuild) { + window.electronAPI.checkIDEDataAccess(ide).then((granted: boolean) => { + setIdeDataAccessGranted(granted); + }); + } + }} + style={selectStyle} + > + + + + {isMASBuild && ( + - )} -
-
- Claude Session Launch - {[ - { keys: '\u2318+Enter', label: 'New Claude Session' }, - { keys: '\u21E7+Enter', label: 'New Claude (CodeV Term)' }, - { keys: '\u2318+Click', label: 'New Claude Session' }, - ].map((row) => ( -
- {row.keys} - {row.label} -
- ))} + {ideDataAccessGranted ? '✓' : 'Grant'} + + )} +
+
+ Working Dir +
+ {workingFolderPath || 'None'}
+
+
)} - {/* Sessions settings (only in Sessions tab) */} - {switcherMode === 'sessions' && ( -
-
- Sessions -
-
- Session Preview - { + const val = e.target.value; + setSessionDisplayMode(val); + window.electronAPI.setSessionDisplayMode(val); + if (saveCallback) saveCallback('sessionDisplayMode', val); + }} + style={selectStyle} + > + + + + +
+
+ ◀ Assistant response always shown +
+
+ Session Status (hooks) +
-
- ◀ Assistant response always shown -
-
- Session Status (hooks) - -
+ /> + +
+ )} - {/* Shortcuts section */} -
-
- - Shortcuts - + {/* Shortcuts tab */} + {settingsTab === 'shortcuts' && ( +
+
+ Global Shortcuts Reset @@ -672,34 +660,18 @@ const PopupDefaultExample = ({ {shortcutError || 'Press keys...'}
) : ( - + {acceleratorToDisplay(shortcuts[row.key as keyof typeof shortcuts])} )} - - {row.label} - + {row.label} { if (editingShortcut === row.key) { - // Cancel editing — resume the shortcut window.electronAPI.resumeShortcut(row.key); setEditingShortcut(null); setShortcutError(''); } else { - // Start editing — pause the shortcut so it doesn't trigger if (editingShortcut) { window.electronAPI.resumeShortcut(editingShortcut); } @@ -708,17 +680,25 @@ const PopupDefaultExample = ({ setShortcutError(''); } }} - style={{ - fontSize: '11px', - color: editingShortcut === row.key ? '#e05252' : THEME.primary, - cursor: 'pointer', - flexShrink: 0, - }} + style={{ fontSize: '11px', color: editingShortcut === row.key ? '#e05252' : THEME.primary, cursor: 'pointer', flexShrink: 0 }} > {editingShortcut === row.key ? 'Cancel' : 'Edit'}
))} +
+ Claude Session Launch + {[ + { keys: '\u2318+Enter', label: 'New Claude Session' }, + { keys: '\u21E7+Enter', label: 'New Claude (CodeV Term)' }, + { keys: '\u2318+Click', label: 'New Claude Session' }, + ].map((row) => ( +
+ {row.keys} + {row.label} +
+ ))} +
Tab Switching {[ @@ -734,6 +714,7 @@ const PopupDefaultExample = ({ ))}
+ )}
)} From a771bebe2acea4c3a32d4926b1e91748a30d0e20 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 23:42:30 +0800 Subject: [PATCH 10/20] style: reorder settings, add hints, rename Terminal.app - Order: Launch at Login > App Mode > Left-Click > Default Tab > Working Dir > Launch Terminal > Launch Mode > IDE - Hints: Working Dir (projects/term), Launch Terminal (sessions), Launch Mode (tab/window), IDE (projects), Left-Click (tray) - Terminal renamed to Terminal.app in dropdown - Claude Session Launch moved below Tab Switching, noted (in Projects) - Working Dir always visible (removed switcherMode conditional) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/popup.tsx | 132 +++++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 67 deletions(-) diff --git a/src/popup.tsx b/src/popup.tsx index fe9c901..985064c 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -349,53 +349,6 @@ const PopupDefaultExample = ({ {/* General tab */} {settingsTab === 'general' && (
-
- Default Tab - -
-
- App Mode - -
-
- Left-Click - -
Launch at Login
- {switcherMode !== 'sessions' && ( -
- Working Dir +
+ App Mode + +
+
+ Left-Click (tray) + +
+
+ Default Tab + +
+
+ Working Dir (projects/term)
{workingFolderPath || 'None'}
@@ -452,10 +451,9 @@ const PopupDefaultExample = ({ > 📁 -
- )} +
- Launch Terminal + Launch Terminal (sessions) { @@ -490,7 +488,7 @@ const PopupDefaultExample = ({
)}
- IDE + IDE (projects) { @@ -524,19 +524,6 @@ const PopupDefaultExample = ({ )}
-
- Working Dir -
- {workingFolderPath || 'None'} -
- -
)} From 1a876e19bcb9be282e46b5c141b7ef5819375420 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 23:49:33 +0800 Subject: [PATCH 13/20] fix: remove Launch Terminal hint, rename Launch Mode to Open In Co-Authored-By: Claude Opus 4.6 (1M context) --- src/popup.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/popup.tsx b/src/popup.tsx index 991c622..85d5d96 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -453,7 +453,7 @@ const PopupDefaultExample = ({
- Launch Terminal (sessions) + Launch Terminal { From f0f9f570232b62a9a2c7234f080d2703b1593187 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 23:52:02 +0800 Subject: [PATCH 14/20] fix: add hint to Launch Terminal, simplify Open In label Co-Authored-By: Claude Opus 4.6 (1M context) --- src/popup.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/popup.tsx b/src/popup.tsx index 85d5d96..6e30f3f 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -453,7 +453,7 @@ const PopupDefaultExample = ({
- Launch Terminal + Launch Terminal (projects/sessions) { From 1dde9849ad31d4ae43a5d17b035b4d7033afa5b7 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 6 Apr 2026 23:53:10 +0800 Subject: [PATCH 15/20] style: indent Open In as sub-item of Launch Terminal Co-Authored-By: Claude Opus 4.6 (1M context) --- src/popup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popup.tsx b/src/popup.tsx index 6e30f3f..969a595 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -472,7 +472,7 @@ const PopupDefaultExample = ({
{(sessionTerminalApp === 'iterm2' || sessionTerminalApp === 'terminal' || sessionTerminalApp === 'ghostty') && (
- Open In + Open In { From aa5d8a0f811bc7ccf42e933a5d32cf5488b43098 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Tue, 7 Apr 2026 00:06:27 +0800 Subject: [PATCH 17/20] docs: add App Mode comparison table to README Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c83c087..797f0a2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Quick switcher for VS Code/Cursor projects, Claude Code session manager with liv ### Quick Switcher for VS Code / Cursor Projects -Spotlight-like quick open: press `⌃+⌘+R` or click the menu bar icon to launch the Quick Switcher. Search and select a project to open or switch to it in VS Code or Cursor — even if the IDE is not running yet. +Press `⌃+⌘+R` or click the menu bar icon to launch the Quick Switcher. Search and select a project to open or switch to it in VS Code or Cursor — even if the IDE is not running yet. In Normal App mode, the window stays visible for monitoring; in Menu Bar mode, it works like Spotlight. - **Recent projects** (white items): your latest VS Code/Cursor folders, workspaces, and recently opened files — read directly from IDE data, no extension required - **Working folder items** (green items): first-level subfolders found by scanning a folder you choose (Settings → Working Directory) @@ -52,6 +52,22 @@ CodeV includes a built-in terminal tab (powered by xterm.js + node-pty, same tec - `Cmd+←/→` jumps to beginning/end of line - **"Claude in Terminal" button**: launches a new Claude Code session in the configured external terminal using the current working directory +### App Mode + +CodeV supports two window modes, configurable in Settings → App Mode: + +| | Normal App (default) | Menu Bar | +|--|--|--| +| **Dock** | Visible | Hidden | +| **On blur** | Stays visible | Auto-hides | +| **Window position** | Remembers last position, draggable | Centers on screen each time | +| **On startup** | Shows window | Hidden until shortcut/tray click | +| **`⌃+⌘+R`** | Toggle show/hide | Toggle show/hide | +| **Click Dock icon** | Shows hidden window | N/A | +| **Best for** | Dashboard / monitoring (keep in corner) | Quick access (spotlight-like) | + +**Real-time updates when unfocused (Normal mode):** Status dots, final assistant/user messages, and session order update via fs.watch — no need to re-focus. New sessions and full list refresh only occur on re-focus. + ### Tab Switching | Shortcut | Action | From 25e456b5ab11d0a787f99cd01506375682c95760 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Tue, 7 Apr 2026 00:10:42 +0800 Subject: [PATCH 18/20] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20ban?= =?UTF-8?q?ner=20timeout,=20dock=20await,=20startup=20timing,=20shortcut?= =?UTF-8?q?=20resume?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clear banner timeout before scheduling new one (cubic P2) - Await app.dock.show() (cubic P2) - Delay showSwitcherWindow to after bootstrap (CodeRabbit) - Resume paused shortcut when switching settings tabs (CodeRabbit) - Use actual shortcut key in mode switch banner (CodeRabbit) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main.ts | 7 ++++--- src/popup.tsx | 10 +++++++++- src/switcher-ui.tsx | 25 +++++++++++++++---------- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/main.ts b/src/main.ts index c6320bb..6d1e771 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1081,9 +1081,10 @@ const trayToggleEvtHandler = async () => { } switcherWindow = createSwitcherWindow(); - // Normal mode: show window immediately on startup + // Normal mode: show window after bootstrap (server must be ready for API calls) if (appMode === 'normal') { - showSwitcherWindow(); + // Delay show to ensure bootstrap() has completed (runs earlier in this block) + setTimeout(() => showSwitcherWindow(), 100); } if (isDebug) { console.log('when ready'); @@ -1973,7 +1974,7 @@ ipcMain.on('set-app-mode', async (_event, mode: string) => { if (newMode === 'menubar') { app.dock.hide(); } else { - app.dock.show(); + await app.dock.show(); } // Notify renderer to update drag region const window = getSwitcherWindow(); diff --git a/src/popup.tsx b/src/popup.tsx index 98a4817..60aec8d 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -327,7 +327,15 @@ const PopupDefaultExample = ({ {(['general', 'sessions', 'shortcuts'] as const).map((tab) => (