diff --git a/CHANGELOG.md b/CHANGELOG.md index 944ddc8..986233d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 1.0.70 + +- Feat: Normal App mode — window stays visible, shows in Dock, draggable + - Toggle in Settings: Normal App (default for new users) / Menu Bar + - Instant switching, no restart needed + - `Cmd+Ctrl+R` toggles show/hide in both modes + - Title bar shows "Dev Hub" sub-title + mode indicator + shortcut key + - Banner on first launch and mode switch (auto-dismiss) + - Clicking Dock icon shows hidden window +- Feat: Settings UI redesigned with tabs (General / Sessions / Shortcuts) + - All settings visible without scrolling + - No more content changing based on active main tab + - Hints on context-specific settings (projects/sessions/tray) +- Style: Terminal renamed to Terminal.app in Launch Terminal dropdown +- Style: title bar padding reduced for tighter layout + ## 1.0.69 - Feat: adaptive VS Code resume via IDE lock file polling diff --git a/README.md b/README.md index c83c087..e5fc6d1 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ 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) +- **Working folder items** (green items): first-level subfolders found by scanning a folder you choose (Settings → General → Working Dir) - **Git branch display**: shows the current branch for each recently opened project - Multi-word search across project names, paths, and branch names -- Supports VS Code and Cursor — switch between them in Settings → IDE Preference +- Supports VS Code and Cursor — switch between them in Settings → General → IDE - Remove items from the recent list by hovering and clicking "x" - **Quick-launch Claude session**: `⌘+Enter` to launch a new Claude Code session in the configured terminal, `⇧+Enter` to launch in CodeV's embedded terminal, `⌘+Click` as mouse alternative @@ -46,12 +46,28 @@ For the full same-cwd accuracy matrix (detection + switch by launch method and t CodeV includes a built-in terminal tab (powered by xterm.js + node-pty, same technology as VS Code's integrated terminal). Press `⌃+⌘+T` from anywhere (global shortcut) or `⌘+3` when CodeV is in foreground to open it. - Pre-spawned on app start for instant access -- Default working directory: Settings → Working Directory (fallback to home) +- Default working directory: Settings → General → Working Dir (fallback to home) - Terminal state preserved when switching tabs - `⌘+K` clears screen, `Shift+Enter` for multi-line input (Claude Code compatible) - `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 → General → 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 | diff --git a/package.json b/package.json index ad9779f..054a1a1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "CodeV", "productName": "CodeV", - "version": "1.0.69", + "version": "1.0.70", "description": "Quick switcher for VS Code, Cursor, and Claude Code sessions", "repository": { "type": "git", diff --git a/src/electron-api.d.ts b/src/electron-api.d.ts index 0a76efd..b3bd35f 100644 --- a/src/electron-api.d.ts +++ b/src/electron-api.d.ts @@ -28,6 +28,12 @@ interface IElectronAPI { getUpdateStatus: () => Promise<{ status: string; releaseName?: string; error?: string } | null>; onUpdateStatus: (callback: IpcCallback) => void; + // App mode + getAppMode: () => Promise; + setAppMode: (mode: string) => void; + onAppModeChanged: (callback: IpcCallback) => void; + onShortcutsUpdated: (callback: IpcCallback) => void; + // Session terminal settings getSessionTerminalApp: () => Promise; setSessionTerminalApp: (app: string) => void; diff --git a/src/main.ts b/src/main.ts index d03fc9e..f947fc9 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); + + if (appMode === 'menubar') { + // Menu bar mode: always center on screen + 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 = () => { @@ -258,6 +265,8 @@ const hideSwitcherWindow = () => { }; const onBlur = (event: any) => { + // Normal mode: don't auto-hide on blur + if (appMode === 'normal') return; hideSwitcherWindow(); }; @@ -506,6 +515,11 @@ app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { switcherWindow = createSwitcherWindow(); } + // Normal mode: clicking Dock icon shows hidden window + const window = getSwitcherWindow(); + if (window && !window.isVisible()) { + showSwitcherWindow(); + } }); /** https://www.electronjs.org/docs/latest/tutorial/ipc */ @@ -1016,6 +1030,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 { @@ -1255,6 +1275,11 @@ const trayToggleEvtHandler = async () => { // Load user settings await loadUserSettings(); + // Normal mode: show window after bootstrap + settings loaded (server ready for API calls) + if (appMode === 'normal') { + showSwitcherWindow(); + } + let title = ''; if (!isDebug) { title = ``; @@ -1293,7 +1318,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'); } @@ -1679,6 +1704,8 @@ const trayToggleEvtHandler = async () => { if (registered) { await settings.set(`shortcut-${key}`, accelerator); await syncTrayShortcuts(); + // Notify switcher window to update shortcut display + switcherWindow?.webContents.send('shortcuts-updated', await getCurrentShortcuts()); return { success: true }; } else { // Re-register the old shortcut since the new one failed @@ -1694,6 +1721,8 @@ const trayToggleEvtHandler = async () => { } await registerAllShortcuts(); await syncTrayShortcuts(); + // Notify switcher window to update shortcut display + switcherWindow?.webContents.send('shortcuts-updated', DEFAULT_SHORTCUTS); return DEFAULT_SHORTCUTS; }); })(); @@ -1934,6 +1963,31 @@ 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 { + await app.dock.show(); + } + // Notify renderer to update drag region + const window = getSwitcherWindow(); + if (window) { + window.webContents.send('app-mode-changed', newMode); + // Re-center when switching to menu bar mode + if (newMode === 'menubar') { + const position = getWindowPosition(); + window.setPosition(position.x, position.y, false); + } + } +}); + ipcMain.handle('get-session-terminal-app', async () => { return (await settings.get('session-terminal-app')) || 'iterm2'; }); @@ -2077,4 +2131,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..1be7497 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -64,6 +64,8 @@ const PopupDefaultExample = ({ useState('switcher_window'); const [isMASBuild, setIsMASBuild] = useState(false); const [sessionStatusHooks, setSessionStatusHooks] = useState(true); + const [appModeState, setAppModeState] = useState('normal'); + const [settingsTab, setSettingsTab] = useState<'general' | 'sessions' | 'shortcuts'>('general'); const [ideDataAccessGranted, setIdeDataAccessGranted] = useState(false); const [shortcuts, setShortcuts] = useState({ quickSwitcher: 'Command+Control+R', @@ -81,6 +83,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'); }); @@ -317,43 +322,42 @@ const PopupDefaultExample = ({ - {/* General settings (always visible) */} -
-
- General -
-
- Default Tab - -
-
- Left-Click - -
+ {tab} + + ))} +
+ + {/* General tab */} + {settingsTab === 'general' && ( +
Launch at Login
- {switcherMode !== 'sessions' && ( -
- Working Dir +
+ App Mode + +
+
+ Left-Click (tray) + +
+
+ Default Tab + +
+
+ Working Dir (projects/term)
{workingFolderPath || 'None'}
@@ -410,10 +460,9 @@ const PopupDefaultExample = ({ > 📁 -
- )} +
- Launch Terminal + Launch Terminal (projects/sessions) { @@ -447,175 +496,127 @@ const PopupDefaultExample = ({
)} -
- - {/* 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'} + + )}
+ )} - {/* 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 @@ -653,34 +654,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); } @@ -689,18 +674,13 @@ 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'}
))} -
+
Tab Switching {[ { keys: 'Tab', label: 'Projects \u2194 Sessions' }, @@ -714,7 +694,21 @@ const PopupDefaultExample = ({
))}
+
+ Claude Session Launch (in Projects) + {[ + { 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} +
+ ))} +
+ )}
)} diff --git a/src/preload.ts b/src/preload.ts index 0ea4d37..6e6a028 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -30,6 +30,10 @@ contextBridge.exposeInMainWorld('electronAPI', { installUpdate: () => ipcRenderer.send('install-update'), getUpdateStatus: () => ipcRenderer.invoke('get-update-status'), onUpdateStatus: (callback: any) => ipcRenderer.on('update-status', callback), + getAppMode: () => ipcRenderer.invoke('get-app-mode'), + setAppMode: (mode: string) => ipcRenderer.send('set-app-mode', mode), + onAppModeChanged: (callback: any) => ipcRenderer.on('app-mode-changed', callback), + onShortcutsUpdated: (callback: any) => ipcRenderer.on('shortcuts-updated', callback), getSessionTerminalApp: () => ipcRenderer.invoke('get-session-terminal-app'), setSessionTerminalApp: (app: string) => ipcRenderer.send('set-session-terminal-app', app), getSessionTerminalMode: () => ipcRenderer.invoke('get-session-terminal-mode'), diff --git a/src/switcher-ui.tsx b/src/switcher-ui.tsx index 92421b8..a9b981f 100644 --- a/src/switcher-ui.tsx +++ b/src/switcher-ui.tsx @@ -360,6 +360,10 @@ function SwitcherApp() { const allSessionsRef = useRef([]); const lastAssistantFetchRef = useRef>({}); const sessionSearchRef2 = useRef(''); // tracks current search value for use in closures + const [currentAppMode, setCurrentAppMode] = useState('menubar'); + const [modeBanner, setModeBanner] = useState(null); + const [quickSwitcherShortcut, setQuickSwitcherShortcut] = useState(''); + const bannerTimeoutRef = useRef | null>(null); const updateWorkingPathUIAndList = async (path: string) => { setWorkingFolderPath(path); @@ -668,6 +672,49 @@ function SwitcherApp() { } }); + // Load shortcut for display + window.electronAPI.getShortcuts().then((s: any) => { + if (s?.quickSwitcher) { + const display = s.quickSwitcher + .replace('Command', 'Cmd') + .replace('Control', 'Ctrl') + .replace(/\+/g, '+'); + setQuickSwitcherShortcut(display); + shortcutDisplay = display; + } + }); + + const showBanner = (msg: string, durationMs = 5000) => { + if (bannerTimeoutRef.current) clearTimeout(bannerTimeoutRef.current); + setModeBanner(msg); + bannerTimeoutRef.current = setTimeout(() => setModeBanner(null), durationMs); + }; + + // Listen for app mode changes to enable/disable drag + let shortcutDisplay = ''; // will be set by getShortcuts + window.electronAPI.getAppMode().then((mode: string) => { + const m = mode || 'normal'; + setCurrentAppMode(m); + if (m === 'normal') { + showBanner('Normal App mode — drag to reposition. Switch to Menu Bar mode in Settings.', 6000); + } + }); + window.electronAPI.onShortcutsUpdated((_event: any, s: any) => { + if (s?.quickSwitcher) { + shortcutDisplay = s.quickSwitcher.replace('Command', 'Cmd').replace('Control', 'Ctrl').replace(/\+/g, '+'); + setQuickSwitcherShortcut(shortcutDisplay); + } + }); + window.electronAPI.onAppModeChanged((_event: any, mode: string) => { + setCurrentAppMode(mode); + const key = shortcutDisplay || 'Cmd+Ctrl+R'; + if (mode === 'normal') { + showBanner('Switched to Normal App mode — window stays visible and is draggable.'); + } else { + showBanner(`Switched to Menu Bar mode — window auto-hides. Use ${key} to toggle.`); + } + }); + window.electronAPI.onCheckTerminalAndHide(() => { if (modeRef.current === 'terminal') { window.electronAPI.hideApp(); @@ -903,9 +950,11 @@ function SwitcherApp() { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - padding: '10px 15px', + padding: '8px 15px', borderBottom: '1px solid #333', backgroundColor: '#252525', + // @ts-ignore — Electron-specific CSS property for frameless window dragging + WebkitAppRegion: currentAppMode === 'normal' ? 'drag' : undefined, }} >
CodeV + + Dev Hub + {currentAppMode && ( + + {currentAppMode === 'normal' ? 'normal mode' : 'menu bar mode'} + + )} +
-
+ {/* @ts-ignore */} +
+ {quickSwitcherShortcut && ( + + {quickSwitcherShortcut} + + )}
+ {/* Mode change banner */} + {modeBanner && ( +
+ {modeBanner} + setModeBanner(null)} + > + x + +
+ )} + {mode !== 'terminal' && (mode === 'sessions' ? (