Fix Story 12: Claude API key storage now working

- Fixed silent API key save failure by switching from keyring to Tauri store
- Removed keyring dependency (didn't work in macOS dev mode for unsigned apps)
- Implemented reliable cross-platform storage using tauri-plugin-store
- Added pendingMessageRef to preserve user message during API key dialog flow
- Refactored sendMessage to accept optional message parameter for retry
- Removed all debug logging and test code
- Removed unused entitlements.plist and macOS config
- API key now persists correctly between sessions
- Auto-retry after saving key works properly

Story 12 complete and archived.
This commit is contained in:
Dave
2025-12-27 20:08:24 +00:00
parent 2976c854d0
commit c2da7f9f18
5 changed files with 981 additions and 898 deletions

View File

@@ -7,6 +7,7 @@ use crate::state::SessionState;
use serde::Deserialize;
use serde_json::json;
use tauri::{AppHandle, Emitter, State};
use tauri_plugin_store::StoreExt;
#[derive(Deserialize)]
pub struct ProviderConfig {
@@ -25,32 +26,73 @@ pub async fn get_ollama_models(base_url: Option<String>) -> Result<Vec<String>,
}
#[tauri::command]
pub async fn get_anthropic_api_key_exists() -> Result<bool, String> {
match keyring::Entry::new("living-spec-anthropic-api-key", "default") {
Ok(entry) => Ok(entry.get_password().is_ok()),
Err(e) => Err(format!("Failed to access keychain: {}", e)),
pub async fn get_anthropic_api_key_exists(app: AppHandle) -> Result<bool, String> {
let store = app
.store("store.json")
.map_err(|e| format!("Failed to access store: {}", e))?;
match store.get("anthropic_api_key") {
Some(value) => {
if let Some(key) = value.as_str() {
Ok(!key.is_empty())
} else {
Ok(false)
}
}
None => Ok(false),
}
}
#[tauri::command]
pub async fn set_anthropic_api_key(api_key: String) -> Result<(), String> {
let entry = keyring::Entry::new("living-spec-anthropic-api-key", "default")
.map_err(|e| format!("Failed to create keychain entry: {}", e))?;
pub async fn set_anthropic_api_key(app: AppHandle, api_key: String) -> Result<(), String> {
let store = app
.store("store.json")
.map_err(|e| format!("Failed to access store: {}", e))?;
entry
.set_password(&api_key)
.map_err(|e| format!("Failed to store API key: {}", e))?;
store.set("anthropic_api_key", json!(api_key));
store
.save()
.map_err(|e| format!("Failed to save store: {}", e))?;
// Verify it was saved
match store.get("anthropic_api_key") {
Some(value) => {
if let Some(retrieved) = value.as_str() {
if retrieved != api_key {
return Err("Retrieved key does not match saved key".to_string());
}
} else {
return Err("Stored value is not a string".to_string());
}
}
None => {
return Err("API key was saved but cannot be retrieved".to_string());
}
}
Ok(())
}
fn get_anthropic_api_key() -> Result<String, String> {
let entry = keyring::Entry::new("living-spec-anthropic-api-key", "default")
.map_err(|e| format!("Failed to access keychain: {}", e))?;
fn get_anthropic_api_key(app: &AppHandle) -> Result<String, String> {
let store = app
.store("store.json")
.map_err(|e| format!("Failed to access store: {}", e))?;
entry
.get_password()
.map_err(|_| "Anthropic API key not found. Please set your API key.".to_string())
match store.get("anthropic_api_key") {
Some(value) => {
if let Some(key) = value.as_str() {
if key.is_empty() {
Err("Anthropic API key is empty. Please set your API key.".to_string())
} else {
Ok(key.to_string())
}
} else {
Err("Stored API key is not a string".to_string())
}
}
None => Err("Anthropic API key not found. Please set your API key.".to_string()),
}
}
#[tauri::command]
@@ -133,7 +175,7 @@ pub async fn chat(
// Call LLM with streaming
let response = if is_claude {
// Use Anthropic provider
let api_key = get_anthropic_api_key()?;
let api_key = get_anthropic_api_key(&app)?;
let anthropic_provider = AnthropicProvider::new(api_key);
anthropic_provider
.chat_stream(&app, &config.model, &current_history, tools, &mut cancel_rx)