From 1c5c24abe37505737e158395e4eaddfa2e22685d Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 5 Oct 2026 16:11:15 +0800 Subject: [PATCH 1/4] fix(switcher): Quick Switcher leaves the Term tab behind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Background the app from the Term tab, press Ctrl+Cmd+R, and you were back on Term. Ctrl+Cmd+T is the shortcut for Term; Ctrl+Cmd+R is for the switcher, and Term is not the switcher. Two separate causes, neither of them a state bug: * "Default Tab" was read once, at app start, and passed to createSwitcherWindow. Nothing re-applied it afterwards. * quickSwitcherCallback only showed, hid or focused the window. It sent no tab message at all, so the renderer kept its last mode. Showing the switcher now leaves ONLY the Term tab, for the default tab. A window parked on Projects or Sessions keeps its place, which is the half worth keeping — this is not a reset-to-default on every show. When the default tab is itself Terminal, the same branch stays put. Also fixes a smaller one on the way: a switcher window created BY the shortcut (after every window was closed) called createSwitcherWindow with no argument, so it carried no `#mode=` hash and ignored Default Tab entirely. Both creation paths now pass it. Formatting: the four touched files already carry 18 to 1814 prettier/prettier findings each and the repo has no lint gate, so the new lines match their neighbours rather than the formatter. Co-Authored-By: Claude Opus 5 (1M context) --- src/electron-api.d.ts | 2 ++ src/main.ts | 18 +++++++++++++++--- src/preload.ts | 1 + src/switcher-ui.tsx | 14 ++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/electron-api.d.ts b/src/electron-api.d.ts index 27ee30c..aec6d95 100644 --- a/src/electron-api.d.ts +++ b/src/electron-api.d.ts @@ -312,6 +312,8 @@ interface IElectronAPI { onWorkingFolderIterated: (callback: IpcCallback) => void; onFocusWindow: (callback: IpcCallback) => void; onSwitchToTerminal: (callback: IpcCallback) => void; + /** Quick Switcher was shown: if the window sits on Term, go to the default tab. */ + onQuickSwitcherLeaveTerminal: (callback: IpcCallback) => void; onCheckTerminalAndHide: (callback: IpcCallback) => void; onXWinNotFound: (callback: IpcCallback) => void; diff --git a/src/main.ts b/src/main.ts index 922221a..bbbfc55 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1728,16 +1728,26 @@ const trayToggleEvtHandler = async () => { }; // Shortcut callback: Quick Switcher - const quickSwitcherCallback = () => { + const quickSwitcherCallback = async () => { if (isDebug) { console.log('Command+Control+R triggered - always shows Switcher window'); } + // "Default Tab" used to apply only when the window was CREATED, and nothing + // re-applied it afterwards — so a switcher left on the Term tab came back to + // Term. Term is not what this shortcut is for; it has its own (Ctrl+Cmd+T). + // Showing the switcher therefore leaves the Term tab for the default, while + // Projects and Sessions keep their place, which is the part worth keeping. + const defaultTab = + ((await settings.get('default-switcher-mode')) as string) || 'projects'; + const leaveTerminalTab = (w: BrowserWindow | null) => + w?.webContents.send('quick-switcher-leave-terminal', defaultTab); + if (BrowserWindow.getAllWindows().length === 0) { if (isDebug) { console.log('No window, creating main window'); } - switcherWindow = createSwitcherWindow(); + switcherWindow = createSwitcherWindow(defaultTab); showSwitcherWindow(); } else { const window = getSwitcherWindow(); @@ -1758,17 +1768,19 @@ const trayToggleEvtHandler = async () => { } window.show(); window.focus(); + leaveTerminalTab(window); } } else if (window) { if (isDebug) { console.log('Switcher window exists but hidden, showing it'); } + leaveTerminalTab(window); showSwitcherWindow(); } else { if (isDebug) { console.log('Switcher window was destroyed, creating a new one'); } - switcherWindow = createSwitcherWindow(); + switcherWindow = createSwitcherWindow(defaultTab); showSwitcherWindow(); } } diff --git a/src/preload.ts b/src/preload.ts index dc34333..f0a6605 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -166,6 +166,7 @@ contextBridge.exposeInMainWorld('electronAPI', { openExternal: (url: string) => ipcRenderer.send('open-external', url), onFocusWindow: (callback: any) => ipcRenderer.on('window-focus', callback), onSwitchToTerminal: (callback: any) => ipcRenderer.on('switch-to-terminal', callback), + onQuickSwitcherLeaveTerminal: (callback: any) => ipcRenderer.on('quick-switcher-leave-terminal', callback), onCheckTerminalAndHide: (callback: any) => ipcRenderer.on('check-terminal-and-hide', callback), onXWinNotFound: (callback: any) => ipcRenderer.on('xwin-not-found', callback), diff --git a/src/switcher-ui.tsx b/src/switcher-ui.tsx index b6e2866..e7ae328 100644 --- a/src/switcher-ui.tsx +++ b/src/switcher-ui.tsx @@ -2077,6 +2077,20 @@ function SwitcherApp() { setMode('terminal'); }); + // The Quick Switcher is being shown. Only the Term tab is left behind — a + // window parked on Projects or Sessions keeps its place, so this is not a + // "reset to the default every time". If the default IS terminal, staying put + // falls out of the same branch. + window.electronAPI.onQuickSwitcherLeaveTerminal((_e: unknown, target: unknown) => { + if (modeRef.current !== 'terminal') return; + // Anything that is not a switcher tab — 'terminal' itself, or a value an + // older settings file could hold — means stay put rather than guess. + if (target !== 'projects' && target !== 'sessions') return; + modeRef.current = target; + setMode(target); + if (target === 'sessions') fetchClaudeSessions(); + }); + // Account count decides whether the ⌥⌘+Enter picker hint is shown. // Re-checked when the embedded Settings popup mutates accounts (same-window // CustomEvent) and on window focus (covers CLI-side changes) — no restart From 4e864511fd8b00d3ebb3d36ef51a535990dcc496 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 5 Oct 2026 16:12:22 +0800 Subject: [PATCH 2/4] chore: 1.0.93 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b5a75..c14592c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.0.93 + +- Fix: **`⌃+⌘+R` no longer comes back to the Term tab.** Leave the app sitting on Term, switch to another app, press the Quick Switcher shortcut, and it reopened on Term — but Term has its own shortcut (`⌃+⌘+T`), and the switcher is what `⌃+⌘+R` is for. Showing the switcher now leaves **only** the Term tab, for whatever **Settings → General → Default Tab** names. A window parked on Projects or Sessions keeps its place, so this is not a reset-to-default on every show; and when Default Tab is itself *Terminal*, staying on Term is the same rule, not an exception. Two causes, neither of them a state bug: Default Tab was read once at app start and nothing re-applied it, and the Quick Switcher callback only showed, hid or focused the window without ever naming a tab +- Fix: a switcher window created **by the shortcut** — after every window had been closed — ignored Default Tab entirely and opened whichever tab the renderer falls back to, because that path built the window without the `#mode=` hash the startup path passes. Both creation paths now pass it + ## 1.0.92 - Feat: **a non-anchor account can share the anchor's auto-memory, per project** (Settings → Accounts → Sharing → *Memory: share with the anchor, per project*; `codev account share-memory on|off`). Auto-memory is stored per project under each account's own config dir, so two accounts on one machine kept two memories for the same repository. Turning this on points the other account at the anchor's copy of whichever repository a session starts in, computed at launch rather than baked in — a repository, its subdirectories and all of its linked worktrees resolve to one memory (the key is the git common directory's parent; outside a repository it is the directory itself, with symlinks resolved — Claude Code files a session under the physical path). Off by default, and the anchor is never offered it: its memory is the one the others share diff --git a/package.json b/package.json index 83a65c7..e115246 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "CodeV", "productName": "CodeV", - "version": "1.0.92", + "version": "1.0.93", "description": "Quick switcher for VS Code, Cursor, and Claude Code sessions", "repository": { "type": "git", From 246045d25298de0e622b5b1b8ed84593c5509a29 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 5 Oct 2026 16:25:39 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(switcher):=20review=20round=20=E2=80=94?= =?UTF-8?q?=20one=20Default=20Tab=20reader,=20no=20double=20fetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All seven review findings were checked against the code and hold. * One reader for Default Tab (readDefaultTab): try/catch, because electron-settings rejects on a malformed file and the now-async shortcut callback would otherwise never open the window — a failure the synchronous callback did not have. Unknown values normalise to 'projects'. Startup, the Quick Switcher and app 'activate' all use it; 'activate' was a third creation path still passing no mode. * The tab message is sent before show/focus in the covered-window branch too, matching the hidden-window branch. * The renderer listener no longer fetches sessions: main-to-renderer messages arrive in send order, so the focus event that follows sees 'sessions' and refreshes once. Before, it refreshed twice. * CHANGELOG corrected: a hash-less window did NOT ignore Default Tab — the renderer re-applies it on mount — it flashed Projects first. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++-- src/main.ts | 30 ++++++++++++++++++++++++------ src/switcher-ui.tsx | 3 ++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c14592c..77b5287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ ## 1.0.93 -- Fix: **`⌃+⌘+R` no longer comes back to the Term tab.** Leave the app sitting on Term, switch to another app, press the Quick Switcher shortcut, and it reopened on Term — but Term has its own shortcut (`⌃+⌘+T`), and the switcher is what `⌃+⌘+R` is for. Showing the switcher now leaves **only** the Term tab, for whatever **Settings → General → Default Tab** names. A window parked on Projects or Sessions keeps its place, so this is not a reset-to-default on every show; and when Default Tab is itself *Terminal*, staying on Term is the same rule, not an exception. Two causes, neither of them a state bug: Default Tab was read once at app start and nothing re-applied it, and the Quick Switcher callback only showed, hid or focused the window without ever naming a tab -- Fix: a switcher window created **by the shortcut** — after every window had been closed — ignored Default Tab entirely and opened whichever tab the renderer falls back to, because that path built the window without the `#mode=` hash the startup path passes. Both creation paths now pass it +- Fix: **`⌃+⌘+R` no longer comes back to the Term tab.** Leave the app sitting on Term, switch to another app, press the Quick Switcher shortcut, and it reopened on Term — but Term has its own shortcut (`⌃+⌘+T`), and the switcher is what `⌃+⌘+R` is for. When the switcher is shown from the Term tab, it now moves to whatever **Settings → General → Default Tab** names; a window parked on Projects or Sessions keeps its place, so this is not a reset-to-default on every show. When Default Tab is itself *Terminal*, staying on Term is the same rule, not an exception. Two causes, neither of them a state bug: Default Tab was applied only when a window was created, and the Quick Switcher callback only showed, hid or focused the window without ever naming a tab +- Fix: a switcher window re-created after every window had closed — by the shortcut, or by clicking the Dock icon — briefly showed the Projects tab before jumping to Default Tab, because those paths built the window without the `#mode=` hash the startup path passes and relied on the renderer re-applying the default once it loaded. All three creation paths now pass it, read through one helper that also survives an unreadable settings file instead of leaving the shortcut dead ## 1.0.92 diff --git a/src/main.ts b/src/main.ts index bbbfc55..991bf49 100644 --- a/src/main.ts +++ b/src/main.ts @@ -672,15 +672,31 @@ app.on('window-all-closed', () => { } }); +/** + * Settings → General → Default Tab, read the one way every caller uses. + * Never throws: electron-settings rejects on an unreadable or malformed file, + * and a shortcut that awaits this must still open the window. Anything that is + * not a known tab normalises to 'projects', the renderer's own fallback. + */ +const readDefaultTab = async (): Promise<'projects' | 'sessions' | 'terminal'> => { + try { + const stored = await settings.get('default-switcher-mode'); + if (stored === 'sessions' || stored === 'terminal') return stored; + } catch (err) { + console.error('[default-tab] could not read the setting:', err); + } + return 'projects'; +}; + /** not triggered yet */ -app.on('activate', () => { +app.on('activate', async () => { if (isDebug) { console.log('activate'); } // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { - switcherWindow = createSwitcherWindow(); + switcherWindow = createSwitcherWindow(await readDefaultTab()); } // Normal mode: clicking Dock icon shows hidden window const window = getSwitcherWindow(); @@ -1483,7 +1499,7 @@ const trayToggleEvtHandler = async () => { } } - const defaultSwitcherMode = ((await settings.get('default-switcher-mode')) as string) || 'projects'; + const defaultSwitcherMode = await readDefaultTab(); switcherWindow = createSwitcherWindow(defaultSwitcherMode); if (isDebug) { console.log('when ready'); @@ -1738,8 +1754,10 @@ const trayToggleEvtHandler = async () => { // Term. Term is not what this shortcut is for; it has its own (Ctrl+Cmd+T). // Showing the switcher therefore leaves the Term tab for the default, while // Projects and Sessions keep their place, which is the part worth keeping. - const defaultTab = - ((await settings.get('default-switcher-mode')) as string) || 'projects'; + // Sent BEFORE the window is shown or focused in every branch: main-to-renderer + // messages arrive in send order, so the tab switches before the focus event + // that follows — which then refreshes Sessions once, so the listener does not. + const defaultTab = await readDefaultTab(); const leaveTerminalTab = (w: BrowserWindow | null) => w?.webContents.send('quick-switcher-leave-terminal', defaultTab); @@ -1766,9 +1784,9 @@ const trayToggleEvtHandler = async () => { if (appMode === 'normal') { app.focus({ steal: true }); } + leaveTerminalTab(window); window.show(); window.focus(); - leaveTerminalTab(window); } } else if (window) { if (isDebug) { diff --git a/src/switcher-ui.tsx b/src/switcher-ui.tsx index e7ae328..6d2c0f1 100644 --- a/src/switcher-ui.tsx +++ b/src/switcher-ui.tsx @@ -2088,7 +2088,8 @@ function SwitcherApp() { if (target !== 'projects' && target !== 'sessions') return; modeRef.current = target; setMode(target); - if (target === 'sessions') fetchClaudeSessions(); + // No fetch here: main sends this before showing/focusing the window, and + // the focus handler that follows refreshes Sessions once on its own. }); // Account count decides whether the ⌥⌘+Enter picker hint is shown. From 72dec16be8df19f6271bc716688f310871496c66 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Mon, 5 Oct 2026 17:49:58 +0800 Subject: [PATCH 4/4] fix(switcher): read Default Tab before the window-count check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cubic P2, verified half of it. In app 'activate' the count check ran before 'await readDefaultTab()' and the create after it, so two quick Dock clicks could both see zero windows and each create one — a race this PR introduced. The read now happens first, so check and create run in one tick, the same order quickSwitcherCallback already had. The second report (the same race in quickSwitcherCallback) does not hold: there the await precedes the check, and check and create share one synchronous continuation; new BrowserWindow registers itself synchronously, so a second press sees the first window. Co-Authored-By: Claude Opus 5.5 --- src/main.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 991bf49..c5b9383 100644 --- a/src/main.ts +++ b/src/main.ts @@ -695,8 +695,12 @@ app.on('activate', async () => { } // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. + // Read BEFORE the count check, so the check and the create run in one tick: + // with the await between them, two quick Dock clicks could both see zero + // windows and each create one, orphaning the first. + const defaultTab = await readDefaultTab(); if (BrowserWindow.getAllWindows().length === 0) { - switcherWindow = createSwitcherWindow(await readDefaultTab()); + switcherWindow = createSwitcherWindow(defaultTab); } // Normal mode: clicking Dock icon shows hidden window const window = getSwitcherWindow();