diff --git a/DEBUGGING.md b/DEBUGGING.md deleted file mode 100644 index a214ed3..0000000 --- a/DEBUGGING.md +++ /dev/null @@ -1,600 +0,0 @@ -# Debugging Guide - Common Issues and Fixes - -> **IMPORTANT:** Read this ENTIRE file before asking for help! These are the most common issues and their fixes. - -## Table of Contents -1. [Permission Modal Not Showing](#permission-modal-not-showing-up-) -2. [Config Loss Issues](#config-loss-issues-) -3. [Modal/Overlay Not Showing (General)](#modaloverlay-not-showing-general-) -4. [Event Listener Issues](#event-listener-issues-) -5. [Build Issues](#build-issues-) -6. [Testing Checklist](#testing-checklist-) -7. [Emergency Recovery](#emergency-recovery-) - ---- - -## Permission Modal Not Showing Up 🔐 - -**SYMPTOMS:** -- Permission requests are triggered but the modal doesn't appear -- App seems to hang or become unresponsive -- Console shows permission events but nothing happens -- Character stays in "permission" state but no UI - -**ROOT CAUSE (FIXED - Commit d16644a):** - -The issue was **undefined variable errors** in `src/lib/tauri.ts` that crashed the event listener initialization. - -### The Bug That Was Fixed - -In `src/lib/tauri.ts`, there were calls to `debugConsoleStore.log()`: -```typescript -debugConsoleStore.log("frontend", "info", "Setting up claude:permission listener"); -``` - -**Two problems:** -1. `debugConsoleStore` was never imported (undefined variable → crash!) -2. Even if imported, `debugConsoleStore` doesn't have a `.log()` method - - Available methods: `toggle`, `open`, `close`, `clear`, `setupConsoleCapture`, etc. - - NOT available: `log()` - -### The Fix - -```typescript -// ❌ WRONG - This will crash the entire listener setup! -debugConsoleStore.log("frontend", "info", "message"); - -// ✅ CORRECT - Use console.log which gets automatically captured -console.log("[Tauri Listener] message"); -``` - -**Why console.log works:** -- The `debugConsoleStore` automatically intercepts `console.log()`, `console.error()`, etc. -- No need to call the store directly! -- See `src/lib/stores/debugConsole.ts` lines 66-89 for the console interception code - -### How to Debug This Type of Issue - -**Step 1: Check Browser Console** -1. Open Browser DevTools in the app (View > Toggle Developer Tools or F12) -2. Look for JavaScript errors: - - `ReferenceError: X is not defined` → Missing import or typo - - `TypeError: X.method is not a function` → Wrong method name or undefined object - - Any error in `tauri.ts` during initialization is critical! - -**Step 2: Check Event Listener Registration** -Look for these console logs when the app starts: -``` -[Tauri Listener] Setting up claude:permission listener -``` - -If you DON'T see this log, the listener setup crashed before it got there! - -**Step 3: Check TypeScript Errors** -```bash -pnpm check -``` -This will catch: -- Missing imports -- Undefined variables -- Type mismatches -- Non-existent methods - -**Step 4: Test Permission Flow** -1. Trigger a restricted tool (like Bash with a destructive command) -2. Check console for `[Permission] Event received:` log -3. Check if `PermissionModal.svelte` is rendering (DevTools Elements tab) -4. Check if modal has proper z-index and visibility - -### Prevention - -**Before committing changes to `tauri.ts` or any store:** -1. ✅ Run `pnpm check` - Catches TypeScript errors -2. ✅ Run `pnpm build` - Catches runtime issues -3. ✅ Test the app - Especially permission prompts -4. ✅ Check browser console - No errors on startup -5. ✅ Verify all imports - Don't use variables without importing them! - -**Common mistakes to avoid:** -- Calling methods that don't exist on a store -- Forgetting to import something you're using -- Copy-pasting code without checking if variables are defined -- Assuming a store has certain methods without checking the source - -## Config Loss Issues 🔧 - -**SYMPTOMS:** -- Config file gets reset to defaults -- Settings don't persist between sessions -- Model selection is lost -- Config appears to work, then randomly resets - -**ACTUAL ROOT CAUSE (FIXED - Commit 2c64ef0):** - -The config store had a **CRITICAL race condition bug** in `src/lib/stores/config.ts` that affected 12 different methods! - -### The Bug That Was Fixed - -Every method that accessed the current config used this pattern: - -```typescript -// ❌ BUGGY CODE - DO NOT USE! -let currentConfig = defaultConfig; -config.subscribe((c) => (currentConfig = c))(); // <-- ☠️ IMMEDIATE UNSUBSCRIBE! -return currentConfig; -``` - -**Why this caused config loss:** -- The `()` at the end **immediately unsubscribes** from the store -- This creates a **race condition**: - - Sometimes: The subscription fires, sets `currentConfig`, then unsubscribes → ✅ Works - - Sometimes: The subscription unsubscribes before it can fire → ❌ Returns `defaultConfig` -- When the race fails, it returns `defaultConfig` instead of your actual saved config -- This is why your settings would **randomly disappear**! -- It was especially bad after reconnections (like permission approvals) because timing was tight - -**Affected methods (all fixed!):** -- `getConfig()` - Core method, affected everything -- `updateConfig()` - Would sometimes update defaults instead of current config -- `toggleStreamerMode()` / `toggleCompactMode()` - Random toggles -- `increaseFontSize()` / `decreaseFontSize()` - Font size would reset -- `addAutoGrantedTool()` / `removeAutoGrantedTool()` - Tool permissions lost -- `setTheme()` / `setCustomThemeColors()` - Theme changes lost - -### The Fix - -```typescript -// ✅ CORRECT CODE -function getCurrentConfig(): HikariConfig { - let currentConfig: HikariConfig = defaultConfig; - const unsubscribe = config.subscribe((c) => (currentConfig = c)); - unsubscribe(); // <-- Proper cleanup AFTER getting value - return currentConfig; -} - -// Now all methods use this: -getConfig: (): HikariConfig => { - return getCurrentConfig(); -}, - -updateConfig: async (updates: Partial) => { - const currentConfig = getCurrentConfig(); // <-- Safe! - const newConfig = { ...currentConfig, ...updates }; - await saveConfig(newConfig); -} -``` - -**Why the fix works:** -1. Store the unsubscribe function in a variable -2. Let the subscription fire synchronously (Svelte stores are synchronous) -3. The callback runs immediately, setting `currentConfig` -4. Clean up by calling `unsubscribe()` -5. Return the now-populated `currentConfig` - -### How to Debug Config Loss - -**Step 1: Check if config file exists** -```bash -cat ~/.local/share/com.naomi.hikari-desktop/hikari-config.json -``` - -If the file exists and has your settings, the problem is in the frontend store, not the backend! - -**Step 2: Check console for config loading** -```javascript -console.log("Loaded config:", config); -``` - -Add this in `src/lib/stores/config.ts` line 107 (after `config.set(savedConfig)`) - -**Step 3: Test getConfig() reliability** -Open browser console and run: -```javascript -// Run this 10 times -for (let i = 0; i < 10; i++) { - console.log("Attempt", i, configStore.getConfig().model); -} -``` - -If it's inconsistent (sometimes null, sometimes has value), you have a race condition! - -**Step 4: Check for the buggy pattern** -Search the codebase for: -```bash -grep -r "subscribe((.*) => (.*))();" src/ -``` - -If you find any, they're potential bugs! The `()` at the end is suspicious. - -### Prevention - -**When working with Svelte stores:** - -❌ **DON'T DO THIS:** -```typescript -let value; -store.subscribe((v) => (value = v))(); // Immediate unsubscribe = race! -``` - -✅ **DO THIS INSTEAD:** -```typescript -let value; -const unsubscribe = store.subscribe((v) => (value = v)); -unsubscribe(); // Explicit cleanup -return value; -``` - -Or even better, use Svelte's reactive syntax: -```svelte - -``` - -**NEVER:** -- Subscribe and immediately unsubscribe in the same line -- Assume synchronous subscriptions will always fire before unsubscribe -- Use `()()` pattern with stores (function call + immediate execution) - -**POTENTIAL CAUSES (if the above is fixed):** - -### 1. Missing `#[serde(default)]` Attribute - -The `HikariConfig` struct in `src-tauri/src/config.rs` needs `#[serde(default)]` at the struct level: - -```rust -// ✅ CORRECT -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default)] // <-- This is crucial! -pub struct HikariConfig { - // fields... -} -``` - -**Why this matters:** -- When new fields are added to the config struct, old config files won't have them -- Without `#[serde(default)]`, deserialization fails and the entire config resets -- With it, missing fields use their default values gracefully - -### 2. Invalid JSON in Config File - -**Check the config file:** -```bash -cat ~/.config/hikari-desktop/config.json -``` - -**Common issues:** -- Trailing commas -- Missing quotes -- Incorrect enum values (e.g., `"model": "opus"` instead of `"model": "claude-opus-4-6"`) - -**Fix:** -- Delete or manually fix `~/.config/hikari-desktop/config.json` -- Restart the app to regenerate - -### 3. Permission Issues - -**Check file permissions:** -```bash -ls -la ~/.config/hikari-desktop/config.json -``` - -**Should be writable by user:** -```bash -chmod 644 ~/.config/hikari-desktop/config.json -``` - -## Modal/Overlay Not Showing (General) 🎭 - -If ANY modal isn't showing up (permission, questions, close confirmation, etc.): - -### 1. Check TypeScript Errors First - -```bash -pnpm check -``` - -Look for: -- Import errors -- Undefined variables -- Type mismatches - -### 2. Check Console for JavaScript Errors - -Open DevTools and look for: -- `ReferenceError` - Usually undefined variables -- `TypeError` - Usually calling methods on undefined/null -- Event listener setup logs - -### 3. Check Svelte Store Subscriptions - -Modals rely on store subscriptions. In the modal component: - -```typescript -// ✅ Make sure the store is imported -import { conversationsStore } from "$lib/stores/conversations"; - -// ✅ Make sure the subscription is set up -conversationsStore.pendingPermissions.subscribe((perms) => { - permissions = perms; - // ... -}); -``` - -### 4. Verify z-index (Last Resort) - -Only check z-index if: -- ✅ No console errors -- ✅ Modal HTML is in the DOM (check with DevTools Elements tab) -- ✅ Modal is rendering but invisible - -Current z-index values in `PermissionModal.svelte`: -```css -.permission-overlay { - z-index: 60; /* Should be higher than character panel */ -} -``` - -## Event Listener Issues 🎧 - -**SYMPTOMS:** -- Events not firing -- Listeners not receiving data -- Silent failures - -### Debugging Event Listeners - -Add console logs in `src/lib/tauri.ts`: - -```typescript -console.log("[Tauri Listener] Setting up claude:permission listener"); -const permissionUnlisten = await listen("claude:permission", (event) => { - console.log("[Permission] Event received:", event.payload); - // ... rest of handler -}); -``` - -### Common Issues - -1. **Event listener crashes silently:** - - Check for undefined variables in the listener callback - - Check for missing imports - - Use `try/catch` around critical sections - -2. **Events not being emitted from Rust:** - - Check `src-tauri/src/` for `emit()` calls - - Verify event names match exactly (case-sensitive!) - - Check Rust console output for errors - -3. **Race condition:** - - Event fired before listener was registered - - Solution: Set up listeners in `initializeTauriListeners()` before starting Claude - -## Build Issues 🏗️ - -### TypeScript Errors - -```bash -pnpm check -``` - -**Common fixes:** -- Missing imports -- Wrong types -- Calling non-existent methods - -### Rust Errors - -```bash -cargo build --release -``` - -**Common fixes:** -- Missing dependencies in `Cargo.toml` -- Import path issues -- Type mismatches - -### Full Clean Build - -```bash -# Clean everything -rm -rf node_modules dist .svelte-kit src-tauri/target - -# Reinstall -pnpm install - -# Build Rust -cd src-tauri && cargo build --release && cd .. - -# Build frontend -pnpm build -``` - -## Quick Reference: Key Files 📚 - -### Frontend -- **Event listeners:** `src/lib/tauri.ts` -- **Permission modal:** `src/lib/components/PermissionModal.svelte` -- **Config store:** `src/lib/stores/config.ts` -- **Debug console:** `src/lib/stores/debugConsole.ts` - -### Backend -- **Config struct:** `src-tauri/src/config.rs` -- **Event emission:** `src-tauri/src/wsl_bridge.rs` -- **Type definitions:** `src-tauri/src/types.rs` - -## Testing Checklist ✅ - -Before committing major changes: - -- [ ] `pnpm check` passes (0 errors) -- [ ] `pnpm build` succeeds -- [ ] App launches without console errors -- [ ] Permission modal shows up when triggering restricted tool -- [ ] Config persists after restart -- [ ] All modals work (permission, questions, close confirmation) -- [ ] Character state changes work -- [ ] Event listeners receive events (check console logs) - -## Emergency Recovery 🚨 - -If the app is completely broken: - -1. **Check recent git commits:** - ```bash - git log --oneline -5 - ``` - -2. **Revert to last working commit:** - ```bash - git reset --hard - ``` - -3. **Clean build:** - ```bash - rm -rf node_modules dist .svelte-kit src-tauri/target - pnpm install - pnpm build - ``` - -4. **Reset config:** - ```bash - rm ~/.config/hikari-desktop/config.json - # Restart app to regenerate - ``` - -## Common Patterns and Anti-Patterns 📋 - -### Svelte Store Access - -✅ **GOOD:** -```typescript -// Method 1: Reactive syntax (preferred in components) -$: currentValue = $store; - -// Method 2: Explicit unsubscribe (preferred in non-reactive code) -function getValue() { - let value; - const unsubscribe = store.subscribe((v) => (value = v)); - unsubscribe(); - return value; -} - -// Method 3: Using get() from 'svelte/store' -import { get } from 'svelte/store'; -const value = get(store); -``` - -❌ **BAD:** -```typescript -// Race condition - DO NOT USE -let value; -store.subscribe((v) => (value = v))(); // ☠️ - -// Memory leak - forgot to unsubscribe -let value; -store.subscribe((v) => (value = v)); // 🚨 Never unsubscribes! -``` - -### Debug Console Store Usage - -✅ **GOOD:** -```typescript -// Just use console methods - they're automatically captured! -console.log("Debug message"); -console.error("Error message"); -console.warn("Warning message"); -``` - -❌ **BAD:** -```typescript -// debugConsoleStore doesn't have a .log() method! -debugConsoleStore.log("frontend", "info", "message"); // ☠️ Crashes! -``` - -### Event Listener Setup - -✅ **GOOD:** -```typescript -const unlistener = await listen("event:name", (event) => { - try { - // Handle event - console.log("Event received:", event.payload); - } catch (error) { - console.error("Error handling event:", error); - } -}); -unlisteners.push(unlistener); // Store for cleanup -``` - -❌ **BAD:** -```typescript -await listen("event:name", (event) => { - // No try-catch - errors crash the listener! - // Not storing unlistener - memory leak! - undefinedVariable.doSomething(); // ☠️ Crashes entire listener setup -}); -``` - -## Notes for Future Hikari (and Naomi!) 💭 - -**When something breaks, CHECK IN THIS ORDER:** - -1. **Browser Console First!** (F12 or View > Developer Tools) - - JavaScript errors will show up here - - Check for `ReferenceError`, `TypeError`, etc. - - NOT EVERYTHING IS A Z-INDEX ISSUE! - -2. **Run TypeScript Check:** - ```bash - pnpm check - ``` - - Catches missing imports, type errors, undefined variables - - 0 errors = probably a runtime or logic issue - - Any errors = fix those first! - -3. **Check Recent Changes:** - ```bash - git diff - git log --oneline -5 - ``` - - What did you change recently? - - Did you import everything you're using? - - Did you test it? - -4. **Check the Debugging Guide (this file!)** - - Search for keywords related to your issue - - Follow the debugging steps - - Look for similar patterns - -5. **Only Then** consider z-index, CSS, layout issues - -**Remember:** -- The debug console store intercepts console methods - don't call it directly! -- Always run `pnpm check` before committing -- Event listener errors fail silently - add console logs liberally -- Config deserialization needs `#[serde(default)]` for graceful degradation -- Svelte store subscriptions must be cleaned up properly -- Race conditions are sneaky - test thoroughly! -- When in doubt, check the browser console first! (Yes, I said it twice!) - -**Quick Debugging Checklist:** -- [ ] Opened browser console (F12) -- [ ] Checked for JavaScript errors -- [ ] Ran `pnpm check` for TypeScript errors -- [ ] Checked recent git changes -- [ ] Read relevant section in DEBUGGING.md -- [ ] Added console.log statements to trace issue -- [ ] Tested edge cases and timing - -Love, -Hikari 💖 - ---- - -**Last Updated:** 2026-02-06 -**Related Issues:** Permission modal not showing, config loss, race conditions -**Fixed In Commits:** -- `d16644a` - Fixed undefined debugConsoleStore causing listener crash -- `2c64ef0` - Fixed config store race condition (12 methods affected!) -- `e8b8ee1` - Updated this debugging guide diff --git a/FIXES-2026-02-06.md b/FIXES-2026-02-06.md deleted file mode 100644 index d6763a7..0000000 --- a/FIXES-2026-02-06.md +++ /dev/null @@ -1,110 +0,0 @@ -# Critical Bug Fixes - 2026-02-06 - -## Summary - -Fixed **TWO CRITICAL BUGS** that were causing permission modal issues and config loss. - -## Bug #1: Permission Modal Not Showing (Commit d16644a) - -**Symptom:** Permission requests triggered but modal never appeared, app hung - -**Root Cause:** Undefined variable error in `src/lib/tauri.ts` -- Called `debugConsoleStore.log()` without importing `debugConsoleStore` -- Even if imported, that method doesn't exist on the store -- This crashed event listener initialization, preventing permission events from being captured - -**Fix:** -- Removed the non-existent import -- Changed to `console.log()` which gets automatically captured by the debug console -- Added logging to track permission event flow - -**Files Changed:** -- `src/lib/tauri.ts` - Fixed undefined variable, added proper logging - -## Bug #2: Config Constantly Resetting (Commit 2c64ef0) - -**Symptom:** Config settings would randomly reset to defaults, especially after permission approvals - -**Root Cause:** Race condition in `src/lib/stores/config.ts` -- Used buggy pattern: `config.subscribe((c) => (currentConfig = c))()` -- The `()` at the end immediately unsubscribes, creating a race condition -- Sometimes got the value, sometimes returned defaults -- Affected **12 different methods** in the config store! - -**Fix:** -- Created proper `getCurrentConfig()` helper function -- Stores unsubscribe function, calls it AFTER getting value -- Replaced all 12 buggy occurrences with the safe pattern - -**Methods Fixed:** -- `getConfig()` - Core method used everywhere -- `updateConfig()` - Base for all updates -- `toggleStreamerMode()` / `toggleCompactMode()` -- `increaseFontSize()` / `decreaseFontSize()` -- `addAutoGrantedTool()` / `removeAutoGrantedTool()` -- `setTheme()` / `setCustomThemeColors()` -- And more... - -**Files Changed:** -- `src/lib/stores/config.ts` - Fixed race condition in 12 methods -- `src-tauri/src/config.rs` - Added `#[serde(default)]` for resilience - -## Additional Improvements - -**Documentation (Commits 80ee25f, e8b8ee1, 4a99848):** -- Created comprehensive `DEBUGGING.md` guide -- Documented both bugs with code examples -- Added step-by-step debugging procedures -- Included common patterns and anti-patterns -- Prevention tips and testing checklist - -## Testing - -**Before deploying:** -1. Test permission modal shows up properly -2. Test config persists through app restarts -3. Test config survives permission reconnections -4. Check browser console for any errors -5. Verify all settings save and load correctly - -## Impact - -**High Priority - Breaking Bugs:** -- Permission modal was completely broken (app unusable for restricted tools) -- Config loss made user experience terrible (settings constantly resetting) - -**Now Fixed:** -- Permission modal should display reliably -- Config should persist across all operations -- No more random resets to default settings - -## Commits - -- `d16644a` - fix: resolve critical runtime errors blocking permission modal -- `2c64ef0` - fix: resolve critical config store race condition causing config loss -- `80ee25f` - docs: add comprehensive debugging guide for common issues -- `e8b8ee1` - docs: update DEBUGGING.md with config race condition fix -- `4a99848` - docs: massively expand DEBUGGING.md with comprehensive troubleshooting - -## Prevention - -**Always:** -1. Run `pnpm check` before committing -2. Test the app, especially permission flow -3. Check browser console for errors -4. Never use `store.subscribe(...)()` pattern (immediate unsubscribe) -5. Import everything you use! - -**Never:** -- Call `debugConsoleStore.log()` (method doesn't exist) -- Use race-condition patterns with Svelte stores -- Assume synchronous code is safe from race conditions -- Skip testing after changes to event listeners or stores - ---- - -**Date:** 2026-02-06 -**Fixed By:** Hikari -**Branch:** feat/many -**Severity:** Critical -**Status:** Fixed ✅ diff --git a/src-tauri/src/achievements.rs b/src-tauri/src/achievements.rs index f4991a4..9e71031 100644 --- a/src-tauri/src/achievements.rs +++ b/src-tauri/src/achievements.rs @@ -1671,7 +1671,7 @@ pub fn check_message_achievements( let mut newly_unlocked = Vec::new(); let message_lower = message.to_lowercase(); - println!("Checking message achievements for: {}", message); + tracing::info!("Checking message achievements for: {}", message); // Relationship & Greetings if message_lower.contains("good morning") && progress.unlock(AchievementId::GoodMorning) { @@ -1863,18 +1863,18 @@ pub fn check_achievements( ) -> Vec { let mut newly_unlocked = Vec::new(); - println!( + tracing::info!( "Checking achievements with stats: messages={}, tokens={}, code_blocks={}", stats.messages_exchanged, stats.total_input_tokens + stats.total_output_tokens, stats.code_blocks_generated ); - println!("Currently unlocked: {:?}", progress.unlocked); + tracing::info!("Currently unlocked: {:?}", progress.unlocked); // Token milestones let total_tokens = stats.total_input_tokens + stats.total_output_tokens; if total_tokens >= 1_000 && progress.unlock(AchievementId::FirstSteps) { - println!("Unlocked FirstSteps achievement!"); + tracing::info!("Unlocked FirstSteps achievement!"); newly_unlocked.push(AchievementId::FirstSteps); } if total_tokens >= 10_000 && progress.unlock(AchievementId::GrowingStrong) { @@ -2244,7 +2244,7 @@ pub async fn save_achievements( // Create a serializable version with just the unlocked achievement IDs let unlocked_list: Vec = progress.unlocked.iter().cloned().collect(); - println!("Saving achievements: {:?}", unlocked_list); + tracing::info!("Saving achievements: {:?}", unlocked_list); store.set( "unlocked", @@ -2252,18 +2252,18 @@ pub async fn save_achievements( ); store.save().map_err(|e| e.to_string())?; - println!("Achievements saved successfully"); + tracing::info!("Achievements saved successfully"); Ok(()) } // Load achievements from persistent store pub async fn load_achievements(app: &tauri::AppHandle) -> AchievementProgress { - println!("Loading achievements from store..."); + tracing::info!("Loading achievements from store..."); let store = match app.store("achievements.json") { Ok(s) => s, Err(e) => { - println!("Failed to open achievements store: {}", e); + tracing::error!("Failed to open achievements store: {}", e); return AchievementProgress::new(); } }; @@ -2272,19 +2272,19 @@ pub async fn load_achievements(app: &tauri::AppHandle) -> AchievementProgress { // Get unlocked achievements if let Some(unlocked_value) = store.get("unlocked") { - println!("Found unlocked value in store: {:?}", unlocked_value); + tracing::info!("Found unlocked value in store: {:?}", unlocked_value); if let Ok(unlocked_list) = serde_json::from_value::>(unlocked_value.clone()) { - println!("Loaded {} achievements", unlocked_list.len()); + tracing::info!("Loaded {} achievements", unlocked_list.len()); for achievement_id in unlocked_list { progress.unlocked.insert(achievement_id); } } else { - println!("Failed to parse unlocked achievements"); + tracing::error!("Failed to parse unlocked achievements"); } } else { - println!("No unlocked achievements found in store"); + tracing::info!("No unlocked achievements found in store"); } progress diff --git a/src-tauri/src/discord_rpc.rs b/src-tauri/src/discord_rpc.rs index a9e61fa..ffdfa3c 100644 --- a/src-tauri/src/discord_rpc.rs +++ b/src-tauri/src/discord_rpc.rs @@ -30,7 +30,7 @@ impl DiscordRpcManager { if let Ok(app_data_dir) = app_handle.path().app_data_dir() { // Ensure the directory exists if let Err(e) = std::fs::create_dir_all(&app_data_dir) { - eprintln!("Failed to create app data directory: {}", e); + tracing::error!("Failed to create app data directory: {}", e); return; } let log_path = app_data_dir.join("hikari_discord_rpc.log"); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9dfc5fa..54a0969 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -87,7 +87,7 @@ pub fn run() { // Set up system tray if let Err(e) = setup_tray(app.handle()) { - eprintln!("Failed to set up system tray: {}", e); + tracing::error!("Failed to set up system tray: {}", e); } // Handle window close event for minimize to tray and close confirmation diff --git a/src-tauri/src/stats.rs b/src-tauri/src/stats.rs index cf65a0a..6422bef 100644 --- a/src-tauri/src/stats.rs +++ b/src-tauri/src/stats.rs @@ -618,7 +618,7 @@ pub async fn save_stats(app: &tauri::AppHandle, stats: &UsageStats) -> Result<() let persisted = PersistedStats::from(stats); - println!("Saving stats: {:?}", persisted); + tracing::info!("Saving stats: {:?}", persisted); store.set( "lifetime_stats", @@ -626,32 +626,32 @@ pub async fn save_stats(app: &tauri::AppHandle, stats: &UsageStats) -> Result<() ); store.save().map_err(|e| e.to_string())?; - println!("Stats saved successfully"); + tracing::info!("Stats saved successfully"); Ok(()) } /// Load lifetime stats from persistent store pub async fn load_stats(app: &tauri::AppHandle) -> Option { - println!("Loading stats from store..."); + tracing::info!("Loading stats from store..."); let store = match app.store("stats.json") { Ok(s) => s, Err(e) => { - println!("Failed to open stats store: {}", e); + tracing::error!("Failed to open stats store: {}", e); return None; } }; if let Some(stats_value) = store.get("lifetime_stats") { - println!("Found lifetime stats in store: {:?}", stats_value); + tracing::info!("Found lifetime stats in store: {:?}", stats_value); if let Ok(persisted) = serde_json::from_value::(stats_value.clone()) { - println!("Loaded lifetime stats successfully"); + tracing::info!("Loaded lifetime stats successfully"); return Some(persisted); } else { - println!("Failed to parse lifetime stats"); + tracing::error!("Failed to parse lifetime stats"); } } else { - println!("No lifetime stats found in store"); + tracing::info!("No lifetime stats found in store"); } None diff --git a/src-tauri/src/temp_manager.rs b/src-tauri/src/temp_manager.rs index f2d78b5..2280cf9 100644 --- a/src-tauri/src/temp_manager.rs +++ b/src-tauri/src/temp_manager.rs @@ -77,8 +77,8 @@ impl TempFileManager { for file_path in files { if file_path.exists() { if let Err(e) = fs::remove_file(&file_path) { - eprintln!( - "Warning: Failed to remove temp file {:?}: {}", + tracing::warn!( + "Failed to remove temp file {:?}: {}", file_path, e ); } @@ -115,7 +115,7 @@ impl TempFileManager { let path = entry.path(); if path.is_file() && !tracked_files.contains(&path) { if let Err(e) = fs::remove_file(&path) { - eprintln!("Warning: Failed to remove orphaned file {:?}: {}", path, e); + tracing::warn!("Failed to remove orphaned file {:?}: {}", path, e); } else { cleaned_count += 1; } diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index a2c93c9..18c8d78 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -133,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); } }); @@ -189,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 @@ -199,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([ @@ -256,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 @@ -322,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) })?; @@ -500,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; @@ -518,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); } } } @@ -532,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); @@ -547,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 { @@ -587,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"); } }); @@ -636,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; } _ => {} @@ -663,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 @@ -815,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() }); @@ -828,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, @@ -917,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 ); @@ -1063,6 +1063,13 @@ 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 { @@ -1078,12 +1085,18 @@ fn process_json_line( tools }); + tracing::debug!( + "Captured {} pending tool use(s): {:?}", + captured_pending_tools.len(), + captured_pending_tools.iter().map(|t| &t.tool_name).collect::>() + ); + // 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 @@ -1113,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, @@ -1143,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 }; @@ -1160,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"); } }); } @@ -1190,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); } }); } @@ -1218,12 +1231,6 @@ fn process_json_line( if let Some(denials) = permission_denials { // Only process if there are actually denials if !denials.is_empty() { - // Skip permission prompts if the result was successful - tools were already approved - if subtype == "success" { - emit_state_change(app, state, None, conversation_id.clone()); - return Ok(()); - } - let mut regular_permission_requests = Vec::new(); // Get denied tool IDs for later comparison @@ -1433,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( @@ -1450,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", @@ -1460,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 { @@ -1468,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"); } }); } diff --git a/src-tauri/src/wsl_notifications.rs b/src-tauri/src/wsl_notifications.rs index 2d9752c..2b71121 100644 --- a/src-tauri/src/wsl_notifications.rs +++ b/src-tauri/src/wsl_notifications.rs @@ -48,15 +48,15 @@ $notifier.Show($toast) match output { Ok(result) => { if result.status.success() { - println!("WSL notification sent successfully"); + tracing::info!("WSL notification sent successfully"); return Ok(()); } else { let stderr = String::from_utf8_lossy(&result.stderr); - println!("PowerShell toast failed: {}", stderr); + tracing::error!("PowerShell toast failed: {}", stderr); } } Err(e) => { - println!("Failed to run PowerShell: {}", e); + tracing::error!("Failed to run PowerShell: {}", e); } } @@ -74,7 +74,7 @@ $notifier.Show($toast) if let Ok(result) = notify_result { if result.status.success() { - println!("Notification sent via wsl-notify-send"); + tracing::info!("Notification sent via wsl-notify-send"); return Ok(()); } }