generated from nhcarrigan/template
fix: critical permission modal and config issues (#127)
## Summary This PR resolves several critical bugs that were blocking the permission modal and causing config loss: - **Permission modal not appearing** - Fixed z-index issues and runtime errors - **Config store race condition** - Resolved critical race condition causing settings to be lost - **Excessive logging** - Removed redundant fmt layer that was writing to hidden stdout - **System tool prompts** - Prevented unnecessary permission prompts for built-in tools - **Permission batching** - Added support for parallel permission requests - **ExitPlanMode tool** - Fixed ExitPlanMode tool not functioning correctly ## Changes Made ### Permission Modal Fixes - Updated z-index to proper value (9999) to ensure modal appears above all other UI elements - Fixed runtime errors that were preventing modal from rendering - Resolved issues with permission grants not being properly applied ### Config Store Race Condition - Fixed critical race condition where multiple rapid config updates would result in lost settings - Ensured config writes are properly sequenced to prevent data loss - Added proper synchronisation for config store operations ### Logging Cleanup - Removed redundant fmt formatting layer that was outputting to hidden stdout - Cleaned up excessive debug logging added during troubleshooting - Removed temporary debugging documentation files ### UX Improvements - Added close confirmation modal with minimise to tray option - Implemented batching for parallel permission requests - Added debug console for viewing frontend and backend logs ### ExitPlanMode Fix - Fixed ExitPlanMode tool not functioning correctly, ensuring proper transitions out of plan mode ## Issues Resolved Closes #112 - Permission flow now properly handles multiple tool requests Closes #113 - ExitPlanMode tool now functions correctly Closes #126 - Debug console feature added (partial - basic implementation complete) ## Test Plan - [x] Permission modal appears and functions correctly - [x] Config settings persist across app restarts - [x] No excessive logging in production builds - [x] System tools don't trigger permission prompts - [x] Parallel permission requests are properly batched - [x] Debug console displays frontend and backend logs - [x] ExitPlanMode properly exits plan mode --- ✨ This PR was created with help from Hikari~ 🌸 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Reviewed-on: #127 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #127.
This commit is contained in:
+187
-58
@@ -16,9 +16,24 @@ use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats};
|
||||
use crate::types::{
|
||||
AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent,
|
||||
ConnectionStatus, ContentBlock, MessageCost, OutputEvent, PermissionPromptEvent,
|
||||
QuestionOption, SessionEvent, StateChangeEvent, UserQuestionEvent, WorkingDirectoryEvent,
|
||||
PermissionPromptEventItem, QuestionOption, SessionEvent, StateChangeEvent,
|
||||
UserQuestionEvent, WorkingDirectoryEvent,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
/// Stores pending tool uses from the most recent Assistant message
|
||||
/// to enable batching permission requests for sibling cancelled tools
|
||||
static PENDING_TOOL_USES: RefCell<Vec<PendingToolUse>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PendingToolUse {
|
||||
tool_use_id: String,
|
||||
tool_name: String,
|
||||
tool_input: serde_json::Value,
|
||||
}
|
||||
|
||||
const SEARCH_TOOLS: [&str; 5] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"];
|
||||
const CODING_TOOLS: [&str; 3] = ["Edit", "Write", "NotebookEdit"];
|
||||
@@ -118,21 +133,21 @@ impl WslBridge {
|
||||
let app_clone = app.clone();
|
||||
let stats = self.stats.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
println!("Loading saved achievements...");
|
||||
tracing::info!("Loading saved achievements...");
|
||||
let achievements = crate::achievements::load_achievements(&app_clone).await;
|
||||
println!(
|
||||
tracing::info!(
|
||||
"Loaded {} unlocked achievements",
|
||||
achievements.unlocked.len()
|
||||
);
|
||||
|
||||
println!("Loading saved stats...");
|
||||
tracing::info!("Loading saved stats...");
|
||||
let persisted_stats = crate::stats::load_stats(&app_clone).await;
|
||||
|
||||
let mut stats_guard = stats.write();
|
||||
stats_guard.achievements = achievements;
|
||||
|
||||
if let Some(persisted) = persisted_stats {
|
||||
println!("Applying persisted lifetime stats");
|
||||
tracing::info!("Applying persisted lifetime stats");
|
||||
stats_guard.apply_persisted(persisted);
|
||||
}
|
||||
});
|
||||
@@ -174,8 +189,8 @@ impl WslBridge {
|
||||
|
||||
// Detect if we're running inside WSL or on Windows
|
||||
let is_wsl = detect_wsl();
|
||||
eprintln!("[DEBUG] is_wsl: {}", is_wsl);
|
||||
eprintln!("[DEBUG] options: {:?}", options);
|
||||
tracing::debug!("is_wsl: {}", is_wsl);
|
||||
tracing::debug!("options: {:?}", options);
|
||||
|
||||
let mut command = if is_wsl {
|
||||
// Running inside WSL - call claude directly
|
||||
@@ -184,8 +199,8 @@ impl WslBridge {
|
||||
"Could not find claude binary. Is Claude Code installed?".to_string()
|
||||
})?;
|
||||
|
||||
eprintln!("[DEBUG] Found claude at: {}", claude_path);
|
||||
eprintln!("[DEBUG] Working dir: {}", working_dir);
|
||||
tracing::debug!("Found claude at: {}", claude_path);
|
||||
tracing::debug!("Working dir: {}", working_dir);
|
||||
|
||||
let mut cmd = Command::new(&claude_path);
|
||||
cmd.args([
|
||||
@@ -241,7 +256,7 @@ impl WslBridge {
|
||||
cmd
|
||||
} else {
|
||||
// Running on Windows - use wsl with bash login shell to ensure PATH is loaded
|
||||
eprintln!("[DEBUG] Windows path - using wsl");
|
||||
tracing::debug!("Windows path - using wsl");
|
||||
let mut cmd = Command::new("wsl");
|
||||
|
||||
// Build the claude command with all arguments
|
||||
@@ -307,7 +322,7 @@ impl WslBridge {
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
let mut child = command.spawn().map_err(|e| {
|
||||
eprintln!("[DEBUG] Spawn error: {:?}", e);
|
||||
tracing::error!("Spawn error: {:?}", e);
|
||||
format!("Failed to spawn process: {}", e)
|
||||
})?;
|
||||
|
||||
@@ -485,7 +500,7 @@ impl WslBridge {
|
||||
(input_chars, stats.current_request_output_chars, stats.current_request_thinking_chars, stats.current_request_tools.clone(), model)
|
||||
};
|
||||
|
||||
println!("[COST ESTIMATION] Estimating cost for interrupted request");
|
||||
tracing::info!("[COST ESTIMATION] Estimating cost for interrupted request");
|
||||
|
||||
// Use conservative 3.5 chars/token for estimation (vs standard 4)
|
||||
let estimated_input_tokens = (input_chars as f64 / 3.5).ceil() as u64;
|
||||
@@ -503,7 +518,7 @@ impl WslBridge {
|
||||
let avg_tokens = (tool_stats.estimated_input_tokens + tool_stats.estimated_output_tokens)
|
||||
/ tool_stats.call_count;
|
||||
tool_overhead_tokens += avg_tokens;
|
||||
println!("[COST ESTIMATION] Tool {} average: {} tokens", tool_name, avg_tokens);
|
||||
tracing::info!("[COST ESTIMATION] Tool {} average: {} tokens", tool_name, avg_tokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,9 +532,9 @@ impl WslBridge {
|
||||
let conservative_input = (total_estimated_input as f64 * safety_margin).ceil() as u64;
|
||||
let conservative_output = (total_estimated_output as f64 * safety_margin).ceil() as u64;
|
||||
|
||||
println!("[COST ESTIMATION] Input: {} chars → {} tokens (+ {} tool overhead) × 1.2 safety = {} tokens",
|
||||
tracing::info!("[COST ESTIMATION] Input: {} chars → {} tokens (+ {} tool overhead) × 1.2 safety = {} tokens",
|
||||
input_chars, estimated_input_tokens, tool_overhead_tokens, conservative_input);
|
||||
println!("[COST ESTIMATION] Output: {} chars → {} tokens × 1.2 safety = {} tokens",
|
||||
tracing::info!("[COST ESTIMATION] Output: {} chars → {} tokens × 1.2 safety = {} tokens",
|
||||
output_chars + thinking_chars,
|
||||
estimated_output_tokens, conservative_output);
|
||||
|
||||
@@ -532,7 +547,7 @@ impl WslBridge {
|
||||
None,
|
||||
);
|
||||
|
||||
println!("[COST ESTIMATION] Estimated cost: ${:.4} (conservative)", estimated_cost);
|
||||
tracing::info!("[COST ESTIMATION] Estimated cost: ${:.4} (conservative)", estimated_cost);
|
||||
|
||||
// Add to stats with estimated flag
|
||||
{
|
||||
@@ -572,11 +587,11 @@ impl WslBridge {
|
||||
let stats_snapshot = self.stats.read().clone();
|
||||
let app_clone = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
println!("Saving stats on session stop...");
|
||||
tracing::info!("Saving stats on session stop...");
|
||||
if let Err(e) = crate::stats::save_stats(&app_clone, &stats_snapshot).await {
|
||||
eprintln!("Failed to save stats: {}", e);
|
||||
tracing::error!("Failed to save stats: {}", e);
|
||||
} else {
|
||||
println!("Stats saved successfully on session stop");
|
||||
tracing::info!("Stats saved successfully on session stop");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -621,11 +636,11 @@ fn handle_stdout(
|
||||
match line {
|
||||
Ok(line) if !line.is_empty() => {
|
||||
if let Err(e) = process_json_line(&line, &app, &stats, &conversation_id) {
|
||||
eprintln!("Error processing line: {}", e);
|
||||
tracing::error!("Error processing line: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error reading stdout: {}", e);
|
||||
tracing::error!("Error reading stdout: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
@@ -648,7 +663,7 @@ fn handle_stderr(
|
||||
// Check if this is a SubagentStart hook message
|
||||
if line.contains("[SubagentStart Hook]") {
|
||||
if let Some(agent_data) = parse_subagent_start_hook(&line) {
|
||||
eprintln!("[DEBUG] Parsed SubagentStart hook: agent_id={}, parent={:?}",
|
||||
tracing::debug!("Parsed SubagentStart hook: agent_id={}, parent={:?}",
|
||||
agent_data.agent_id, agent_data.parent_tool_use_id);
|
||||
|
||||
// Emit an agent-update event with the agent_id
|
||||
@@ -770,6 +785,26 @@ fn process_json_line(
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Store pending tool uses for permission batching (only for top-level, not subagents)
|
||||
if parent_tool_use_id.is_none() {
|
||||
PENDING_TOOL_USES.with(|pending| {
|
||||
let tool_uses: Vec<PendingToolUse> = message
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|block| match block {
|
||||
ContentBlock::ToolUse { id, name, input } => Some(PendingToolUse {
|
||||
tool_use_id: id.clone(),
|
||||
tool_name: name.clone(),
|
||||
tool_input: input.clone(),
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
// Append to existing pending tools instead of replacing
|
||||
pending.borrow_mut().extend(tool_uses);
|
||||
});
|
||||
}
|
||||
|
||||
// Track message cost for display
|
||||
let mut message_cost: Option<MessageCost> = None;
|
||||
|
||||
@@ -780,7 +815,7 @@ fn process_json_line(
|
||||
let stats_guard = stats.read();
|
||||
stats_guard.model.clone()
|
||||
}).unwrap_or_else(|| {
|
||||
println!("[WARNING] No model info available for cost calculation, using default");
|
||||
tracing::warn!("No model info available for cost calculation, using default");
|
||||
"claude-sonnet-4-5-20250929".to_string()
|
||||
});
|
||||
|
||||
@@ -793,7 +828,7 @@ fn process_json_line(
|
||||
usage.cache_read_input_tokens,
|
||||
);
|
||||
|
||||
println!("Assistant message tokens - input: {}, output: {}, cache_creation: {:?}, cache_read: {:?}, cost: ${:.4}",
|
||||
tracing::info!("Assistant message tokens - input: {}, output: {}, cache_creation: {:?}, cache_read: {:?}, cost: ${:.4}",
|
||||
usage.input_tokens,
|
||||
usage.output_tokens,
|
||||
usage.cache_creation_input_tokens,
|
||||
@@ -882,8 +917,8 @@ fn process_json_line(
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
|
||||
eprintln!(
|
||||
"[DEBUG] Emitting agent-start: id={}, desc={}, type={}, parent={:?}",
|
||||
tracing::debug!(
|
||||
"Emitting agent-start: id={}, desc={}, type={}, parent={:?}",
|
||||
id, description, subagent_type, parent_tool_use_id
|
||||
);
|
||||
|
||||
@@ -1028,18 +1063,40 @@ fn process_json_line(
|
||||
duration_ms,
|
||||
num_turns,
|
||||
} => {
|
||||
tracing::info!(
|
||||
"Received Result message: subtype={}, has_denials={}, denial_count={:?}",
|
||||
subtype,
|
||||
permission_denials.is_some(),
|
||||
permission_denials.as_ref().map(|d| d.len())
|
||||
);
|
||||
|
||||
let state = if subtype == "success" {
|
||||
CharacterState::Success
|
||||
} else {
|
||||
CharacterState::Error
|
||||
};
|
||||
|
||||
// Capture pending tool uses before clearing them
|
||||
// We'll use these for permission batching if there are denials
|
||||
let captured_pending_tools = PENDING_TOOL_USES.with(|pending| {
|
||||
let tools = pending.borrow().clone();
|
||||
// Clear immediately so they don't accumulate across requests
|
||||
pending.borrow_mut().clear();
|
||||
tools
|
||||
});
|
||||
|
||||
tracing::debug!(
|
||||
"Captured {} pending tool use(s): {:?}",
|
||||
captured_pending_tools.len(),
|
||||
captured_pending_tools.iter().map(|t| &t.tool_name).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
// Log turn metrics if available
|
||||
if let Some(duration) = duration_ms {
|
||||
println!("Turn completed in {}ms", duration);
|
||||
tracing::info!("Turn completed in {}ms", duration);
|
||||
}
|
||||
if let Some(turns) = num_turns {
|
||||
println!("Turn count: {}", turns);
|
||||
tracing::info!("Turn count: {}", turns);
|
||||
}
|
||||
|
||||
// Track token usage from Result messages if available
|
||||
@@ -1069,7 +1126,7 @@ fn process_json_line(
|
||||
usage_info.cache_creation_input_tokens,
|
||||
usage_info.cache_read_input_tokens,
|
||||
);
|
||||
println!("Result message tokens - input: {}, output: {}, cache_creation: {:?}, cache_read: {:?}",
|
||||
tracing::info!("Result message tokens - input: {}, output: {}, cache_creation: {:?}, cache_read: {:?}",
|
||||
usage_info.input_tokens,
|
||||
usage_info.output_tokens,
|
||||
usage_info.cache_creation_input_tokens,
|
||||
@@ -1099,9 +1156,9 @@ fn process_json_line(
|
||||
let newly_unlocked = {
|
||||
let mut stats_guard = stats.write();
|
||||
stats_guard.get_session_duration();
|
||||
println!("Checking achievements after result message...");
|
||||
tracing::info!("Checking achievements after result message...");
|
||||
let unlocked = stats_guard.check_achievements();
|
||||
println!("Newly unlocked achievements: {:?}", unlocked);
|
||||
tracing::info!("Newly unlocked achievements: {:?}", unlocked);
|
||||
unlocked
|
||||
};
|
||||
|
||||
@@ -1116,20 +1173,20 @@ fn process_json_line(
|
||||
|
||||
// Save achievements after unlocking new ones
|
||||
if !newly_unlocked.is_empty() {
|
||||
println!("Saving newly unlocked achievements: {:?}", newly_unlocked);
|
||||
tracing::info!("Saving newly unlocked achievements: {:?}", newly_unlocked);
|
||||
let app_handle = app.clone();
|
||||
let achievements_progress = stats.read().achievements.clone();
|
||||
|
||||
// Use Tauri's async runtime instead of tokio::spawn
|
||||
tauri::async_runtime::spawn(async move {
|
||||
println!("Spawned save task for achievements");
|
||||
tracing::info!("Spawned save task for achievements");
|
||||
if let Err(e) =
|
||||
crate::achievements::save_achievements(&app_handle, &achievements_progress)
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to save achievements: {}", e);
|
||||
tracing::error!("Failed to save achievements: {}", e);
|
||||
} else {
|
||||
println!("Achievement save task completed successfully");
|
||||
tracing::info!("Achievement save task completed successfully");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1146,9 +1203,9 @@ fn process_json_line(
|
||||
{
|
||||
let app_handle = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
println!("Periodic stats save (every 10 messages)...");
|
||||
tracing::info!("Periodic stats save (every 10 messages)...");
|
||||
if let Err(e) = crate::stats::save_stats(&app_handle, ¤t_stats).await {
|
||||
eprintln!("Failed to save stats: {}", e);
|
||||
tracing::error!("Failed to save stats: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1172,9 +1229,36 @@ fn process_json_line(
|
||||
|
||||
// Check for permission denials and emit prompts for each
|
||||
if let Some(denials) = permission_denials {
|
||||
let mut has_regular_denials = false;
|
||||
// Only process if there are actually denials
|
||||
if !denials.is_empty() {
|
||||
let mut regular_permission_requests = Vec::new();
|
||||
|
||||
// Get denied tool IDs for later comparison
|
||||
let denied_tool_ids: Vec<String> = denials.iter()
|
||||
.map(|d| d.tool_use_id.clone())
|
||||
.collect();
|
||||
|
||||
// Helper function to check if a tool is a system tool that should never require permission
|
||||
let is_system_tool = |tool_name: &str| -> bool {
|
||||
matches!(tool_name, "ExitPlanMode" | "EnterPlanMode")
|
||||
};
|
||||
|
||||
for denial in denials {
|
||||
// Skip system tools that should never require permission
|
||||
if is_system_tool(&denial.tool_name) {
|
||||
tracing::debug!(
|
||||
"Skipping system tool: {} (id: {})",
|
||||
denial.tool_name,
|
||||
denial.tool_use_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
tracing::debug!(
|
||||
"Processing permission denial for: {} (id: {})",
|
||||
denial.tool_name,
|
||||
denial.tool_use_id
|
||||
);
|
||||
|
||||
for denial in denials {
|
||||
// Special handling for AskUserQuestion tool
|
||||
if denial.tool_name == "AskUserQuestion" {
|
||||
if let Some(questions) = denial
|
||||
@@ -1235,24 +1319,57 @@ fn process_json_line(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
has_regular_denials = true;
|
||||
let description =
|
||||
format_tool_description(&denial.tool_name, &denial.tool_input);
|
||||
let _ = app.emit(
|
||||
"claude:permission",
|
||||
PermissionPromptEvent {
|
||||
id: denial.tool_use_id.clone(),
|
||||
tool_name: denial.tool_name.clone(),
|
||||
tool_input: denial.tool_input.clone(),
|
||||
description,
|
||||
conversation_id: conversation_id.clone(),
|
||||
},
|
||||
);
|
||||
regular_permission_requests.push(PermissionPromptEventItem {
|
||||
id: denial.tool_use_id.clone(),
|
||||
tool_name: denial.tool_name.clone(),
|
||||
tool_input: denial.tool_input.clone(),
|
||||
description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Show permission state if there were any denials (questions or regular)
|
||||
if has_regular_denials || !denials.is_empty() {
|
||||
// Check for sibling tools that may have been cancelled
|
||||
// Add them to the permission batch so they can be approved together
|
||||
for tool_use in captured_pending_tools.iter() {
|
||||
// Skip system tools that should never require permission
|
||||
if is_system_tool(&tool_use.tool_name) {
|
||||
continue;
|
||||
}
|
||||
// Only add tools that weren't explicitly denied (these are likely cancelled siblings)
|
||||
if !denied_tool_ids.contains(&tool_use.tool_use_id) {
|
||||
let description = format_tool_description(&tool_use.tool_name, &tool_use.tool_input);
|
||||
regular_permission_requests.push(PermissionPromptEventItem {
|
||||
id: tool_use.tool_use_id.clone(),
|
||||
tool_name: tool_use.tool_name.clone(),
|
||||
tool_input: tool_use.tool_input.clone(),
|
||||
description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Emit all regular permission requests as a single batched event
|
||||
if !regular_permission_requests.is_empty() {
|
||||
tracing::info!(
|
||||
"Emitting permission event for {} tool(s) in conversation {:?}",
|
||||
regular_permission_requests.len(),
|
||||
conversation_id
|
||||
);
|
||||
for req in ®ular_permission_requests {
|
||||
tracing::debug!(
|
||||
"Permission requested: {} (id: {})",
|
||||
req.tool_name,
|
||||
req.id
|
||||
);
|
||||
}
|
||||
let _ = app.emit(
|
||||
"claude:permission",
|
||||
PermissionPromptEvent {
|
||||
permissions: regular_permission_requests,
|
||||
conversation_id: conversation_id.clone(),
|
||||
},
|
||||
);
|
||||
emit_state_change(
|
||||
app,
|
||||
CharacterState::Permission,
|
||||
@@ -1261,7 +1378,19 @@ fn process_json_line(
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Show permission state if there were any question denials
|
||||
if !denials.is_empty() {
|
||||
emit_state_change(
|
||||
app,
|
||||
CharacterState::Permission,
|
||||
None,
|
||||
conversation_id.clone(),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
} // end of else block for non-empty denials
|
||||
} // end of if let Some(denials)
|
||||
|
||||
emit_state_change(app, state, None, conversation_id.clone());
|
||||
}
|
||||
@@ -1311,7 +1440,7 @@ fn process_json_line(
|
||||
// Check achievements after user message
|
||||
let newly_unlocked = {
|
||||
let mut stats_guard = stats.write();
|
||||
println!("User sent message, checking achievements...");
|
||||
tracing::info!("User sent message, checking achievements...");
|
||||
|
||||
// Check message-based achievements
|
||||
let mut unlocked = crate::achievements::check_message_achievements(
|
||||
@@ -1328,7 +1457,7 @@ fn process_json_line(
|
||||
|
||||
// Emit achievement events for any newly unlocked achievements
|
||||
for achievement_id in &newly_unlocked {
|
||||
println!("User message unlocked achievement: {:?}", achievement_id);
|
||||
tracing::info!("User message unlocked achievement: {:?}", achievement_id);
|
||||
let info = get_achievement_info(achievement_id);
|
||||
let _ = app.emit(
|
||||
"achievement:unlocked",
|
||||
@@ -1338,7 +1467,7 @@ fn process_json_line(
|
||||
|
||||
// Save achievements after unlocking new ones
|
||||
if !newly_unlocked.is_empty() {
|
||||
println!("Saving newly unlocked achievements from user message");
|
||||
tracing::info!("Saving newly unlocked achievements from user message");
|
||||
let app_handle = app.clone();
|
||||
let achievements_progress = stats.read().achievements.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -1346,9 +1475,9 @@ fn process_json_line(
|
||||
crate::achievements::save_achievements(&app_handle, &achievements_progress)
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to save achievements: {}", e);
|
||||
tracing::error!("Failed to save achievements: {}", e);
|
||||
} else {
|
||||
println!("Achievements saved after user message");
|
||||
tracing::info!("Achievements saved after user message");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user