From bacc1e5af2bb13344899bc81d203a41371801819 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 27 Jun 2026 16:30:06 +0530 Subject: [PATCH 1/2] fix: prevent API key erasure on settings save and fix graph view unmount crash --- AUDIT_LOG.md | 12 ++++++ CHANGELOG.md | 3 +- src-tauri/src/commands/keychain.rs | 8 ++-- src/Settings.tsx | 65 ++++++++++++++++++++++++------ 4 files changed, 72 insertions(+), 16 deletions(-) diff --git a/AUDIT_LOG.md b/AUDIT_LOG.md index eda349e..f72ee8b 100644 --- a/AUDIT_LOG.md +++ b/AUDIT_LOG.md @@ -2,6 +2,18 @@ This log tracks all significant changes, updates, and versions in the PaperCache project. +## 2026-06-27 (API Key Persistence & Graph View Fixes) +**Change:** fix(ai): fix API key saving/clearing logic and macOS keychain credential updating; fix(graph): prevent `fg.graphData` crashes when opening or closing Graph View + +**Details/Why:** +1. **API Key Persistence**: When opening Settings, `apiKey` state initialized to empty string `''`. Clicking "Save Settings" after changing other preferences unintentionally took the `else` branch (`await window.electronAPI.setApiKey('')`), erasing existing keys from the OS keyring. Updated `saveSettings` to only save when `apiKey.trim()` is non-empty, and only clear when `!isApiKeySet`. Added an explicit "Clear Key" UI button next to the password input field when an API key is set. +2. **Keyring Credential Updating**: In `src-tauri/src/commands/keychain.rs`, calling `set_password` on an existing keychain entry could fail on macOS. Updated `set_api_key` to delete any existing credential before setting the new password. +3. **Graph View Crash Fixes**: Merged comprehensive defensive checks (`typeof fg.method === 'function'`) and ref caching into `GraphView.tsx` to prevent `fg.graphData is not a function` crashes when unmounting or toggling Graph View via `Cmd+G`. + +**Files changed:** `src/Settings.tsx`, `src-tauri/src/commands/keychain.rs`, `src/GraphView.tsx`, `CHANGELOG.md`, `AUDIT_LOG.md`. + +--- + ## 2026-06-27 (Graph View Bugfix) **Change:** fix(graph): prevent `e.graphData is not a function` crash on unmount and replace setInterval diff --git a/CHANGELOG.md b/CHANGELOG.md index 1862d50..bb783f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Settings Bug Report & About Menu**: Added a "Submit a Bug Report" button under System settings linking directly to the GitHub issue creation form. Added a dedicated "About" section displaying the app logo, current version number, update checker, Ko-fi support link, and a thank you message. ### Fixed -- **Graph View crash on navigation**: Fixed `e.graphData is not a function` TypeError when navigating to a note from the Graph View by adding defensive checks before invoking ref methods on component unmount and falling back to a ref cache. Also replaced `setInterval` with a chained `setTimeout` loop. +- **API Key Persistence & Clearing**: Fixed an issue where clicking "Save Settings" without re-entering an API key unintentionally cleared existing keys from the OS keyring. Added an explicit "Clear Key" button and defensive trimming before saving credentials securely. Also improved macOS keyring replacement logic to prevent duplicate item errors. +- **Graph View crash on navigation & toggle**: Fixed `fg.graphData is not a function` TypeError when navigating to notes or toggling Graph View (`Cmd+G`) by adding defensive checks before invoking ref methods on component unmount and falling back to a ref cache. Also replaced `setInterval` with a chained `setTimeout` loop. - **Windows Onboarding File Linking & Generation**: Fixed a bug on Windows where backslashes in generated note IDs caused internal `/file` links in `Welcome.md` to fail and create duplicate empty notes. Normalized note ID generation across Rust and TypeScript to consistently use forward slashes on all platforms, and ensured onboarding template files regenerate correctly on application updates. - **Window position/size now persists across restarts**: The window-state plugin's `on_window_ready` fires before the macOS display server is ready, causing `available_monitors()` to return empty and the saved position to be silently discarded. Fixed by deferring window-state restoration via a background thread + `run_on_main_thread` 300ms after `setup()` completes, bypassing the plugin's monitor-intersection check with a direct file read. Both the tray "Quit" and Settings "Quit" buttons now explicitly save window state before exit. - **Launch at Startup now registers as a proper Login Item**: Changed `MacosLauncher` from `LaunchAgent` to `AppleScript`, which registers PaperCache in System Settings > General > Login Items instead of creating a hidden `launchd` plist. Users can now see and manage the autostart entry directly from System Settings. diff --git a/src-tauri/src/commands/keychain.rs b/src-tauri/src/commands/keychain.rs index e414d0d..3240dd0 100644 --- a/src-tauri/src/commands/keychain.rs +++ b/src-tauri/src/commands/keychain.rs @@ -12,12 +12,14 @@ const SERVICE_NAME: &str = "com.variablethe.papercache"; pub fn set_api_key(key: String) -> Result { let entry = Entry::new(SERVICE_NAME, "openai_api_key") .map_err(|e| format!("Failed to access keyring: {}", e))?; - if key.is_empty() { - entry.delete_credential().ok(); + let trimmed = key.trim(); + if trimmed.is_empty() { + let _ = entry.delete_credential(); return Ok(true); } + let _ = entry.delete_credential(); entry - .set_password(&key) + .set_password(trimmed) .map_err(|e| format!("Failed to set API key: {}", e))?; Ok(true) } diff --git a/src/Settings.tsx b/src/Settings.tsx index 1bb389d..40a18ec 100644 --- a/src/Settings.tsx +++ b/src/Settings.tsx @@ -88,13 +88,26 @@ export default function Settings({ onClose }: { onClose?: () => void }) { localStorage.setItem(SETTINGS_KEYS.API_MODEL, apiModel) localStorage.setItem(SETTINGS_KEYS.AI_SYSTEM_PROMPT, aiSystemPrompt) - if (apiKey) { - const success = await window.electronAPI.setApiKey(apiKey) - if (!success) { - alert('Failed to save API key securely. Check console.') + if (apiKey.trim()) { + try { + const success = await window.electronAPI.setApiKey(apiKey.trim()) + if (success) { + setIsApiKeySet(true) + } else { + alert('Failed to save API key securely. Check console.') + } + } catch (err) { + // eslint-disable-next-line no-console + console.error('Failed to save API key:', err) + alert(`Failed to save API key securely: ${err}`) + } + } else if (!isApiKeySet) { + try { + await window.electronAPI.setApiKey('') // clear key + } catch (err) { + // eslint-disable-next-line no-console + console.error('Failed to clear API key:', err) } - } else { - await window.electronAPI.setApiKey('') // clear key } useSettingsStore.getState().setSettings({ @@ -167,12 +180,40 @@ export default function Settings({ onClose }: { onClose?: () => void }) {

