Files
hikari-desktop/DEBUGGING.md
T
hikari 4a99848647 docs: massively expand DEBUGGING.md with comprehensive troubleshooting
Added:
- Table of contents for easy navigation
- Detailed explanations of BOTH bugs (permission modal + config loss)
- Step-by-step debugging procedures for each issue
- Code examples showing wrong vs right patterns
- Common anti-patterns to avoid (Svelte stores, event listeners)
- Quick debugging checklist
- Prevention tips and testing procedures
- Clear ordering: check console FIRST, not z-index!

This guide now documents the complete troubleshooting process so Naomi
never has to explain these issues again.
2026-02-06 22:43:08 -08:00

16 KiB

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
  2. Config Loss Issues
  3. Modal/Overlay Not Showing (General)
  4. Event Listener Issues
  5. Build Issues
  6. Testing Checklist
  7. 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():

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

// ❌ 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

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:

// ❌ 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

// ✅ 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<HikariConfig>) => {
  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

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

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:

// 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:

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:

let value;
store.subscribe((v) => (value = v))();  // Immediate unsubscribe = race!

DO THIS INSTEAD:

let value;
const unsubscribe = store.subscribe((v) => (value = v));
unsubscribe();  // Explicit cleanup
return value;

Or even better, use Svelte's reactive syntax:

<script>
  $: currentValue = $store;  // Auto-subscribes safely
</script>

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:

// ✅ 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:

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:

ls -la ~/.config/hikari-desktop/config.json

Should be writable by user:

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

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:

// ✅ 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:

.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:

console.log("[Tauri Listener] Setting up claude:permission listener");
const permissionUnlisten = await listen<PermissionPromptEvent>("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

pnpm check

Common fixes:

  • Missing imports
  • Wrong types
  • Calling non-existent methods

Rust Errors

cargo build --release

Common fixes:

  • Missing dependencies in Cargo.toml
  • Import path issues
  • Type mismatches

Full Clean Build

# 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:

    git log --oneline -5
    
  2. Revert to last working commit:

    git reset --hard <commit-hash>
    
  3. Clean build:

    rm -rf node_modules dist .svelte-kit src-tauri/target
    pnpm install
    pnpm build
    
  4. Reset config:

    rm ~/.config/hikari-desktop/config.json
    # Restart app to regenerate
    

Common Patterns and Anti-Patterns 📋

Svelte Store Access

GOOD:

// 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:

// 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:

// Just use console methods - they're automatically captured!
console.log("Debug message");
console.error("Error message");
console.warn("Warning message");

BAD:

// debugConsoleStore doesn't have a .log() method!
debugConsoleStore.log("frontend", "info", "message");  // ☠️ Crashes!

Event Listener Setup

GOOD:

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:

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:

    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:

    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