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:
11
src-tauri/Cargo.lock
generated
11
src-tauri/Cargo.lock
generated
@@ -1999,16 +1999,6 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "3.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
|
||||
dependencies = [
|
||||
"log",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuchikiki"
|
||||
version = "0.8.8-speedreader"
|
||||
@@ -2098,7 +2088,6 @@ dependencies = [
|
||||
"eventsource-stream",
|
||||
"futures",
|
||||
"ignore",
|
||||
"keyring",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -32,5 +32,4 @@ chrono = { version = "0.4.42", features = ["serde"] }
|
||||
async-trait = "0.1.89"
|
||||
tauri-plugin-store = "2.4.1"
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
keyring = "3.2"
|
||||
eventsource-stream = "0.2.3"
|
||||
|
||||
@@ -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, ¤t_history, tools, &mut cancel_rx)
|
||||
|
||||
Reference in New Issue
Block a user