AI Configuration

- setApiKey(e.target.value)} - placeholder={isApiKeySet ? 'Enter new key to replace existing' : 'sk-...'} - /> +
+ setApiKey(e.target.value)} + placeholder={isApiKeySet ? 'Enter new key to replace existing' : 'sk-...'} + style={{ flex: 1 }} + /> + {isApiKeySet && ( + + )} +
From 3c36dc3341296551c99dd26a3277b654e199dbd5 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:10:09 +0000 Subject: [PATCH 2/2] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit --- src-tauri/src/commands/keychain.rs | 13 ++++++++++--- src/Settings.tsx | 7 ------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/commands/keychain.rs b/src-tauri/src/commands/keychain.rs index 3240dd0..2685b87 100644 --- a/src-tauri/src/commands/keychain.rs +++ b/src-tauri/src/commands/keychain.rs @@ -14,10 +14,17 @@ pub fn set_api_key(key: String) -> Result { .map_err(|e| format!("Failed to access keyring: {}", e))?; let trimmed = key.trim(); if trimmed.is_empty() { - let _ = entry.delete_credential(); - return Ok(true); + match entry.delete_credential() { + Ok(_) => return Ok(true), + Err(keyring::Error::NoEntry) => return Ok(true), + Err(e) => return Err(format!("Failed to delete API key: {}", e)), + } + } + match entry.delete_credential() { + Ok(_) => {}, + Err(keyring::Error::NoEntry) => {}, + Err(e) => return Err(format!("Failed to delete existing API key: {}", e)), } - let _ = entry.delete_credential(); entry .set_password(trimmed) .map_err(|e| format!("Failed to set API key: {}", e))?; diff --git a/src/Settings.tsx b/src/Settings.tsx index 40a18ec..d0cb2b5 100644 --- a/src/Settings.tsx +++ b/src/Settings.tsx @@ -101,13 +101,6 @@ export default function Settings({ onClose }: { onClose?: () => void }) { console.error('Failed to save API key:', err) alert(`Failed to save API key securely: ${err}`) } - } else if (!isApiKeySet) { - try { - await window.electronAPI.setApiKey('') // clear key - } catch (err) { - // eslint-disable-next-line no-console - console.error('Failed to clear API key:', err) - } } useSettingsStore.getState().setSettings({