Claude-skill-registry dev-debugging
UNIFIED DEBUGGER - Use when tasks disappear, data is lost, things are broken, or bugs need fixing. Debug Vue.js reactivity, Pinia state, task store CRUD, keyboard shortcuts, canvas positions, drag-drop, cache, memory leaks, and performance. Invoke for "not working", "broken", "fix bug", "debug", "tasks missing", "shortcuts not working", "state not updating".
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/dev-debugging" ~/.claude/skills/majiayu000-claude-skill-registry-dev-debugging && rm -rf "$T"
skills/data/dev-debugging/SKILL.mdAutomatic Skill Chaining
IMPORTANT: After completing debugging work, automatically invoke these skills:
- After fixing a bug → Use
to verify the fix with proper testsSkill(qa-testing) - If canvas issues → Use
for specialized Vue Flow debuggingSkill(vue-flow-debug) - If timer issues → Use
for Pomodoro-specific debuggingSkill(dev-fix-timer) - If Supabase issues → Use
for database/auth debuggingSkill(supabase-debugger)
Example chaining:
User: "Fix the bug where tasks disappear" 1. Claude uses dev-debugging skill (this skill) 2. After fix implemented → Claude invokes: Skill(qa-testing) 3. QA skill runs verification tests 4. Only then claim the fix is complete
When to Defer to Specialized Skills
IMPORTANT: This skill handles general debugging. For domain-specific issues, use specialized skills instead:
| Issue Type | Use This Skill Instead |
|---|---|
| Timer/Pomodoro bugs | |
| Canvas/Vue Flow issues | |
| Supabase/Auth problems | |
| Calendar interface bugs | |
| Port/server conflicts | |
When to use THIS skill (dev-debugging):
- General Vue.js reactivity issues
- Pinia state management bugs
- Task store CRUD problems
- Keyboard shortcut failures
- Memory leaks and performance issues
- Cross-cutting bugs that span multiple systems
Comprehensive Debugging
Instructions
Debugging Philosophy
Always follow systematic debugging methodology:
- Identify the Problem: Gather specific symptoms and error messages
- Isolate the Issue: Use binary search to narrow down the root cause
- Verify the Fix: Ensure the solution works and doesn't introduce regressions
- Document the Solution: Create reusable debugging patterns
Vue.js Reactivity Debugging
Common Reactivity Issues
- Direct Property Assignment: Adding properties to reactive objects without
orVue.set()reactive() - Array Mutation: Direct index assignment or length modification without proper methods
- Computed Caching: Computed properties not updating due to stale dependencies
- Watch Invalidation: Watchers not firing due to incorrect configuration
Debugging Tools
import { watch, watchEffect, computed, ref, reactive } from 'vue' // Debug reactivity dependencies const debugReactivity = () => { const state = reactive({ count: 0, nested: { value: 0 } }) // Track what triggers reactivity watchEffect(() => { console.log('Effect triggered with:', state.count, state.nested.value) }) // Debug computed dependencies const doubled = computed(() => { console.log('Computed recalculating with:', state.count) return state.count * 2 }) return { state, doubled } } // Component-level debugging hooks export default { setup() { // Debug what triggers re-renders onRenderTracked((e) => { console.log('Render tracked:', e.key, e.type, e.target) }) onRenderTriggered((e) => { console.log('Render triggered:', e.key, e.type, e.target) }) } }
Pinia State Debugging
Store State Issues
- Stale State: State not updating due to missing reactivity
- Persistence Conflicts: LocalStorage hydration overriding current state
- Action Side Effects: Actions causing unexpected state mutations
- Subscription Leaks: Unmanaged store subscriptions
Debugging Patterns
import { defineStore } from 'pinia' export const useDebugStore = defineStore('debug', { state: () => ({ tasks: [], selectedTaskId: null, lastMutation: null, debugMode: true }), actions: { // Add mutation tracking addTask(task) { if (this.debugMode) { console.trace('addTask called with:', task) console.log('State before:', JSON.parse(JSON.stringify(this.tasks))) } this.tasks.push(task) this.lastMutation = { type: 'addTask', timestamp: Date.now(), payload: task } if (this.debugMode) { console.log('State after:', JSON.parse(JSON.stringify(this.tasks))) } }, // Batch mutations for atomic updates updateTasks(updates) { this.$patch((state) => { updates.forEach(({ id, ...changes }) => { const task = state.tasks.find(t => t.id === id) if (task) { Object.assign(task, changes) if (this.debugMode) { console.log(`Task ${id} updated:`, changes) } } }) }) } }, // Debug getters with caching issues getters: { completedTasks: (state) => { console.log('completedTasks getter recalculating') return state.tasks.filter(task => task.completed) }, taskById: (state) => { console.log('taskById getter recalculating') return (id) => state.tasks.find(task => task.id === id) } } })
Cached State Issues
Browser Storage Debugging
import { useLocalStorage, useSessionStorage } from '@vueuse/core' const debugStorage = () => { const settings = useLocalStorage('app-settings', { theme: 'light', language: 'en' }) // Monitor storage changes window.addEventListener('storage', (e) => { console.log('Storage changed:', e.key, e.oldValue, e.newValue) }) // Debug hydration conflicts const checkStorageConsistency = () => { const raw = localStorage.getItem('app-settings') const parsed = JSON.parse(raw) console.log('Raw storage:', raw) console.log('Parsed storage:', parsed) console.log('Current reactive:', settings.value) if (JSON.stringify(settings.value) !== JSON.stringify(parsed)) { console.warn('Storage inconsistency detected!') } } return { settings, checkStorageConsistency } }
Memory Leak Detection
const detectMemoryLeaks = () => { const componentInstances = new WeakSet() const subscriptions = new Set() const trackComponent = (component) => { componentInstances.add(component) console.log('Component tracked:', component.$options.name) } const trackSubscription = (unsubscribe) => { subscriptions.add(unsubscribe) // Auto-cleanup after component unmounts onUnmounted(() => { if (subscriptions.has(unsubscribe)) { unsubscribe() subscriptions.delete(unsubscribe) console.log('Subscription cleaned up') } }) } // Monitor for memory leaks setInterval(() => { console.log('Active subscriptions:', subscriptions.size) console.log('Tracked components:', componentInstances.size || 0) }, 10000) return { trackComponent, trackSubscription } }
Performance Debugging
Component Performance
<template> <div> <!-- Use v-memo for expensive rendering --> <ExpensiveComponent v-for="item in items" :key="item.id" v-memo="[item.id, item.lastModified]" :data="item" /> </div> </template> <script setup> import { ref, computed, shallowRef } from 'vue' // Performance monitoring const renderCount = ref(0) const lastRenderTime = ref(Date.now()) // Use shallowRef for large arrays const items = shallowRef([]) // Optimize computed properties const expensiveComputed = computed(() => { renderCount.value++ lastRenderTime.value = Date.now() console.log(`Computed recalculated (${renderCount.value} times)`) return items.value .filter(item => item.active) .map(item => processItem(item)) }) // Debug performance onMounted(() => { const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (entry.entryType === 'measure') { console.log(`Performance: ${entry.name} took ${entry.duration}ms`) } }) }) observer.observe({ entryTypes: ['measure'] }) // Measure render performance performance.mark('render-start') nextTick(() => { performance.mark('render-end') performance.measure('render-time', 'render-start', 'render-end') }) }) </script>
Canvas Debugging
Canvas State Issues
const debugCanvas = () => { const canvasState = reactive({ nodes: [], selectedNodes: [], viewport: { x: 0, y: 0, zoom: 1 }, isDragging: false }) // Debug canvas mutations const debugCanvasMutation = (action, data) => { console.group(`Canvas ${action}`) console.log('State before:', JSON.parse(JSON.stringify(canvasState))) console.log('Mutation data:', data) action(canvasState, data) console.log('State after:', JSON.parse(JSON.stringify(canvasState))) console.groupEnd() } // Debug selection issues const debugSelection = (nodeIds) => { console.log('Selecting nodes:', nodeIds) console.log('Current selection:', canvasState.selectedNodes) console.log('Available nodes:', canvasState.nodes.map(n => n.id)) const invalidIds = nodeIds.filter(id => !canvasState.nodes.some(n => n.id === id) ) if (invalidIds.length > 0) { console.warn('Invalid node IDs in selection:', invalidIds) } } return { canvasState, debugCanvasMutation, debugSelection } }
Async Operation Debugging
Promise and Timer Debugging
const debugAsync = () => { const activePromises = new Set() const activeTimers = new Set() const debugPromise = (promise, description) => { activePromises.add(promise) console.log(`Promise started: ${description}`) promise .then(result => { console.log(`Promise resolved: ${description}`, result) activePromises.delete(promise) }) .catch(error => { console.error(`Promise rejected: ${description}`, error) activePromises.delete(promise) }) return promise } const debugTimer = (callback, delay, description) => { const timerId = setTimeout(() => { console.log(`Timer fired: ${description}`) callback() activeTimers.delete(timerId) }, delay) activeTimers.add(timerId) console.log(`Timer set: ${description} (${delay}ms)`) return timerId } // Check for leaks setInterval(() => { console.log('Active promises:', activePromises.size) console.log('Active timers:', activeTimers.size) if (activePromises.size > 10) { console.warn('Possible promise leak detected!') } if (activeTimers.size > 5) { console.warn('Possible timer leak detected!') } }, 5000) return { debugPromise, debugTimer } }
Error Boundary Debugging
Global Error Handling
const setupErrorHandling = () => { // Vue error handler app.config.errorHandler = (err, instance, info) => { console.group('Vue Error') console.error('Error:', err) console.error('Component:', instance?.$options.name) console.error('Info:', info) console.error('Props:', instance?.$props) console.error('State:', instance?.$data) console.groupEnd() // Send to error tracking service errorTracking.captureException(err, { tags: { component: instance?.$options.name }, extra: { info, props: instance?.$props } }) } // Unhandled promise rejections window.addEventListener('unhandledrejection', (event) => { console.error('Unhandled promise rejection:', event.reason) // Prevent default browser behavior event.preventDefault() }) // Global error tracking window.addEventListener('error', (event) => { console.error('Global error:', event.error) }) }
Network Debugging
API Request Debugging
const debugNetwork = () => { const originalFetch = window.fetch window.fetch = async (...args) => { const [url, options = {}] = args const requestId = Math.random().toString(36).substr(2, 9) console.group(`🌐 Request ${requestId}: ${options.method || 'GET'} ${url}`) console.log('Request:', options) const startTime = performance.now() try { const response = await originalFetch(...args) const duration = performance.now() - startTime console.log(`✅ Response ${requestId}: ${response.status} (${duration.toFixed(2)}ms)`) console.log('Headers:', Object.fromEntries(response.headers.entries())) // Clone response to avoid consuming it const clonedResponse = response.clone() const data = await clonedResponse.json().catch(() => clonedResponse.text()) console.log('Data:', data) console.groupEnd() return response } catch (error) { const duration = performance.now() - startTime console.error(`❌ Error ${requestId}: ${error.message} (${duration.toFixed(2)}ms)`) console.groupEnd() throw error } } }
Debugging Utilities
State Snapshot
const createDebugger = () => { const snapshots = [] const maxSnapshots = 10 const takeSnapshot = (label, data) => { const snapshot = { label, timestamp: Date.now(), data: JSON.parse(JSON.stringify(data)), stackTrace: new Error().stack } snapshots.push(snapshot) if (snapshots.length > maxSnapshots) { snapshots.shift() } console.log(`📸 Snapshot: ${label}`, snapshot) } const compareSnapshots = (index1, index2) => { const snap1 = snapshots[index1] const snap2 = snapshots[index2] const diff = findDiff(snap1.data, snap2.data) console.log(`🔍 Comparing snapshots: ${snap1.label} vs ${snap2.label}`) console.log('Differences:', diff) } const findDiff = (obj1, obj2) => { // Simple diff implementation const diffs = [] const keys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]) for (const key of keys) { if (JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) { diffs.push({ key, oldValue: obj1[key], newValue: obj2[key] }) } } return diffs } return { takeSnapshot, compareSnapshots, snapshots } }
Usage Examples
Quick Debug Patterns
// Quick reactivity check const checkReactivity = (obj, property) => { const value = obj[property] obj[property] = value console.log('Reactivity check:', obj[property] === value) } // Performance measurement const measurePerformance = (fn, label) => { const start = performance.now() const result = fn() const end = performance.now() console.log(`⏱️ ${label}: ${(end - start).toFixed(2)}ms`) return result } // Debug store mutations const debugStoreMutations = (store) => { store.$subscribe((mutation, state) => { console.group(`🔄 Store Mutation: ${mutation.type}`) console.log('Payload:', mutation.payload) console.log('New state:', state) console.groupEnd() }) }
Canvas Debugging (Integrated from dev-debug-canvas)
🚨 CRITICAL Canvas Testing Requirements
ZERO TOLERANCE POLICY
NEVER claim canvas fixes work without comprehensive visual testing. Canvas issues are often visual and require actual interaction testing.
Before Claiming Success - MANDATORY Steps:
- Start Development Server:
(ensure port 5546 is available)npm run dev - Reactivate Playwright: Use Playwright MCP for canvas interaction testing
- Open Browser DevTools: Use Elements and Console panels for debugging
- Test Mouse Events: Verify click, drag, hover interactions work
- Test Node Selection: Verify single and multi-selection work
- Test Drag/Drop: Verify nodes can be dragged and dropped correctly
- Test Viewport: Verify zoom, pan, and viewport transformations work
- Test Edge Cases: Verify boundary conditions and error scenarios
Canvas Debugging Protocol
Common Canvas Issues
- Mouse Event Failures: Click, drag, hover events not firing
- Node Selection Problems: Single/multi-selection not working
- Viewport Transform Issues: Zoom, pan, or coordinate transformations broken
- Drag/Drop Failures: Nodes won't move or drop correctly
- Coordinate System Problems: Mouse positions don't match node positions
Canvas Debugging Tools
// Canvas coordinate debugging const debugCanvasCoords = (canvas, event) => { const rect = canvas.getBoundingClientRect() const x = event.clientX - rect.left const y = event.clientY - rect.top console.log('Canvas Debug:') console.log('Event client:', event.clientX, event.clientY) console.log('Canvas rect:', rect.left, rect.top, rect.width, rect.height) console.log('Canvas coords:', x, y) console.log('Transformed coords:', canvas.transformPoint(x, y)) return { x, y } } // Mouse event debugging const debugMouseEvents = (element) => { const events = ['mousedown', 'mousemove', 'mouseup', 'click'] events.forEach(eventType => { element.addEventListener(eventType, (e) => { console.log(`🖱️ ${eventType}:`, { target: e.target, button: e.button, buttons: e.buttons, clientX: e.clientX, clientY: e.clientY, ctrlKey: e.ctrlKey, shiftKey: e.shiftKey }) }) }) } // Viewport debugging const debugViewport = (viewport) => { console.log('Viewport Debug:') console.log('Zoom:', viewport.zoom) console.log('Pan:', viewport.x, viewport.y) console.log('Bounds:', viewport.getBounds()) console.log('Transform matrix:', viewport.getTransform()) }
Task Store Debugging
Task CRUD Issues
// Debug task store operations const debugTaskStore = () => { const taskStore = useTaskStore() // Watch for mutations taskStore.$subscribe((mutation, state) => { console.group('🔄 Task store mutation') console.log('Type:', mutation.type) console.log('Tasks count:', state.tasks.length) console.groupEnd() }) // Check for duplicate IDs const checkDuplicates = () => { const ids = taskStore.tasks.map(t => t.id) const duplicates = ids.filter((id, i) => ids.indexOf(id) !== i) if (duplicates.length) console.error('🚨 Duplicate IDs:', duplicates) } return { checkDuplicates } }
Pinia-Supabase Sync Issues
// Diagnose sync problems const diagnosePiniaSync = async () => { const taskStore = useTaskStore() console.log('📊 Pinia tasks:', taskStore.tasks.length) // Compare with Supabase const { data } = await supabase.from('tasks').select('id') console.log('💾 Supabase tasks:', data?.length || 0) // Find discrepancies const piniaIds = new Set(taskStore.tasks.map(t => t.id)) const dbIds = new Set(data?.map(t => t.id) || []) console.log('Missing from Pinia:', [...dbIds].filter(id => !piniaIds.has(id))) }
Keyboard Shortcut Debugging
Event Handler Debug
// Debug keyboard events const debugKeyboard = () => { document.addEventListener('keydown', (e) => { console.log('⌨️ Key:', { key: e.key, code: e.code, ctrl: e.ctrlKey, shift: e.shiftKey, target: e.target.tagName }) // Check if input has focus (shortcuts may be disabled) const isInput = ['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName) if (isInput) console.log('📝 Input focused - shortcuts disabled') }, true) } // Test VueUse magic keys import { useMagicKeys } from '@vueuse/core' const keys = useMagicKeys() watch(keys.ctrl_z, (v) => v && console.log('Ctrl+Z pressed'))
Event Conflict Detection
// Find multiple handlers for same key const detectConflicts = () => { const handlers = [] const originalAdd = EventTarget.prototype.addEventListener EventTarget.prototype.addEventListener = function(type, handler, opts) { if (type === 'keydown') handlers.push({ el: this, handler }) return originalAdd.call(this, type, handler, opts) } console.log('Active keydown handlers:', handlers.length) }
Cache Debugging (Integrated from dev-debug-cache)
Vue 3 Cache Troubleshooting
When to Activate:
- Changes to Vue components are not appearing after refresh
- Hard refresh (Ctrl+R, Ctrl+Shift+R) does not work
- "Disable cache" in DevTools Network tab does not help
- HMR (Hot Module Replacement) appears broken
- The app seems to be serving old/stale code
Service Worker Debugging
// Check for active service workers const debugServiceWorkers = async () => { if ('serviceWorker' in navigator) { const registrations = await navigator.serviceWorker.getRegistrations() console.log('Service Workers:', registrations) registrations.forEach(registration => { console.log('Scope:', registration.scope) console.log('Active:', registration.active) console.log('Installing:', registration.installing) console.log('Waiting:', registration.waiting) }) return registrations } } // Clear service workers const clearServiceWorkers = async () => { const registrations = await navigator.serviceWorker.getRegistrations() for (const registration of registrations) { await registration.unregister() console.log('Unregistered:', registration.scope) } }
Vite Cache Issues
# Clear Vite cache rm -rf node_modules/.vite # Clear npm cache npm cache clean --force # Reinstall dependencies npm install # Restart dev server npm run dev
Production & CDN Debugging
When to Use This Section
- App works locally but fails in production
- Chromium browsers fail, Firefox works
shows correct response but browser failscurl- MIME type errors in browser console
- Stale assets after deployment
Cloudflare Cache MIME Type Issue (CRITICAL)
Symptom: Chromium browsers show MIME type errors for CSS/JS, but
curl returns correct content-type.
Root Cause: Cloudflare caches by URL only. Chromium's preload scanner sends
Accept: text/html, and Cloudflare serves cached HTML instead of CSS/JS.
Quick Diagnostic:
# Check if Vary header is present (MUST include "Accept") curl -sI "https://in-theflow.com/assets/index.css" | grep -iE "vary|content-type" # Expected: # content-type: text/css; charset=utf-8 # vary: Accept-Encoding, Accept
Fix:
# Add to Caddyfile on VPS: @static path /assets/* header @static Vary "Accept-Encoding, Accept"
Full Documentation:
docs/sop/SOP-032-cloudflare-cache-mime-prevention.md
Cloudflare Debug Commands
# Check cache status curl -sI https://example.com/path | grep cf-cache-status # HIT = edge cache, MISS = origin, DYNAMIC = not cached # Check response headers curl -sI https://example.com/assets/app.css | grep -iE "content-type|cache-control|vary" # Bypass Cloudflare (test origin directly) curl -sI --resolve "example.com:443:YOUR_VPS_IP" https://example.com/path
VPS/Caddy Debug Commands
# Check Caddy status ssh root@VPS "systemctl status caddy" # View Caddy logs ssh root@VPS "journalctl -u caddy -f" # Validate Caddy config ssh root@VPS "caddy validate --config /etc/caddy/Caddyfile" # Reload Caddy ssh root@VPS "systemctl reload caddy"
Browser-Specific Issues
| Works | Fails | Likely Cause |
|---|---|---|
| Firefox | Chrome/Brave | Cloudflare cache + preload scanner |
| curl | All browsers | Service Worker cache |
| Incognito | Normal mode | Browser cache |
| VPN | Local | Regional CDN issue |
Quick Fix Script
# Emergency fix for Cloudflare MIME issue ./scripts/fix-cloudflare-cache.sh # Then purge Cloudflare cache: # Dashboard → Caching → Purge Everything
Full Reference:
.claude/skills/🐛 dev-debugging/references/production-cdn-debugging.md
This comprehensive debugging skill provides systematic approaches to identify and resolve complex state management, reactivity, performance, canvas interactions, cache issues, and production/CDN problems in Vue.js applications with Pinia stores.
MANDATORY USER VERIFICATION REQUIREMENT
Policy: No Fix Claims Without User Confirmation
CRITICAL: Before claiming ANY issue, bug, or problem is "fixed", "resolved", "working", or "complete", the following verification protocol is MANDATORY:
Step 1: Technical Verification
- Run all relevant tests (build, type-check, unit tests)
- Verify no console errors
- Take screenshots/evidence of the fix
Step 2: User Verification Request
REQUIRED: Use the
AskUserQuestion tool to explicitly ask the user to verify the fix:
"I've implemented [description of fix]. Before I mark this as complete, please verify: 1. [Specific thing to check #1] 2. [Specific thing to check #2] 3. Does this fix the issue you were experiencing? Please confirm the fix works as expected, or let me know what's still not working."
Step 3: Wait for User Confirmation
- DO NOT proceed with claims of success until user responds
- DO NOT mark tasks as "completed" without user confirmation
- DO NOT use phrases like "fixed", "resolved", "working" without user verification
Step 4: Handle User Feedback
- If user confirms: Document the fix and mark as complete
- If user reports issues: Continue debugging, repeat verification cycle
Prohibited Actions (Without User Verification)
- Claiming a bug is "fixed"
- Stating functionality is "working"
- Marking issues as "resolved"
- Declaring features as "complete"
- Any success claims about fixes
Required Evidence Before User Verification Request
- Technical tests passing
- Visual confirmation via Playwright/screenshots
- Specific test scenarios executed
- Clear description of what was changed
Remember: The user is the final authority on whether something is fixed. No exceptions.
Tauri/Linux Desktop Icon Troubleshooting
When Icons Don't Update After Build
If you rebuild a Tauri app with new icons but the old icons still appear in the taskbar/window title bar, follow this troubleshooting guide.
Common Causes
- Conflicting .desktop files - User-local file takes precedence over system file
- KDE icon cache - Cached icons not refreshed
- StartupWMClass mismatch - Desktop file doesn't match app's WM_CLASS
- Old binary still running - System using old installed version
Diagnostic Commands
# 1. Find all .desktop files for your app find /usr/share/applications ~/.local/share/applications -iname "*flowstate*" -o -iname "*flow-state*" 2>/dev/null # 2. Check WM_CLASS of running app (click on app window when prompted) xprop WM_CLASS # 3. View .desktop file contents cat /usr/share/applications/FlowState.desktop cat ~/.local/share/applications/flowstate.desktop # 4. Check installed icon locations ls -la /usr/share/icons/hicolor/*/apps/flow-state.png
Fix Procedure (KDE Plasma)
# Step 1: Remove conflicting user .desktop file rm -f ~/.local/share/applications/flowstate.desktop # Step 2: Clear ALL icon caches rm -f ~/.cache/icon-cache.kcache rm -f ~/.cache/ksvg-elements-* rm -rf ~/.cache/ksycoca6* # Step 3: Update desktop database update-desktop-database ~/.local/share/applications 2>/dev/null # Step 4: Rebuild KDE system configuration kbuildsycoca6 --noincremental # For Plasma 6 # OR kbuildsycoca5 --noincremental # For Plasma 5 # Step 5: Kill the app pkill -f "flow-state" # Step 6: Restart plasmashell kquitapp6 plasmashell && kstart plasmashell # Step 7: Relaunch app /usr/bin/flow-state &
If Icons Still Don't Update
-
Reinstall the .deb package:
sudo dpkg -i src-tauri/target/release/bundle/deb/FlowState_*.deb -
Manually copy icons (requires sudo):
sudo cp src-tauri/icons/32x32.png /usr/share/icons/hicolor/32x32/apps/flow-state.png sudo cp src-tauri/icons/128x128.png /usr/share/icons/hicolor/128x128/apps/flow-state.png sudo cp src-tauri/icons/128x128@2x.png /usr/share/icons/hicolor/256x256@2/apps/flow-state.png sudo gtk-update-icon-cache -f /usr/share/icons/hicolor/ -
Log out and log back in - Some icon changes only take effect after re-login
Key Files Reference
| File | Purpose |
|---|---|
| Source icons for Tauri build |
| Icon configuration ( array) |
| System .desktop file |
| User .desktop files (take precedence!) |
| Installed icon locations |
| KDE icon cache |
Desktop File Best Practices
[Desktop Entry] Name=FlowState Comment=Productivity app Exec=flow-state Icon=flow-state Terminal=false Type=Application Categories=Office;Productivity; StartupWMClass=flow-state # MUST match app's WM_CLASS exactly!
Note: The
StartupWMClass must match the WM_CLASS reported by xprop WM_CLASS when clicking on the app window.
Creating Transparent Taskbar Icons from Complex SVGs
Professional taskbar icons (like Spotify, Chrome, Telegram) have transparent backgrounds - just the icon shape floating, no visible background square. Here's how to convert complex SVGs to proper transparent icons.
Why Icons Look Wrong
| Problem | Cause |
|---|---|
| Visible dark/black square behind icon | SVG has background rectangle or fill |
| Icon doesn't match other taskbar icons | Background not truly transparent |
| Icon looks muddy/dark | Opaque background blending with panel |
The Solution: Flood-Fill Transparency
Use ImageMagick to flood-fill from corners, which removes connected background pixels:
# Step 1: Render SVG and make background transparent via flood-fill from all 4 corners magick convert "original.svg" \ -fuzz 25% -fill none -draw "color 0,0 floodfill" \ -fuzz 25% -fill none -draw "color WIDTH-1,0 floodfill" \ -fuzz 25% -fill none -draw "color 0,HEIGHT-1 floodfill" \ -fuzz 25% -fill none -draw "color WIDTH-1,HEIGHT-1 floodfill" \ /tmp/transparent.png # Step 2: Trim transparent areas and resize to 512x512 magick convert /tmp/transparent.png \ -trim +repage \ -resize 490x490 \ -gravity center \ -background none \ -extent 512x512 \ src-tauri/icons/icon.png # Step 3: Verify corners are transparent (should output 0) magick convert src-tauri/icons/icon.png -crop 10x10+0+0 +repage -alpha extract -format "%[fx:mean]" info:
Key Parameters:
- Tolerance for color matching (increase if background not fully removed)-fuzz 25%
- Removes connected pixels of similar color starting from specified pointfloodfill
- Remove transparent borders, reset canvas-trim +repage
- Ensure transparent background when extending-background none
Generate Full Icon Set
# Generate all required sizes for size in 16 24 32 48 64 128 256; do magick convert src-tauri/icons/icon.png -resize ${size}x${size} /tmp/ico/${size}.png done # Windows ICO (multi-size) magick convert /tmp/ico/16.png /tmp/ico/24.png /tmp/ico/32.png /tmp/ico/48.png \ /tmp/ico/64.png /tmp/ico/128.png /tmp/ico/256.png src-tauri/icons/icon.ico # macOS ICNS (requires icnsutil: pip install icnsutil) python3 -c " import icnsutil img = icnsutil.IcnsFile() for size in [16, 32, 128, 256, 512]: img.add_media(file=f'/tmp/icns/icon_{size}x{size}.png') img.write('src-tauri/icons/icon.icns') " # Tauri-specific sizes magick convert src-tauri/icons/icon.png -resize 32x32 src-tauri/icons/32x32.png magick convert src-tauri/icons/icon.png -resize 128x128 src-tauri/icons/128x128.png magick convert src-tauri/icons/icon.png -resize 256x256 "src-tauri/icons/128x128@2x.png"
Full Workflow: SVG → Tauri App → KDE Taskbar
# 1. Create transparent icon from SVG SVG_FILE="/path/to/original.svg" # Get SVG dimensions first identify "$SVG_FILE" # Note WIDTH and HEIGHT magick convert "$SVG_FILE" \ -fuzz 25% -fill none -draw "color 0,0 floodfill" \ -fuzz 25% -fill none -draw "color WIDTH-1,0 floodfill" \ -fuzz 25% -fill none -draw "color 0,HEIGHT-1 floodfill" \ -fuzz 25% -fill none -draw "color WIDTH-1,HEIGHT-1 floodfill" \ -trim +repage \ -resize 490x490 \ -gravity center \ -background none \ -extent 512x512 \ src-tauri/icons/icon.png # 2. Generate all icon sizes (run the generate script above) # 3. Rebuild Tauri app npm run tauri build # 4. Reinstall deb to update system icons sudo dpkg -i src-tauri/target/release/bundle/deb/FlowState_*.deb # 5. Refresh KDE caches kbuildsycoca6 --noincremental # 6. Restart plasmashell (REQUIRED for icon to appear) kquitapp6 plasmashell && kstart plasmashell
Adjusting Icon Size Within Frame
To make the icon larger/smaller within the 512x512 frame:
# Larger icon (less padding) - use 500x500 or 510x510 magick convert /tmp/transparent.png -trim +repage \ -resize 510x510 -gravity center -background none -extent 512x512 \ src-tauri/icons/icon.png # Smaller icon (more padding) - use 450x450 or 400x400 magick convert /tmp/transparent.png -trim +repage \ -resize 450x450 -gravity center -background none -extent 512x512 \ src-tauri/icons/icon.png
Troubleshooting Transparency
# Check alpha range (should be 0-1, not 1-1) magick convert icon.png -alpha extract -format "Alpha: %[fx:minima]-%[fx:maxima]" info: # Check specific corner (should be 0 for transparent) magick convert icon.png -crop 10x10+0+0 +repage -alpha extract -format "%[fx:mean]" info: # If alpha is 1-1 (fully opaque), try increasing fuzz: magick convert original.svg -fuzz 35% -fill none -draw "color 0,0 floodfill" ...