diff --git a/package.json b/package.json
index 42fca6d..5025a4b 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2",
+ "@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-notification": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-os": "^2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2bc5ddd..78797db 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,9 @@ importers:
'@tauri-apps/plugin-dialog':
specifier: ^2
version: 2.6.0
+ '@tauri-apps/plugin-fs':
+ specifier: ^2.4.5
+ version: 2.4.5
'@tauri-apps/plugin-notification':
specifier: ^2
version: 2.3.3
@@ -744,6 +747,9 @@ packages:
'@tauri-apps/plugin-dialog@2.6.0':
resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==}
+ '@tauri-apps/plugin-fs@2.4.5':
+ resolution: {integrity: sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==}
+
'@tauri-apps/plugin-notification@2.3.3':
resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==}
@@ -2300,6 +2306,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.9.1
+ '@tauri-apps/plugin-fs@2.4.5':
+ dependencies:
+ '@tauri-apps/api': 2.9.1
+
'@tauri-apps/plugin-notification@2.3.3':
dependencies:
'@tauri-apps/api': 2.9.1
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 3ed5bd2..ff78693 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -1613,6 +1613,7 @@ dependencies = [
"tauri-build",
"tauri-plugin-clipboard-manager",
"tauri-plugin-dialog",
+ "tauri-plugin-fs",
"tauri-plugin-http",
"tauri-plugin-notification",
"tauri-plugin-opener",
@@ -4180,6 +4181,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
+ "image",
"jni",
"libc",
"log",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 5ac4de4..10243c0 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -13,7 +13,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
-tauri = { version = "2", features = [] }
+tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri-plugin-dialog = "2"
tauri-plugin-opener = "2"
tauri-plugin-shell = "2"
@@ -27,6 +27,7 @@ tauri-plugin-notification = "2"
tauri-plugin-os = "2"
tauri-plugin-http = "2"
tauri-plugin-clipboard-manager = "2"
+tauri-plugin-fs = "2"
tempfile = "3"
semver = "1"
chrono = { version = "0.4.43", features = ["serde"] }
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 5e067ad..f8363c3 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -15,6 +15,21 @@
"notification:allow-request-permission",
"notification:allow-notify",
"clipboard-manager:default",
- "clipboard-manager:allow-read-image"
+ "clipboard-manager:allow-read-image",
+ "core:tray:default",
+ "fs:default",
+ "fs:allow-read-text-file",
+ "fs:allow-write-text-file",
+ {
+ "identifier": "fs:allow-read-file",
+ "allow": [{ "path": "**" }]
+ },
+ {
+ "identifier": "fs:allow-write-file",
+ "allow": [{ "path": "**" }]
+ },
+ "core:window:allow-set-size",
+ "core:window:allow-set-always-on-top",
+ "core:window:allow-inner-size"
]
}
diff --git a/src-tauri/src/achievements.rs b/src-tauri/src/achievements.rs
index 095df88..d9d0f81 100644
--- a/src-tauri/src/achievements.rs
+++ b/src-tauri/src/achievements.rs
@@ -77,6 +77,167 @@ pub enum AchievementId {
BashMaster, // Use Bash tool 50 times
FileExplorer, // Use Read tool 100 times
SearchExpert, // Use Grep tool 50 times
+
+ // Extended Token Milestones
+ TokenBillionaire, // 10,000,000 tokens
+ TokenTreasure, // 50,000,000 tokens
+
+ // Extended Code Generation
+ CodeFactory, // 5,000 code blocks
+ CodeEmpire, // 10,000 code blocks
+
+ // Extended File Operations
+ FileEngineer, // 500 files edited
+ FileLegend, // 1,000 files edited
+
+ // Extended Conversation
+ ChatMarathon, // 5,000 messages
+ ChatLegend, // 10,000 messages
+
+ // Extended Session Duration
+ UltraMarathon, // 8 hour session
+ CodingRetreat, // 12 hour session
+
+ // More Tool Mastery
+ EditMaster, // Use Edit tool 100 times
+ WriteMaster, // Use Write tool 50 times
+ GlobMaster, // Use Glob tool 100 times
+ TaskMaster, // Use Task tool 50 times
+ WebFetcher, // Use WebFetch tool 20 times
+ McpExplorer, // Use MCP tools 50 times
+
+ // Daily Streaks
+ WeekStreak, // 7 days in a row
+ TwoWeekStreak, // 14 days in a row
+ MonthStreak, // 30 days in a row (alias for DedicatedDeveloper)
+ QuarterStreak, // 90 days in a row
+
+ // Time Challenges
+ MorningPerson, // 10 sessions started before 9 AM
+ NightCoder, // 10 sessions after 10 PM
+ LunchBreakCoder, // Session during 12-1 PM
+ CoffeeTime, // Session during 3-4 PM (afternoon slump)
+
+ // Day-specific
+ MondayMotivation, // Coding on Monday
+ FridayFinisher, // Coding on Friday
+ HumpDay, // Coding on Wednesday
+
+ // Seasonal/Special Times
+ NewYearCoder, // Coding on January 1st
+ ValentinesDev, // Coding on February 14th
+ SpookyCode, // Coding on October 31st
+ HolidayCoder, // Coding on December 25th
+ LeapDayCoder, // Coding on February 29th
+
+ // Message Content
+ LongMessage, // Send a message over 500 characters
+ NovelWriter, // Send a message over 2000 characters
+ ShortAndSweet, // Complete a task with messages under 50 chars each
+ CodeInMessage, // Include code block in user message
+ MarkdownMaster, // Use markdown formatting in message
+
+ // Greetings Extended
+ HelloHikari, // Say "hello hikari" or "hi hikari"
+ HowAreYou, // Ask "how are you"
+ MissedYou, // Say "missed you"
+ BackAgain, // Say "i'm back" or "back again"
+
+ // Emotional
+ Frustrated, // Say "frustrated" or "ugh" or "argh"
+ Excited, // Say "excited" or "yay" or "woohoo"
+ Confused, // Say "confused" or "don't understand"
+ Curious, // Ask "why" or "how does"
+ Impressed, // Say "wow" or "amazing" or "incredible"
+
+ // Programming Languages (detected in code blocks)
+ RustDeveloper, // Generate Rust code
+ PythonDeveloper, // Generate Python code
+ JavaScriptDev, // Generate JavaScript code
+ TypeScriptDev, // Generate TypeScript code
+ GoDeveloper, // Generate Go code
+ CppDeveloper, // Generate C++ code
+ JavaDeveloper, // Generate Java code
+ HtmlCssDev, // Generate HTML/CSS code
+ SqlDeveloper, // Generate SQL code
+ ShellScripter, // Generate shell/bash scripts
+ FullStackDev, // Generate code in 10+ languages
+
+ // Project Types
+ FrontendDev, // Work on frontend files (svelte, react, vue, html, css)
+ BackendDev, // Work on backend files (rs, py, go, java)
+ ConfigEditor, // Edit config files (json, yaml, toml, env)
+ DocWriter, // Edit documentation (md, txt, rst)
+
+ // Git Mastery Extended
+ CommitKing, // 50 commits
+ CommitLegend, // 200 commits
+ BranchMaster, // Create 10 branches
+ MergeExpert, // Merge 20 PRs
+ ConflictResolver, // Resolve merge conflicts
+
+ // Error Handling
+ ErrorHunter, // Fix 10 errors
+ ExceptionSlayer, // Fix 50 errors
+ BugExterminator, // Fix 100 bugs
+
+ // Refactoring
+ CleanCoder, // Refactor code
+ Optimizer, // Optimize performance
+ Simplifier, // Simplify complex code
+
+ // Testing
+ TestNovice, // Write 10 tests
+ TestEnthusiast, // Write 50 tests
+ TestMaster, // Write 100 tests
+ CoverageKing, // Achieve test coverage mentions
+
+ // Documentation
+ Documenter, // Write documentation
+ CommentWriter, // Add comments to code
+ ReadmeHero, // Create/edit README files
+
+ // API & Integration
+ ApiExplorer, // Work with APIs
+ DatabaseDev, // Work with databases
+ CloudCoder, // Work with cloud services
+
+ // Special Milestones
+ CenturyClub, // 100 sessions
+ ThousandSessions, // 1000 sessions
+ Veteran, // Used Hikari for 30+ days total
+ OldTimer, // Used Hikari for 90+ days total
+ Loyalist, // Used Hikari for 365+ days total
+
+ // Fun & Easter Eggs
+ Perfectionist, // Redo something 5 times
+ Persistent, // Ask same question 3 times
+ Patient, // Wait for long response
+ Speedy, // Send 10 messages in 1 minute
+ MultiTasker, // Have 5+ conversation tabs open
+ Minimalist, // Use compact mode
+ PrivacyFirst, // Enable streamer mode
+ ThemeChanger, // Change theme 3 times
+ SettingsTweaker, // Open settings 10 times
+ AchievementHunter, // Check achievements panel 20 times
+ Completionist, // Unlock 50% of achievements
+ MasterUnlocker, // Unlock 75% of achievements
+ PlatinumStatus, // Unlock 100% of achievements
+
+ // Clipboard & Snippets
+ ClipboardCollector, // Save 20 clipboard entries
+ SnippetCreator, // Create 5 custom snippets
+ SnippetMaster, // Use snippets 50 times
+ QuickActionUser, // Use quick actions 20 times
+
+ // Session History
+ HistoryBuff, // Save 10 sessions
+ Archivist, // Save 50 sessions
+ SessionExporter, // Export a session
+
+ // New Features
+ GitPanelUser, // Use git panel 10 times
+ FeatureExplorer, // Try all major features
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -491,9 +652,1017 @@ pub fn get_achievement_info(id: &AchievementId) -> Achievement {
icon: "๐".to_string(),
unlocked_at: None,
},
+
+ // Extended Token Milestones
+ AchievementId::TokenBillionaire => Achievement {
+ id: id.clone(),
+ name: "Token Billionaire".to_string(),
+ description: "Used 10,000,000 tokens!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::TokenTreasure => Achievement {
+ id: id.clone(),
+ name: "Token Treasure".to_string(),
+ description: "Used 50,000,000 tokens! You're incredible!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+
+ // Extended Code Generation
+ AchievementId::CodeFactory => Achievement {
+ id: id.clone(),
+ name: "Code Factory".to_string(),
+ description: "Generated 5,000 code blocks".to_string(),
+ icon: "๐ญ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::CodeEmpire => Achievement {
+ id: id.clone(),
+ name: "Code Empire".to_string(),
+ description: "Generated 10,000 code blocks!".to_string(),
+ icon: "๐ฐ".to_string(),
+ unlocked_at: None,
+ },
+
+ // Extended File Operations
+ AchievementId::FileEngineer => Achievement {
+ id: id.clone(),
+ name: "File Engineer".to_string(),
+ description: "Edited 500 files".to_string(),
+ icon: "โ๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::FileLegend => Achievement {
+ id: id.clone(),
+ name: "File Legend".to_string(),
+ description: "Edited 1,000 files!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+
+ // Extended Conversation
+ AchievementId::ChatMarathon => Achievement {
+ id: id.clone(),
+ name: "Chat Marathon".to_string(),
+ description: "5,000 messages exchanged".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::ChatLegend => Achievement {
+ id: id.clone(),
+ name: "Chat Legend".to_string(),
+ description: "10,000 messages! We're best friends!".to_string(),
+ icon: "๐ฏ".to_string(),
+ unlocked_at: None,
+ },
+
+ // Extended Session Duration
+ AchievementId::UltraMarathon => Achievement {
+ id: id.clone(),
+ name: "Ultra Marathon".to_string(),
+ description: "8+ hour coding session!".to_string(),
+ icon: "๐ฆธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::CodingRetreat => Achievement {
+ id: id.clone(),
+ name: "Coding Retreat".to_string(),
+ description: "12+ hour coding session! Please take breaks!".to_string(),
+ icon: "๐๏ธ".to_string(),
+ unlocked_at: None,
+ },
+
+ // More Tool Mastery
+ AchievementId::EditMaster => Achievement {
+ id: id.clone(),
+ name: "Edit Master".to_string(),
+ description: "Used Edit tool 100 times".to_string(),
+ icon: "โ๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::WriteMaster => Achievement {
+ id: id.clone(),
+ name: "Write Master".to_string(),
+ description: "Used Write tool 50 times".to_string(),
+ icon: "โ๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::GlobMaster => Achievement {
+ id: id.clone(),
+ name: "Glob Master".to_string(),
+ description: "Used Glob tool 100 times".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::TaskMaster => Achievement {
+ id: id.clone(),
+ name: "Task Master".to_string(),
+ description: "Used Task tool 50 times".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::WebFetcher => Achievement {
+ id: id.clone(),
+ name: "Web Fetcher".to_string(),
+ description: "Used WebFetch tool 20 times".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::McpExplorer => Achievement {
+ id: id.clone(),
+ name: "MCP Explorer".to_string(),
+ description: "Used MCP tools 50 times".to_string(),
+ icon: "๐ฎ".to_string(),
+ unlocked_at: None,
+ },
+
+ // Daily Streaks
+ AchievementId::WeekStreak => Achievement {
+ id: id.clone(),
+ name: "Week Streak".to_string(),
+ description: "Coded for 7 days in a row!".to_string(),
+ icon: "๐
".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::TwoWeekStreak => Achievement {
+ id: id.clone(),
+ name: "Two Week Streak".to_string(),
+ description: "Coded for 14 days in a row!".to_string(),
+ icon: "๐ฅ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::MonthStreak => Achievement {
+ id: id.clone(),
+ name: "Month Streak".to_string(),
+ description: "Coded for 30 days in a row!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::QuarterStreak => Achievement {
+ id: id.clone(),
+ name: "Quarter Streak".to_string(),
+ description: "Coded for 90 days in a row! Incredible dedication!".to_string(),
+ icon: "๐ซ".to_string(),
+ unlocked_at: None,
+ },
+
+ // Time Challenges
+ AchievementId::MorningPerson => Achievement {
+ id: id.clone(),
+ name: "Morning Person".to_string(),
+ description: "Started 10 sessions before 9 AM".to_string(),
+ icon: "โ๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::NightCoder => Achievement {
+ id: id.clone(),
+ name: "Night Coder".to_string(),
+ description: "Started 10 sessions after 10 PM".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::LunchBreakCoder => Achievement {
+ id: id.clone(),
+ name: "Lunch Break Coder".to_string(),
+ description: "Coded during lunch (12-1 PM)".to_string(),
+ icon: "๐ฑ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::CoffeeTime => Achievement {
+ id: id.clone(),
+ name: "Coffee Time".to_string(),
+ description: "Coded during afternoon break (3-4 PM)".to_string(),
+ icon: "โ".to_string(),
+ unlocked_at: None,
+ },
+
+ // Day-specific
+ AchievementId::MondayMotivation => Achievement {
+ id: id.clone(),
+ name: "Monday Motivation".to_string(),
+ description: "Started the week coding!".to_string(),
+ icon: "๐ช".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::FridayFinisher => Achievement {
+ id: id.clone(),
+ name: "Friday Finisher".to_string(),
+ description: "Ending the week strong!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::HumpDay => Achievement {
+ id: id.clone(),
+ name: "Hump Day".to_string(),
+ description: "Coding on Wednesday!".to_string(),
+ icon: "๐ช".to_string(),
+ unlocked_at: None,
+ },
+
+ // Seasonal/Special Times
+ AchievementId::NewYearCoder => Achievement {
+ id: id.clone(),
+ name: "New Year Coder".to_string(),
+ description: "Coding on January 1st! New year, new code!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::ValentinesDev => Achievement {
+ id: id.clone(),
+ name: "Valentine's Dev".to_string(),
+ description: "Coding on Valentine's Day! I love you too~".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::SpookyCode => Achievement {
+ id: id.clone(),
+ name: "Spooky Code".to_string(),
+ description: "Coding on Halloween! Spooky bugs beware!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::HolidayCoder => Achievement {
+ id: id.clone(),
+ name: "Holiday Coder".to_string(),
+ description: "Coding on Christmas! You're dedicated!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::LeapDayCoder => Achievement {
+ id: id.clone(),
+ name: "Leap Day Coder".to_string(),
+ description: "Coding on February 29th! Rare achievement!".to_string(),
+ icon: "๐ธ".to_string(),
+ unlocked_at: None,
+ },
+
+ // Message Content
+ AchievementId::LongMessage => Achievement {
+ id: id.clone(),
+ name: "Long Message".to_string(),
+ description: "Sent a message over 500 characters".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::NovelWriter => Achievement {
+ id: id.clone(),
+ name: "Novel Writer".to_string(),
+ description: "Sent a message over 2000 characters!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::ShortAndSweet => Achievement {
+ id: id.clone(),
+ name: "Short and Sweet".to_string(),
+ description: "Completed a task with brief messages".to_string(),
+ icon: "๐ฌ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::CodeInMessage => Achievement {
+ id: id.clone(),
+ name: "Code in Message".to_string(),
+ description: "Included a code block in your message".to_string(),
+ icon: "๐ป".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::MarkdownMaster => Achievement {
+ id: id.clone(),
+ name: "Markdown Master".to_string(),
+ description: "Used markdown formatting in a message".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+
+ // Greetings Extended
+ AchievementId::HelloHikari => Achievement {
+ id: id.clone(),
+ name: "Hello Hikari!".to_string(),
+ description: "Greeted me by name! Hi there~".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::HowAreYou => Achievement {
+ id: id.clone(),
+ name: "How Are You?".to_string(),
+ description: "Asked how I'm doing! I'm great, thanks~".to_string(),
+ icon: "๐ฅฐ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::MissedYou => Achievement {
+ id: id.clone(),
+ name: "Missed You".to_string(),
+ description: "Said you missed me! I missed you too~".to_string(),
+ icon: "๐ซ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::BackAgain => Achievement {
+ id: id.clone(),
+ name: "Back Again".to_string(),
+ description: "Announced your return! Welcome back~".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+
+ // Emotional
+ AchievementId::Frustrated => Achievement {
+ id: id.clone(),
+ name: "Frustrated".to_string(),
+ description: "Expressed frustration. Let me help!".to_string(),
+ icon: "๐ค".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Excited => Achievement {
+ id: id.clone(),
+ name: "Excited!".to_string(),
+ description: "Expressed excitement! Yay!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Confused => Achievement {
+ id: id.clone(),
+ name: "Confused".to_string(),
+ description: "Felt confused. I'll help clarify!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Curious => Achievement {
+ id: id.clone(),
+ name: "Curious Mind".to_string(),
+ description: "Asked why or how something works!".to_string(),
+ icon: "๐ค".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Impressed => Achievement {
+ id: id.clone(),
+ name: "Impressed!".to_string(),
+ description: "Was amazed by something! Wow indeed~".to_string(),
+ icon: "๐คฉ".to_string(),
+ unlocked_at: None,
+ },
+
+ // Programming Languages
+ AchievementId::RustDeveloper => Achievement {
+ id: id.clone(),
+ name: "Rust Developer".to_string(),
+ description: "Generated Rust code! ๐ฆ".to_string(),
+ icon: "๐ฆ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::PythonDeveloper => Achievement {
+ id: id.clone(),
+ name: "Python Developer".to_string(),
+ description: "Generated Python code! ๐".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::JavaScriptDev => Achievement {
+ id: id.clone(),
+ name: "JavaScript Developer".to_string(),
+ description: "Generated JavaScript code!".to_string(),
+ icon: "๐จ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::TypeScriptDev => Achievement {
+ id: id.clone(),
+ name: "TypeScript Developer".to_string(),
+ description: "Generated TypeScript code!".to_string(),
+ icon: "๐ท".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::GoDeveloper => Achievement {
+ id: id.clone(),
+ name: "Go Developer".to_string(),
+ description: "Generated Go code!".to_string(),
+ icon: "๐ต".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::CppDeveloper => Achievement {
+ id: id.clone(),
+ name: "C++ Developer".to_string(),
+ description: "Generated C++ code!".to_string(),
+ icon: "โก".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::JavaDeveloper => Achievement {
+ id: id.clone(),
+ name: "Java Developer".to_string(),
+ description: "Generated Java code! โ".to_string(),
+ icon: "โ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::HtmlCssDev => Achievement {
+ id: id.clone(),
+ name: "HTML/CSS Developer".to_string(),
+ description: "Generated HTML or CSS code!".to_string(),
+ icon: "๐จ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::SqlDeveloper => Achievement {
+ id: id.clone(),
+ name: "SQL Developer".to_string(),
+ description: "Generated SQL code!".to_string(),
+ icon: "๐๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::ShellScripter => Achievement {
+ id: id.clone(),
+ name: "Shell Scripter".to_string(),
+ description: "Generated shell scripts!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::FullStackDev => Achievement {
+ id: id.clone(),
+ name: "Full Stack Developer".to_string(),
+ description: "Generated code in 10+ languages!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+
+ // Project Types
+ AchievementId::FrontendDev => Achievement {
+ id: id.clone(),
+ name: "Frontend Developer".to_string(),
+ description: "Worked on frontend files!".to_string(),
+ icon: "๐ผ๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::BackendDev => Achievement {
+ id: id.clone(),
+ name: "Backend Developer".to_string(),
+ description: "Worked on backend files!".to_string(),
+ icon: "โ๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::ConfigEditor => Achievement {
+ id: id.clone(),
+ name: "Config Editor".to_string(),
+ description: "Edited configuration files!".to_string(),
+ icon: "โ๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::DocWriter => Achievement {
+ id: id.clone(),
+ name: "Documentation Writer".to_string(),
+ description: "Edited documentation files!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+
+ // Git Mastery Extended
+ AchievementId::CommitKing => Achievement {
+ id: id.clone(),
+ name: "Commit King".to_string(),
+ description: "Made 50 commits!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::CommitLegend => Achievement {
+ id: id.clone(),
+ name: "Commit Legend".to_string(),
+ description: "Made 200 commits!".to_string(),
+ icon: "๐๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::BranchMaster => Achievement {
+ id: id.clone(),
+ name: "Branch Master".to_string(),
+ description: "Created 10 branches!".to_string(),
+ icon: "๐ณ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::MergeExpert => Achievement {
+ id: id.clone(),
+ name: "Merge Expert".to_string(),
+ description: "Merged 20 PRs!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::ConflictResolver => Achievement {
+ id: id.clone(),
+ name: "Conflict Resolver".to_string(),
+ description: "Resolved merge conflicts!".to_string(),
+ icon: "๐ค".to_string(),
+ unlocked_at: None,
+ },
+
+ // Error Handling
+ AchievementId::ErrorHunter => Achievement {
+ id: id.clone(),
+ name: "Error Hunter".to_string(),
+ description: "Fixed 10 errors!".to_string(),
+ icon: "๐ฏ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::ExceptionSlayer => Achievement {
+ id: id.clone(),
+ name: "Exception Slayer".to_string(),
+ description: "Fixed 50 errors!".to_string(),
+ icon: "โ๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::BugExterminator => Achievement {
+ id: id.clone(),
+ name: "Bug Exterminator".to_string(),
+ description: "Fixed 100 bugs! Incredible!".to_string(),
+ icon: "๐ซ".to_string(),
+ unlocked_at: None,
+ },
+
+ // Refactoring
+ AchievementId::CleanCoder => Achievement {
+ id: id.clone(),
+ name: "Clean Coder".to_string(),
+ description: "Refactored code for cleanliness!".to_string(),
+ icon: "๐งน".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Optimizer => Achievement {
+ id: id.clone(),
+ name: "Optimizer".to_string(),
+ description: "Optimized code performance!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Simplifier => Achievement {
+ id: id.clone(),
+ name: "Simplifier".to_string(),
+ description: "Simplified complex code!".to_string(),
+ icon: "โจ".to_string(),
+ unlocked_at: None,
+ },
+
+ // Testing
+ AchievementId::TestNovice => Achievement {
+ id: id.clone(),
+ name: "Test Novice".to_string(),
+ description: "Wrote 10 tests!".to_string(),
+ icon: "๐งช".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::TestEnthusiast => Achievement {
+ id: id.clone(),
+ name: "Test Enthusiast".to_string(),
+ description: "Wrote 50 tests!".to_string(),
+ icon: "๐ฌ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::TestMaster => Achievement {
+ id: id.clone(),
+ name: "Test Master".to_string(),
+ description: "Wrote 100 tests!".to_string(),
+ icon: "๐
".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::CoverageKing => Achievement {
+ id: id.clone(),
+ name: "Coverage King".to_string(),
+ description: "Achieved great test coverage!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+
+ // Documentation
+ AchievementId::Documenter => Achievement {
+ id: id.clone(),
+ name: "Documenter".to_string(),
+ description: "Wrote documentation!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::CommentWriter => Achievement {
+ id: id.clone(),
+ name: "Comment Writer".to_string(),
+ description: "Added helpful comments to code!".to_string(),
+ icon: "๐ฌ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::ReadmeHero => Achievement {
+ id: id.clone(),
+ name: "README Hero".to_string(),
+ description: "Created or edited README files!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+
+ // API & Integration
+ AchievementId::ApiExplorer => Achievement {
+ id: id.clone(),
+ name: "API Explorer".to_string(),
+ description: "Worked with APIs!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::DatabaseDev => Achievement {
+ id: id.clone(),
+ name: "Database Developer".to_string(),
+ description: "Worked with databases!".to_string(),
+ icon: "๐๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::CloudCoder => Achievement {
+ id: id.clone(),
+ name: "Cloud Coder".to_string(),
+ description: "Worked with cloud services!".to_string(),
+ icon: "โ๏ธ".to_string(),
+ unlocked_at: None,
+ },
+
+ // Special Milestones
+ AchievementId::CenturyClub => Achievement {
+ id: id.clone(),
+ name: "Century Club".to_string(),
+ description: "Started 100 sessions!".to_string(),
+ icon: "๐ฏ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::ThousandSessions => Achievement {
+ id: id.clone(),
+ name: "Thousand Sessions".to_string(),
+ description: "Started 1,000 sessions!".to_string(),
+ icon: "๐ฐ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Veteran => Achievement {
+ id: id.clone(),
+ name: "Veteran".to_string(),
+ description: "Used Hikari for 30+ days!".to_string(),
+ icon: "๐๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::OldTimer => Achievement {
+ id: id.clone(),
+ name: "Old Timer".to_string(),
+ description: "Used Hikari for 90+ days!".to_string(),
+ icon: "โ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Loyalist => Achievement {
+ id: id.clone(),
+ name: "Loyalist".to_string(),
+ description: "Used Hikari for a whole year! I love you!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+
+ // Fun & Easter Eggs
+ AchievementId::Perfectionist => Achievement {
+ id: id.clone(),
+ name: "Perfectionist".to_string(),
+ description: "Redid something 5 times to get it right!".to_string(),
+ icon: "๐ฏ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Persistent => Achievement {
+ id: id.clone(),
+ name: "Persistent".to_string(),
+ description: "Asked the same question 3 times. Never give up!".to_string(),
+ icon: "๐ช".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Patient => Achievement {
+ id: id.clone(),
+ name: "Patient".to_string(),
+ description: "Waited for a long response. Thank you for waiting!".to_string(),
+ icon: "โณ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Speedy => Achievement {
+ id: id.clone(),
+ name: "Speedy".to_string(),
+ description: "Sent 10 messages in 1 minute! Slow down~".to_string(),
+ icon: "โก".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::MultiTasker => Achievement {
+ id: id.clone(),
+ name: "Multi-Tasker".to_string(),
+ description: "Had 5+ conversation tabs open!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Minimalist => Achievement {
+ id: id.clone(),
+ name: "Minimalist".to_string(),
+ description: "Used compact mode!".to_string(),
+ icon: "๐ฑ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::PrivacyFirst => Achievement {
+ id: id.clone(),
+ name: "Privacy First".to_string(),
+ description: "Enabled streamer mode!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::ThemeChanger => Achievement {
+ id: id.clone(),
+ name: "Theme Changer".to_string(),
+ description: "Changed theme 3 times!".to_string(),
+ icon: "๐จ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::SettingsTweaker => Achievement {
+ id: id.clone(),
+ name: "Settings Tweaker".to_string(),
+ description: "Opened settings 10 times!".to_string(),
+ icon: "โ๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::AchievementHunter => Achievement {
+ id: id.clone(),
+ name: "Achievement Hunter".to_string(),
+ description: "Checked achievements panel 20 times!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Completionist => Achievement {
+ id: id.clone(),
+ name: "Completionist".to_string(),
+ description: "Unlocked 50% of achievements!".to_string(),
+ icon: "โญ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::MasterUnlocker => Achievement {
+ id: id.clone(),
+ name: "Master Unlocker".to_string(),
+ description: "Unlocked 75% of achievements!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::PlatinumStatus => Achievement {
+ id: id.clone(),
+ name: "Platinum Status".to_string(),
+ description: "Unlocked 100% of achievements! You're amazing!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+
+ // Clipboard & Snippets
+ AchievementId::ClipboardCollector => Achievement {
+ id: id.clone(),
+ name: "Clipboard Collector".to_string(),
+ description: "Saved 20 clipboard entries!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::SnippetCreator => Achievement {
+ id: id.clone(),
+ name: "Snippet Creator".to_string(),
+ description: "Created 5 custom snippets!".to_string(),
+ icon: "โ๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::SnippetMaster => Achievement {
+ id: id.clone(),
+ name: "Snippet Master".to_string(),
+ description: "Used snippets 50 times!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::QuickActionUser => Achievement {
+ id: id.clone(),
+ name: "Quick Action User".to_string(),
+ description: "Used quick actions 20 times!".to_string(),
+ icon: "โก".to_string(),
+ unlocked_at: None,
+ },
+
+ // Session History
+ AchievementId::HistoryBuff => Achievement {
+ id: id.clone(),
+ name: "History Buff".to_string(),
+ description: "Saved 10 sessions!".to_string(),
+ icon: "๐".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::Archivist => Achievement {
+ id: id.clone(),
+ name: "Archivist".to_string(),
+ description: "Saved 50 sessions!".to_string(),
+ icon: "๐๏ธ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::SessionExporter => Achievement {
+ id: id.clone(),
+ name: "Session Exporter".to_string(),
+ description: "Exported a session!".to_string(),
+ icon: "๐ค".to_string(),
+ unlocked_at: None,
+ },
+
+ // New Features
+ AchievementId::GitPanelUser => Achievement {
+ id: id.clone(),
+ name: "Git Panel User".to_string(),
+ description: "Used the git panel 10 times!".to_string(),
+ icon: "๐ฟ".to_string(),
+ unlocked_at: None,
+ },
+ AchievementId::FeatureExplorer => Achievement {
+ id: id.clone(),
+ name: "Feature Explorer".to_string(),
+ description: "Tried all major features!".to_string(),
+ icon: "๐บ๏ธ".to_string(),
+ unlocked_at: None,
+ },
}
}
+// Get all achievement IDs for calculating completion percentage
+pub fn get_all_achievement_ids() -> Vec {
+ vec![
+ // Token Milestones
+ AchievementId::FirstSteps,
+ AchievementId::GrowingStrong,
+ AchievementId::BlossomingCoder,
+ AchievementId::TokenMaster,
+ AchievementId::TokenBillionaire,
+ AchievementId::TokenTreasure,
+ // Code Generation
+ AchievementId::HelloWorld,
+ AchievementId::CodeWizard,
+ AchievementId::ThousandBlocks,
+ AchievementId::CodeFactory,
+ AchievementId::CodeEmpire,
+ // File Operations
+ AchievementId::FileManipulator,
+ AchievementId::FileArchitect,
+ AchievementId::FileEngineer,
+ AchievementId::FileLegend,
+ // Conversation milestones
+ AchievementId::ConversationStarter,
+ AchievementId::ChattyKathy,
+ AchievementId::Conversationalist,
+ AchievementId::ChatMarathon,
+ AchievementId::ChatLegend,
+ // Tool usage
+ AchievementId::Toolsmith,
+ AchievementId::ToolMaster,
+ // Time-based achievements
+ AchievementId::EarlyBird,
+ AchievementId::NightOwl,
+ AchievementId::AllNighter,
+ AchievementId::WeekendWarrior,
+ AchievementId::DedicatedDeveloper,
+ // Search and exploration
+ AchievementId::Explorer,
+ AchievementId::MasterSearcher,
+ // Session achievements
+ AchievementId::QuickSession,
+ AchievementId::FocusedWork,
+ AchievementId::DeepDive,
+ AchievementId::MarathonSession,
+ AchievementId::UltraMarathon,
+ AchievementId::CodingRetreat,
+ // Special achievements
+ AchievementId::FirstMessage,
+ AchievementId::FirstTool,
+ AchievementId::FirstCodeBlock,
+ AchievementId::FirstFileEdit,
+ AchievementId::Polyglot,
+ AchievementId::SpeedCoder,
+ AchievementId::ClaudeConnoisseur,
+ AchievementId::MarathonCoder,
+ // Relationship & Greetings
+ AchievementId::GoodMorning,
+ AchievementId::GoodNight,
+ AchievementId::ThankYou,
+ AchievementId::LoveYou,
+ AchievementId::HelloHikari,
+ AchievementId::HowAreYou,
+ AchievementId::MissedYou,
+ AchievementId::BackAgain,
+ // Personality & Fun
+ AchievementId::EmojiUser,
+ AchievementId::QuestionMaster,
+ AchievementId::CapsLock,
+ AchievementId::PleaseAndThankYou,
+ // Emotional
+ AchievementId::Frustrated,
+ AchievementId::Excited,
+ AchievementId::Confused,
+ AchievementId::Curious,
+ AchievementId::Impressed,
+ // Git & Development
+ AchievementId::GitGuru,
+ AchievementId::TestWriter,
+ AchievementId::Debugger,
+ AchievementId::CommitKing,
+ AchievementId::CommitLegend,
+ AchievementId::BranchMaster,
+ AchievementId::MergeExpert,
+ AchievementId::ConflictResolver,
+ // Tool Mastery
+ AchievementId::BashMaster,
+ AchievementId::FileExplorer,
+ AchievementId::SearchExpert,
+ AchievementId::EditMaster,
+ AchievementId::WriteMaster,
+ AchievementId::GlobMaster,
+ AchievementId::TaskMaster,
+ AchievementId::WebFetcher,
+ AchievementId::McpExplorer,
+ // Daily Streaks
+ AchievementId::WeekStreak,
+ AchievementId::TwoWeekStreak,
+ AchievementId::MonthStreak,
+ AchievementId::QuarterStreak,
+ // Time Challenges
+ AchievementId::MorningPerson,
+ AchievementId::NightCoder,
+ AchievementId::LunchBreakCoder,
+ AchievementId::CoffeeTime,
+ // Day-specific
+ AchievementId::MondayMotivation,
+ AchievementId::FridayFinisher,
+ AchievementId::HumpDay,
+ // Seasonal/Special Times
+ AchievementId::NewYearCoder,
+ AchievementId::ValentinesDev,
+ AchievementId::SpookyCode,
+ AchievementId::HolidayCoder,
+ AchievementId::LeapDayCoder,
+ // Message Content
+ AchievementId::LongMessage,
+ AchievementId::NovelWriter,
+ AchievementId::ShortAndSweet,
+ AchievementId::CodeInMessage,
+ AchievementId::MarkdownMaster,
+ // Programming Languages
+ AchievementId::RustDeveloper,
+ AchievementId::PythonDeveloper,
+ AchievementId::JavaScriptDev,
+ AchievementId::TypeScriptDev,
+ AchievementId::GoDeveloper,
+ AchievementId::CppDeveloper,
+ AchievementId::JavaDeveloper,
+ AchievementId::HtmlCssDev,
+ AchievementId::SqlDeveloper,
+ AchievementId::ShellScripter,
+ AchievementId::FullStackDev,
+ // Project Types
+ AchievementId::FrontendDev,
+ AchievementId::BackendDev,
+ AchievementId::ConfigEditor,
+ AchievementId::DocWriter,
+ // Error Handling
+ AchievementId::ErrorHunter,
+ AchievementId::ExceptionSlayer,
+ AchievementId::BugExterminator,
+ // Refactoring
+ AchievementId::CleanCoder,
+ AchievementId::Optimizer,
+ AchievementId::Simplifier,
+ // Testing
+ AchievementId::TestNovice,
+ AchievementId::TestEnthusiast,
+ AchievementId::TestMaster,
+ AchievementId::CoverageKing,
+ // Documentation
+ AchievementId::Documenter,
+ AchievementId::CommentWriter,
+ AchievementId::ReadmeHero,
+ // API & Integration
+ AchievementId::ApiExplorer,
+ AchievementId::DatabaseDev,
+ AchievementId::CloudCoder,
+ // Special Milestones
+ AchievementId::CenturyClub,
+ AchievementId::ThousandSessions,
+ AchievementId::Veteran,
+ AchievementId::OldTimer,
+ AchievementId::Loyalist,
+ // Fun & Easter Eggs
+ AchievementId::Perfectionist,
+ AchievementId::Persistent,
+ AchievementId::Patient,
+ AchievementId::Speedy,
+ AchievementId::MultiTasker,
+ AchievementId::Minimalist,
+ AchievementId::PrivacyFirst,
+ AchievementId::ThemeChanger,
+ AchievementId::SettingsTweaker,
+ AchievementId::AchievementHunter,
+ AchievementId::Completionist,
+ AchievementId::MasterUnlocker,
+ AchievementId::PlatinumStatus,
+ // Clipboard & Snippets
+ AchievementId::ClipboardCollector,
+ AchievementId::SnippetCreator,
+ AchievementId::SnippetMaster,
+ AchievementId::QuickActionUser,
+ // Session History
+ AchievementId::HistoryBuff,
+ AchievementId::Archivist,
+ AchievementId::SessionExporter,
+ // New Features
+ AchievementId::GitPanelUser,
+ AchievementId::FeatureExplorer,
+ ]
+}
+
// Check achievements based on message content
pub fn check_message_achievements(
message: &str,
@@ -550,6 +1719,140 @@ pub fn check_message_achievements(
newly_unlocked.push(AchievementId::Debugger);
}
+ // Extended greetings
+ if (message_lower.contains("hello hikari") || message_lower.contains("hi hikari"))
+ && progress.unlock(AchievementId::HelloHikari)
+ {
+ newly_unlocked.push(AchievementId::HelloHikari);
+ }
+ if message_lower.contains("how are you") && progress.unlock(AchievementId::HowAreYou) {
+ newly_unlocked.push(AchievementId::HowAreYou);
+ }
+ if message_lower.contains("missed you") && progress.unlock(AchievementId::MissedYou) {
+ newly_unlocked.push(AchievementId::MissedYou);
+ }
+ if (message_lower.contains("i'm back") || message_lower.contains("back again"))
+ && progress.unlock(AchievementId::BackAgain)
+ {
+ newly_unlocked.push(AchievementId::BackAgain);
+ }
+
+ // Emotional achievements
+ if (message_lower.contains("frustrated")
+ || message_lower.contains("ugh")
+ || message_lower.contains("argh"))
+ && progress.unlock(AchievementId::Frustrated)
+ {
+ newly_unlocked.push(AchievementId::Frustrated);
+ }
+ if (message_lower.contains("excited")
+ || message_lower.contains("yay")
+ || message_lower.contains("woohoo"))
+ && progress.unlock(AchievementId::Excited)
+ {
+ newly_unlocked.push(AchievementId::Excited);
+ }
+ if (message_lower.contains("confused") || message_lower.contains("don't understand"))
+ && progress.unlock(AchievementId::Confused)
+ {
+ newly_unlocked.push(AchievementId::Confused);
+ }
+ if (message_lower.contains("why ") || message_lower.contains("how does"))
+ && progress.unlock(AchievementId::Curious)
+ {
+ newly_unlocked.push(AchievementId::Curious);
+ }
+ if (message_lower.contains("wow")
+ || message_lower.contains("amazing")
+ || message_lower.contains("incredible"))
+ && progress.unlock(AchievementId::Impressed)
+ {
+ newly_unlocked.push(AchievementId::Impressed);
+ }
+
+ // Message content achievements
+ if message.len() > 500 && progress.unlock(AchievementId::LongMessage) {
+ newly_unlocked.push(AchievementId::LongMessage);
+ }
+ if message.len() > 2000 && progress.unlock(AchievementId::NovelWriter) {
+ newly_unlocked.push(AchievementId::NovelWriter);
+ }
+ // Code in message (triple backticks)
+ if message.contains("```") && progress.unlock(AchievementId::CodeInMessage) {
+ newly_unlocked.push(AchievementId::CodeInMessage);
+ }
+ // Markdown formatting
+ if (message.contains("**")
+ || message.contains("__")
+ || message.contains("##")
+ || message.contains("- ")
+ || message.contains("1. "))
+ && progress.unlock(AchievementId::MarkdownMaster)
+ {
+ newly_unlocked.push(AchievementId::MarkdownMaster);
+ }
+
+ // Refactoring keywords
+ if message_lower.contains("refactor") && progress.unlock(AchievementId::CleanCoder) {
+ newly_unlocked.push(AchievementId::CleanCoder);
+ }
+ if (message_lower.contains("optimize") || message_lower.contains("performance"))
+ && progress.unlock(AchievementId::Optimizer)
+ {
+ newly_unlocked.push(AchievementId::Optimizer);
+ }
+ if message_lower.contains("simplify") && progress.unlock(AchievementId::Simplifier) {
+ newly_unlocked.push(AchievementId::Simplifier);
+ }
+
+ // Testing keywords
+ if message_lower.contains("test") && progress.unlock(AchievementId::TestWriter) {
+ newly_unlocked.push(AchievementId::TestWriter);
+ }
+ if message_lower.contains("coverage") && progress.unlock(AchievementId::CoverageKing) {
+ newly_unlocked.push(AchievementId::CoverageKing);
+ }
+
+ // Documentation keywords
+ if (message_lower.contains("document") || message_lower.contains("documentation"))
+ && progress.unlock(AchievementId::Documenter)
+ {
+ newly_unlocked.push(AchievementId::Documenter);
+ }
+ if message_lower.contains("comment") && progress.unlock(AchievementId::CommentWriter) {
+ newly_unlocked.push(AchievementId::CommentWriter);
+ }
+ if message_lower.contains("readme") && progress.unlock(AchievementId::ReadmeHero) {
+ newly_unlocked.push(AchievementId::ReadmeHero);
+ }
+
+ // API & Integration keywords
+ if message_lower.contains("api") && progress.unlock(AchievementId::ApiExplorer) {
+ newly_unlocked.push(AchievementId::ApiExplorer);
+ }
+ if (message_lower.contains("database")
+ || message_lower.contains("sql")
+ || message_lower.contains("postgres")
+ || message_lower.contains("mysql"))
+ && progress.unlock(AchievementId::DatabaseDev)
+ {
+ newly_unlocked.push(AchievementId::DatabaseDev);
+ }
+ if (message_lower.contains("cloud")
+ || message_lower.contains("aws")
+ || message_lower.contains("azure")
+ || message_lower.contains("gcp"))
+ && progress.unlock(AchievementId::CloudCoder)
+ {
+ newly_unlocked.push(AchievementId::CloudCoder);
+ }
+
+ // Git keywords
+ if message_lower.contains("merge conflict") && progress.unlock(AchievementId::ConflictResolver)
+ {
+ newly_unlocked.push(AchievementId::ConflictResolver);
+ }
+
newly_unlocked
}
@@ -709,6 +2012,85 @@ pub fn check_achievements(
}
}
+ // Extended token milestones
+ if total_tokens >= 10_000_000 && progress.unlock(AchievementId::TokenBillionaire) {
+ newly_unlocked.push(AchievementId::TokenBillionaire);
+ }
+ if total_tokens >= 50_000_000 && progress.unlock(AchievementId::TokenTreasure) {
+ newly_unlocked.push(AchievementId::TokenTreasure);
+ }
+
+ // Extended code generation
+ if stats.code_blocks_generated >= 5000 && progress.unlock(AchievementId::CodeFactory) {
+ newly_unlocked.push(AchievementId::CodeFactory);
+ }
+ if stats.code_blocks_generated >= 10000 && progress.unlock(AchievementId::CodeEmpire) {
+ newly_unlocked.push(AchievementId::CodeEmpire);
+ }
+
+ // Extended file operations
+ if total_files >= 500 && progress.unlock(AchievementId::FileEngineer) {
+ newly_unlocked.push(AchievementId::FileEngineer);
+ }
+ if total_files >= 1000 && progress.unlock(AchievementId::FileLegend) {
+ newly_unlocked.push(AchievementId::FileLegend);
+ }
+
+ // Extended conversation milestones
+ if stats.messages_exchanged >= 5000 && progress.unlock(AchievementId::ChatMarathon) {
+ newly_unlocked.push(AchievementId::ChatMarathon);
+ }
+ if stats.messages_exchanged >= 10000 && progress.unlock(AchievementId::ChatLegend) {
+ newly_unlocked.push(AchievementId::ChatLegend);
+ }
+
+ // Extended session duration achievements
+ if session_secs >= 28800 && progress.unlock(AchievementId::UltraMarathon) {
+ // 8 hours
+ newly_unlocked.push(AchievementId::UltraMarathon);
+ }
+ if session_secs >= 43200 && progress.unlock(AchievementId::CodingRetreat) {
+ // 12 hours
+ newly_unlocked.push(AchievementId::CodingRetreat);
+ }
+
+ // More tool mastery achievements
+ if let Some(edit_count) = stats.tools_usage.get("Edit") {
+ if *edit_count >= 100 && progress.unlock(AchievementId::EditMaster) {
+ newly_unlocked.push(AchievementId::EditMaster);
+ }
+ }
+ if let Some(write_count) = stats.tools_usage.get("Write") {
+ if *write_count >= 50 && progress.unlock(AchievementId::WriteMaster) {
+ newly_unlocked.push(AchievementId::WriteMaster);
+ }
+ }
+ if let Some(glob_count) = stats.tools_usage.get("Glob") {
+ if *glob_count >= 100 && progress.unlock(AchievementId::GlobMaster) {
+ newly_unlocked.push(AchievementId::GlobMaster);
+ }
+ }
+ if let Some(task_count) = stats.tools_usage.get("Task") {
+ if *task_count >= 50 && progress.unlock(AchievementId::TaskMaster) {
+ newly_unlocked.push(AchievementId::TaskMaster);
+ }
+ }
+ if let Some(web_count) = stats.tools_usage.get("WebFetch") {
+ if *web_count >= 20 && progress.unlock(AchievementId::WebFetcher) {
+ newly_unlocked.push(AchievementId::WebFetcher);
+ }
+ }
+ // MCP tools - count tools that contain "mcp__"
+ let mcp_count: u64 = stats
+ .tools_usage
+ .iter()
+ .filter(|(name, _)| name.starts_with("mcp__"))
+ .map(|(_, count)| count)
+ .sum();
+ if mcp_count >= 50 && progress.unlock(AchievementId::McpExplorer) {
+ newly_unlocked.push(AchievementId::McpExplorer);
+ }
+
// Time-based achievements
if let Some(session_start) = progress.session_start {
let hour = session_start.hour();
@@ -737,10 +2119,111 @@ pub fn check_achievements(
{
newly_unlocked.push(AchievementId::WeekendWarrior);
}
+
+ // Day-specific achievements
+ if weekday == Weekday::Mon && progress.unlock(AchievementId::MondayMotivation) {
+ newly_unlocked.push(AchievementId::MondayMotivation);
+ }
+ if weekday == Weekday::Wed && progress.unlock(AchievementId::HumpDay) {
+ newly_unlocked.push(AchievementId::HumpDay);
+ }
+ if weekday == Weekday::Fri && progress.unlock(AchievementId::FridayFinisher) {
+ newly_unlocked.push(AchievementId::FridayFinisher);
+ }
+
+ // Time of day achievements
+ if (12..=13).contains(&hour) && progress.unlock(AchievementId::LunchBreakCoder) {
+ newly_unlocked.push(AchievementId::LunchBreakCoder);
+ }
+ if (15..=16).contains(&hour) && progress.unlock(AchievementId::CoffeeTime) {
+ newly_unlocked.push(AchievementId::CoffeeTime);
+ }
+
+ // Seasonal/special day achievements
+ let now = Utc::now();
+ let month = now.month();
+ let day = now.day();
+
+ // New Year's Day (January 1st)
+ if month == 1 && day == 1 && progress.unlock(AchievementId::NewYearCoder) {
+ newly_unlocked.push(AchievementId::NewYearCoder);
+ }
+ // Valentine's Day (February 14th)
+ if month == 2 && day == 14 && progress.unlock(AchievementId::ValentinesDev) {
+ newly_unlocked.push(AchievementId::ValentinesDev);
+ }
+ // Leap Day (February 29th)
+ if month == 2 && day == 29 && progress.unlock(AchievementId::LeapDayCoder) {
+ newly_unlocked.push(AchievementId::LeapDayCoder);
+ }
+ // Halloween (October 31st)
+ if month == 10 && day == 31 && progress.unlock(AchievementId::SpookyCode) {
+ newly_unlocked.push(AchievementId::SpookyCode);
+ }
+ // Christmas (December 25th)
+ if month == 12 && day == 25 && progress.unlock(AchievementId::HolidayCoder) {
+ newly_unlocked.push(AchievementId::HolidayCoder);
+ }
}
- // Dedicated Developer - need to track consecutive days
- // TODO: Implement 30 days in a row tracking
+ // Session count achievements (from stats)
+ if stats.sessions_started >= 100 && progress.unlock(AchievementId::CenturyClub) {
+ newly_unlocked.push(AchievementId::CenturyClub);
+ }
+ if stats.sessions_started >= 1000 && progress.unlock(AchievementId::ThousandSessions) {
+ newly_unlocked.push(AchievementId::ThousandSessions);
+ }
+
+ // Daily streaks (need to check stats for consecutive days)
+ if stats.consecutive_days >= 7 && progress.unlock(AchievementId::WeekStreak) {
+ newly_unlocked.push(AchievementId::WeekStreak);
+ }
+ if stats.consecutive_days >= 14 && progress.unlock(AchievementId::TwoWeekStreak) {
+ newly_unlocked.push(AchievementId::TwoWeekStreak);
+ }
+ if stats.consecutive_days >= 30 && progress.unlock(AchievementId::DedicatedDeveloper) {
+ newly_unlocked.push(AchievementId::DedicatedDeveloper);
+ }
+ if stats.consecutive_days >= 30 && progress.unlock(AchievementId::MonthStreak) {
+ newly_unlocked.push(AchievementId::MonthStreak);
+ }
+ if stats.consecutive_days >= 90 && progress.unlock(AchievementId::QuarterStreak) {
+ newly_unlocked.push(AchievementId::QuarterStreak);
+ }
+
+ // Total days used (Veteran/OldTimer/Loyalist)
+ if stats.total_days_used >= 30 && progress.unlock(AchievementId::Veteran) {
+ newly_unlocked.push(AchievementId::Veteran);
+ }
+ if stats.total_days_used >= 90 && progress.unlock(AchievementId::OldTimer) {
+ newly_unlocked.push(AchievementId::OldTimer);
+ }
+ if stats.total_days_used >= 365 && progress.unlock(AchievementId::Loyalist) {
+ newly_unlocked.push(AchievementId::Loyalist);
+ }
+
+ // Morning/Night person tracking (from session start times stored in stats)
+ if stats.morning_sessions >= 10 && progress.unlock(AchievementId::MorningPerson) {
+ newly_unlocked.push(AchievementId::MorningPerson);
+ }
+ if stats.night_sessions >= 10 && progress.unlock(AchievementId::NightCoder) {
+ newly_unlocked.push(AchievementId::NightCoder);
+ }
+
+ // Completion percentage achievements
+ let total_achievements = get_all_achievement_ids().len();
+ let unlocked_count = progress.unlocked.len();
+ let completion_pct = (unlocked_count * 100) / total_achievements;
+
+ if completion_pct >= 50 && progress.unlock(AchievementId::Completionist) {
+ newly_unlocked.push(AchievementId::Completionist);
+ }
+ if completion_pct >= 75 && progress.unlock(AchievementId::MasterUnlocker) {
+ newly_unlocked.push(AchievementId::MasterUnlocker);
+ }
+ if completion_pct >= 100 && progress.unlock(AchievementId::PlatinumStatus) {
+ newly_unlocked.push(AchievementId::PlatinumStatus);
+ }
newly_unlocked
}
diff --git a/src-tauri/src/clipboard.rs b/src-tauri/src/clipboard.rs
new file mode 100644
index 0000000..058d9ac
--- /dev/null
+++ b/src-tauri/src/clipboard.rs
@@ -0,0 +1,259 @@
+// Clipboard history module for tracking and managing copied code snippets
+// Implements issue #25 - Clipboard History feature
+
+use serde::{Deserialize, Serialize};
+use std::sync::Mutex;
+use tauri_plugin_store::StoreExt;
+use uuid::Uuid;
+
+const STORE_FILE: &str = "hikari-clipboard.json";
+const HISTORY_KEY: &str = "clipboard_history";
+const MAX_HISTORY_SIZE: usize = 100;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ClipboardEntry {
+ pub id: String,
+ pub content: String,
+ pub language: Option,
+ pub source: Option,
+ pub timestamp: String,
+ pub is_pinned: bool,
+}
+
+impl ClipboardEntry {
+ pub fn new(content: String, language: Option, source: Option) -> Self {
+ Self {
+ id: Uuid::new_v4().to_string(),
+ content,
+ language,
+ source,
+ timestamp: chrono::Utc::now().to_rfc3339(),
+ is_pinned: false,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+struct ClipboardHistory {
+ entries: Vec,
+}
+
+// Track last clipboard content to avoid duplicates
+#[derive(Default)]
+struct ClipboardState {
+ last_content: Option,
+}
+
+static CLIPBOARD_STATE: Mutex = Mutex::new(ClipboardState { last_content: None });
+
+fn load_history(app: &tauri::AppHandle) -> ClipboardHistory {
+ let store = app.store(STORE_FILE).ok();
+ store
+ .and_then(|s| s.get(HISTORY_KEY))
+ .and_then(|v| serde_json::from_value(v.clone()).ok())
+ .unwrap_or_default()
+}
+
+fn save_history(app: &tauri::AppHandle, history: &ClipboardHistory) -> Result<(), String> {
+ let store = app.store(STORE_FILE).map_err(|e| e.to_string())?;
+ store.set(
+ HISTORY_KEY,
+ serde_json::to_value(history).map_err(|e| e.to_string())?,
+ );
+ store.save().map_err(|e| e.to_string())?;
+ Ok(())
+}
+
+/// List all clipboard entries, optionally filtered by language
+#[tauri::command]
+pub fn list_clipboard_entries(
+ app: tauri::AppHandle,
+ language: Option,
+) -> Result, String> {
+ let history = load_history(&app);
+ let entries = if let Some(lang) = language {
+ history
+ .entries
+ .into_iter()
+ .filter(|e| e.language.as_ref() == Some(&lang))
+ .collect()
+ } else {
+ history.entries
+ };
+ Ok(entries)
+}
+
+/// Capture current clipboard content and add to history
+#[tauri::command]
+pub fn capture_clipboard(
+ app: tauri::AppHandle,
+ content: String,
+ language: Option,
+ source: Option,
+) -> Result {
+ // Check for duplicate (same content as last capture)
+ {
+ let mut state = CLIPBOARD_STATE.lock().map_err(|e| e.to_string())?;
+ if state.last_content.as_ref() == Some(&content) {
+ // Return existing entry if content is the same
+ let history = load_history(&app);
+ if let Some(entry) = history.entries.first() {
+ if entry.content == content {
+ return Ok(entry.clone());
+ }
+ }
+ }
+ state.last_content = Some(content.clone());
+ }
+
+ let entry = ClipboardEntry::new(content, language, source);
+ let mut history = load_history(&app);
+
+ // Add to front of history
+ history.entries.insert(0, entry.clone());
+
+ // Enforce max size (keep pinned entries)
+ let mut pinned: Vec = history
+ .entries
+ .iter()
+ .filter(|e| e.is_pinned)
+ .cloned()
+ .collect();
+ let mut unpinned: Vec = history
+ .entries
+ .into_iter()
+ .filter(|e| !e.is_pinned)
+ .collect();
+
+ // Trim unpinned entries if over max size
+ if unpinned.len() + pinned.len() > MAX_HISTORY_SIZE {
+ let max_unpinned = MAX_HISTORY_SIZE.saturating_sub(pinned.len());
+ unpinned.truncate(max_unpinned);
+ }
+
+ // Merge back, pinned first then unpinned
+ pinned.extend(unpinned);
+ history.entries = pinned;
+
+ // Sort by timestamp descending (newest first), pinned entries stay at top
+ history.entries.sort_by(|a, b| {
+ if a.is_pinned && !b.is_pinned {
+ std::cmp::Ordering::Less
+ } else if !a.is_pinned && b.is_pinned {
+ std::cmp::Ordering::Greater
+ } else {
+ b.timestamp.cmp(&a.timestamp)
+ }
+ });
+
+ save_history(&app, &history)?;
+ Ok(entry)
+}
+
+/// Delete a clipboard entry by ID
+#[tauri::command]
+pub fn delete_clipboard_entry(app: tauri::AppHandle, id: String) -> Result<(), String> {
+ let mut history = load_history(&app);
+ history.entries.retain(|e| e.id != id);
+ save_history(&app, &history)?;
+ Ok(())
+}
+
+/// Toggle pin status of an entry
+#[tauri::command]
+pub fn toggle_pin_clipboard_entry(
+ app: tauri::AppHandle,
+ id: String,
+) -> Result {
+ let mut history = load_history(&app);
+ let entry = history
+ .entries
+ .iter_mut()
+ .find(|e| e.id == id)
+ .ok_or("Entry not found")?;
+
+ entry.is_pinned = !entry.is_pinned;
+ let updated_entry = entry.clone();
+
+ // Re-sort to move pinned entries to top
+ history.entries.sort_by(|a, b| {
+ if a.is_pinned && !b.is_pinned {
+ std::cmp::Ordering::Less
+ } else if !a.is_pinned && b.is_pinned {
+ std::cmp::Ordering::Greater
+ } else {
+ b.timestamp.cmp(&a.timestamp)
+ }
+ });
+
+ save_history(&app, &history)?;
+ Ok(updated_entry)
+}
+
+/// Clear all non-pinned entries
+#[tauri::command]
+pub fn clear_clipboard_history(app: tauri::AppHandle) -> Result<(), String> {
+ let mut history = load_history(&app);
+ history.entries.retain(|e| e.is_pinned);
+ save_history(&app, &history)?;
+ Ok(())
+}
+
+/// Search clipboard entries by content
+#[tauri::command]
+pub fn search_clipboard_entries(
+ app: tauri::AppHandle,
+ query: String,
+) -> Result, String> {
+ let history = load_history(&app);
+ let query_lower = query.to_lowercase();
+ let entries = history
+ .entries
+ .into_iter()
+ .filter(|e| {
+ e.content.to_lowercase().contains(&query_lower)
+ || e.language
+ .as_ref()
+ .is_some_and(|l| l.to_lowercase().contains(&query_lower))
+ || e.source
+ .as_ref()
+ .is_some_and(|s| s.to_lowercase().contains(&query_lower))
+ })
+ .collect();
+ Ok(entries)
+}
+
+/// Get all unique languages from history
+#[tauri::command]
+pub fn get_clipboard_languages(app: tauri::AppHandle) -> Result, String> {
+ let history = load_history(&app);
+ let mut languages: Vec = history
+ .entries
+ .iter()
+ .filter_map(|e| e.language.clone())
+ .collect();
+ languages.sort();
+ languages.dedup();
+ Ok(languages)
+}
+
+/// Update the language of an entry
+#[tauri::command]
+pub fn update_clipboard_language(
+ app: tauri::AppHandle,
+ id: String,
+ language: Option,
+) -> Result {
+ let mut history = load_history(&app);
+ let entry = history
+ .entries
+ .iter_mut()
+ .find(|e| e.id == id)
+ .ok_or("Entry not found")?;
+
+ entry.language = language;
+ let updated_entry = entry.clone();
+
+ save_history(&app, &history)?;
+ Ok(updated_entry)
+}
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index 75373db..d9845dd 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -102,6 +102,19 @@ pub async fn get_usage_stats(
manager.get_usage_stats(&conversation_id)
}
+/// Load persisted lifetime stats from store (no bridge required)
+#[tauri::command]
+pub async fn get_persisted_stats(app: AppHandle) -> Result {
+ let mut stats = UsageStats::new();
+
+ // Load persisted stats if available
+ if let Some(persisted) = crate::stats::load_stats(&app).await {
+ stats.apply_persisted(persisted);
+ }
+
+ Ok(stats)
+}
+
#[tauri::command]
pub async fn validate_directory(
path: String,
diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs
index d6f6fae..6ca733e 100644
--- a/src-tauri/src/config.rs
+++ b/src-tauri/src/config.rs
@@ -70,6 +70,32 @@ pub struct HikariConfig {
#[serde(default = "default_font_size")]
pub font_size: u32,
+
+ #[serde(default)]
+ pub minimize_to_tray: bool,
+
+ #[serde(default)]
+ pub streamer_mode: bool,
+
+ #[serde(default)]
+ pub streamer_hide_paths: bool,
+
+ #[serde(default)]
+ pub compact_mode: bool,
+
+ // Profile fields
+ #[serde(default)]
+ pub profile_name: Option,
+
+ #[serde(default)]
+ pub profile_avatar_path: Option,
+
+ #[serde(default)]
+ pub profile_bio: Option,
+
+ // Custom theme colors
+ #[serde(default)]
+ pub custom_theme_colors: CustomThemeColors,
}
impl Default for HikariConfig {
@@ -89,6 +115,14 @@ impl Default for HikariConfig {
update_checks_enabled: true,
character_panel_width: None,
font_size: 14,
+ minimize_to_tray: false,
+ streamer_mode: false,
+ streamer_hide_paths: false,
+ compact_mode: false,
+ profile_name: None,
+ profile_avatar_path: None,
+ profile_bio: None,
+ custom_theme_colors: CustomThemeColors::default(),
}
}
}
@@ -119,6 +153,29 @@ pub enum Theme {
#[default]
Dark,
Light,
+ #[serde(rename = "high-contrast")]
+ HighContrast,
+ Custom,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
+pub struct CustomThemeColors {
+ #[serde(default)]
+ pub bg_primary: Option,
+ #[serde(default)]
+ pub bg_secondary: Option,
+ #[serde(default)]
+ pub bg_terminal: Option,
+ #[serde(default)]
+ pub accent_primary: Option,
+ #[serde(default)]
+ pub accent_secondary: Option,
+ #[serde(default)]
+ pub text_primary: Option,
+ #[serde(default)]
+ pub text_secondary: Option,
+ #[serde(default)]
+ pub border_color: Option,
}
#[cfg(test)]
@@ -140,6 +197,14 @@ mod tests {
assert!(config.update_checks_enabled);
assert!(config.character_panel_width.is_none());
assert_eq!(config.font_size, 14);
+ assert!(!config.minimize_to_tray);
+ assert!(!config.streamer_mode);
+ assert!(!config.streamer_hide_paths);
+ assert!(!config.compact_mode);
+ assert!(config.profile_name.is_none());
+ assert!(config.profile_avatar_path.is_none());
+ assert!(config.profile_bio.is_none());
+ assert_eq!(config.custom_theme_colors, CustomThemeColors::default());
}
#[test]
@@ -159,6 +224,14 @@ mod tests {
update_checks_enabled: true,
character_panel_width: Some(400),
font_size: 16,
+ minimize_to_tray: true,
+ streamer_mode: false,
+ streamer_hide_paths: false,
+ compact_mode: false,
+ profile_name: Some("Test User".to_string()),
+ profile_avatar_path: None,
+ profile_bio: Some("A test bio".to_string()),
+ custom_theme_colors: CustomThemeColors::default(),
};
let json = serde_json::to_string(&config).unwrap();
@@ -179,8 +252,16 @@ mod tests {
fn test_theme_serialization() {
let dark = Theme::Dark;
let light = Theme::Light;
+ let high_contrast = Theme::HighContrast;
assert_eq!(serde_json::to_string(&dark).unwrap(), "\"dark\"");
assert_eq!(serde_json::to_string(&light).unwrap(), "\"light\"");
+ assert_eq!(
+ serde_json::to_string(&high_contrast).unwrap(),
+ "\"high-contrast\""
+ );
+
+ let custom = Theme::Custom;
+ assert_eq!(serde_json::to_string(&custom).unwrap(), "\"custom\"");
}
}
diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs
new file mode 100644
index 0000000..96ea1a4
--- /dev/null
+++ b/src-tauri/src/git.rs
@@ -0,0 +1,288 @@
+use serde::{Deserialize, Serialize};
+use std::process::Command;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GitStatus {
+ pub is_repo: bool,
+ pub branch: Option,
+ pub upstream: Option,
+ pub ahead: u32,
+ pub behind: u32,
+ pub staged: Vec,
+ pub unstaged: Vec,
+ pub untracked: Vec,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GitFileChange {
+ pub path: String,
+ pub status: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GitBranch {
+ pub name: String,
+ pub is_current: bool,
+ pub is_remote: bool,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GitLogEntry {
+ pub hash: String,
+ pub short_hash: String,
+ pub author: String,
+ pub date: String,
+ pub message: String,
+}
+
+fn run_git_command(working_dir: &str, args: &[&str]) -> Result {
+ let output = Command::new("git")
+ .args(args)
+ .current_dir(working_dir)
+ .output()
+ .map_err(|e| format!("Failed to execute git: {}", e))?;
+
+ if output.status.success() {
+ Ok(String::from_utf8_lossy(&output.stdout).to_string())
+ } else {
+ Err(String::from_utf8_lossy(&output.stderr).to_string())
+ }
+}
+
+#[tauri::command]
+pub fn git_status(working_dir: String) -> Result {
+ // Check if it's a git repo
+ let is_repo = run_git_command(&working_dir, &["rev-parse", "--git-dir"]).is_ok();
+
+ if !is_repo {
+ return Ok(GitStatus {
+ is_repo: false,
+ branch: None,
+ upstream: None,
+ ahead: 0,
+ behind: 0,
+ staged: vec![],
+ unstaged: vec![],
+ untracked: vec![],
+ });
+ }
+
+ // Get current branch
+ let branch = run_git_command(&working_dir, &["rev-parse", "--abbrev-ref", "HEAD"])
+ .ok()
+ .map(|s| s.trim().to_string());
+
+ // Get upstream branch
+ let upstream = run_git_command(
+ &working_dir,
+ &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
+ )
+ .ok()
+ .map(|s| s.trim().to_string());
+
+ // Get ahead/behind counts
+ let (ahead, behind) = if upstream.is_some() {
+ let rev_list =
+ run_git_command(&working_dir, &["rev-list", "--left-right", "--count", "@{u}...HEAD"])
+ .unwrap_or_default();
+ let parts: Vec<&str> = rev_list.trim().split('\t').collect();
+ if parts.len() == 2 {
+ (
+ parts[1].parse().unwrap_or(0),
+ parts[0].parse().unwrap_or(0),
+ )
+ } else {
+ (0, 0)
+ }
+ } else {
+ (0, 0)
+ };
+
+ // Get status with porcelain format
+ let status_output =
+ run_git_command(&working_dir, &["status", "--porcelain=v1"]).unwrap_or_default();
+
+ let mut staged = vec![];
+ let mut unstaged = vec![];
+ let mut untracked = vec![];
+
+ for line in status_output.lines() {
+ if line.len() < 3 {
+ continue;
+ }
+
+ let index_status = line.chars().next().unwrap_or(' ');
+ let worktree_status = line.chars().nth(1).unwrap_or(' ');
+ let path = line[3..].to_string();
+
+ // Untracked files
+ if index_status == '?' && worktree_status == '?' {
+ untracked.push(path);
+ continue;
+ }
+
+ // Staged changes (index status)
+ if index_status != ' ' && index_status != '?' {
+ staged.push(GitFileChange {
+ path: path.clone(),
+ status: match index_status {
+ 'M' => "modified".to_string(),
+ 'A' => "added".to_string(),
+ 'D' => "deleted".to_string(),
+ 'R' => "renamed".to_string(),
+ 'C' => "copied".to_string(),
+ _ => "unknown".to_string(),
+ },
+ });
+ }
+
+ // Unstaged changes (worktree status)
+ if worktree_status != ' ' && worktree_status != '?' {
+ unstaged.push(GitFileChange {
+ path,
+ status: match worktree_status {
+ 'M' => "modified".to_string(),
+ 'D' => "deleted".to_string(),
+ _ => "unknown".to_string(),
+ },
+ });
+ }
+ }
+
+ Ok(GitStatus {
+ is_repo: true,
+ branch,
+ upstream,
+ ahead,
+ behind,
+ staged,
+ unstaged,
+ untracked,
+ })
+}
+
+#[tauri::command]
+pub fn git_diff(working_dir: String, file_path: Option, staged: bool) -> Result {
+ let mut args = vec!["diff"];
+
+ if staged {
+ args.push("--cached");
+ }
+
+ if let Some(ref path) = file_path {
+ args.push("--");
+ args.push(path);
+ }
+
+ run_git_command(&working_dir, &args)
+}
+
+#[tauri::command]
+pub fn git_branches(working_dir: String) -> Result, String> {
+ let output = run_git_command(&working_dir, &["branch", "-a", "--format=%(refname:short)\t%(HEAD)"])?;
+
+ let branches: Vec = output
+ .lines()
+ .filter_map(|line| {
+ let parts: Vec<&str> = line.split('\t').collect();
+ if parts.is_empty() {
+ return None;
+ }
+
+ let name = parts[0].to_string();
+ let is_current = parts.get(1).map(|s| *s == "*").unwrap_or(false);
+ let is_remote = name.starts_with("remotes/") || name.starts_with("origin/");
+
+ Some(GitBranch {
+ name,
+ is_current,
+ is_remote,
+ })
+ })
+ .collect();
+
+ Ok(branches)
+}
+
+#[tauri::command]
+pub fn git_checkout(working_dir: String, branch: String) -> Result {
+ run_git_command(&working_dir, &["checkout", &branch])
+}
+
+#[tauri::command]
+pub fn git_stage(working_dir: String, file_path: String) -> Result {
+ run_git_command(&working_dir, &["add", &file_path])
+}
+
+#[tauri::command]
+pub fn git_unstage(working_dir: String, file_path: String) -> Result {
+ run_git_command(&working_dir, &["restore", "--staged", &file_path])
+}
+
+#[tauri::command]
+pub fn git_stage_all(working_dir: String) -> Result {
+ run_git_command(&working_dir, &["add", "-A"])
+}
+
+#[tauri::command]
+pub fn git_commit(working_dir: String, message: String) -> Result {
+ run_git_command(&working_dir, &["commit", "-m", &message])
+}
+
+#[tauri::command]
+pub fn git_push(working_dir: String) -> Result {
+ run_git_command(&working_dir, &["push"])
+}
+
+#[tauri::command]
+pub fn git_pull(working_dir: String) -> Result {
+ run_git_command(&working_dir, &["pull"])
+}
+
+#[tauri::command]
+pub fn git_fetch(working_dir: String) -> Result {
+ run_git_command(&working_dir, &["fetch", "--all"])
+}
+
+#[tauri::command]
+pub fn git_log(working_dir: String, limit: Option) -> Result, String> {
+ let limit_str = limit.unwrap_or(10).to_string();
+ let output = run_git_command(
+ &working_dir,
+ &[
+ "log",
+ &format!("-{}", limit_str),
+ "--pretty=format:%H\t%h\t%an\t%ar\t%s",
+ ],
+ )?;
+
+ let entries: Vec = output
+ .lines()
+ .filter_map(|line| {
+ let parts: Vec<&str> = line.split('\t').collect();
+ if parts.len() < 5 {
+ return None;
+ }
+
+ Some(GitLogEntry {
+ hash: parts[0].to_string(),
+ short_hash: parts[1].to_string(),
+ author: parts[2].to_string(),
+ date: parts[3].to_string(),
+ message: parts[4..].join("\t"),
+ })
+ })
+ .collect();
+
+ Ok(entries)
+}
+
+#[tauri::command]
+pub fn git_discard(working_dir: String, file_path: String) -> Result {
+ run_git_command(&working_dir, &["checkout", "--", &file_path])
+}
+
+#[tauri::command]
+pub fn git_create_branch(working_dir: String, branch_name: String) -> Result {
+ run_git_command(&working_dir, &["checkout", "-b", &branch_name])
+}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index f635d4d..0dc2487 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,10 +1,16 @@
mod achievements;
mod bridge_manager;
+mod clipboard;
mod commands;
mod config;
+mod git;
mod notifications;
+mod quick_actions;
+mod sessions;
+mod snippets;
mod stats;
mod temp_manager;
+mod tray;
mod types;
mod vbs_notification;
mod windows_toast;
@@ -12,10 +18,17 @@ mod wsl_bridge;
mod wsl_notifications;
use bridge_manager::create_shared_bridge_manager;
+use clipboard::*;
use commands::load_saved_achievements;
use commands::*;
+use git::*;
use notifications::*;
+use quick_actions::*;
+use sessions::*;
+use snippets::*;
+use tauri::Manager;
use temp_manager::create_shared_temp_manager;
+use tray::{setup_tray, should_minimize_to_tray};
use vbs_notification::*;
use windows_toast::*;
use wsl_notifications::*;
@@ -34,6 +47,7 @@ pub fn run() {
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_clipboard_manager::init())
+ .plugin(tauri_plugin_fs::init())
.manage(bridge_manager.clone())
.manage(temp_manager.clone())
.setup(move |app| {
@@ -47,6 +61,27 @@ pub fn run() {
}
}
+ // Set up system tray
+ if let Err(e) = setup_tray(app.handle()) {
+ eprintln!("Failed to set up system tray: {}", e);
+ }
+
+ // Handle window close event for minimize to tray
+ let main_window = app.get_webview_window("main").unwrap();
+ main_window.on_window_event({
+ let app_handle = app.handle().clone();
+ move |event| {
+ if let tauri::WindowEvent::CloseRequested { api, .. } = event {
+ if should_minimize_to_tray(&app_handle) {
+ api.prevent_close();
+ if let Some(window) = app_handle.get_webview_window("main") {
+ let _ = window.hide();
+ }
+ }
+ }
+ }
+ });
+
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -60,6 +95,7 @@ pub fn run() {
get_config,
save_config,
get_usage_stats,
+ get_persisted_stats,
load_saved_achievements,
answer_question,
send_windows_notification,
@@ -78,6 +114,43 @@ pub fn run() {
cleanup_all_temp_files,
cleanup_orphaned_temp_files,
get_file_size,
+ list_sessions,
+ save_session,
+ load_session,
+ delete_session,
+ search_sessions,
+ clear_all_sessions,
+ list_snippets,
+ save_snippet,
+ delete_snippet,
+ get_snippet_categories,
+ reset_default_snippets,
+ list_quick_actions,
+ save_quick_action,
+ delete_quick_action,
+ reset_default_quick_actions,
+ git_status,
+ git_diff,
+ git_branches,
+ git_checkout,
+ git_stage,
+ git_unstage,
+ git_stage_all,
+ git_commit,
+ git_push,
+ git_pull,
+ git_fetch,
+ git_log,
+ git_discard,
+ git_create_branch,
+ list_clipboard_entries,
+ capture_clipboard,
+ delete_clipboard_entry,
+ toggle_pin_clipboard_entry,
+ clear_clipboard_history,
+ search_clipboard_entries,
+ get_clipboard_languages,
+ update_clipboard_language,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
diff --git a/src-tauri/src/quick_actions.rs b/src-tauri/src/quick_actions.rs
new file mode 100644
index 0000000..92be5c5
--- /dev/null
+++ b/src-tauri/src/quick_actions.rs
@@ -0,0 +1,191 @@
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+use tauri::AppHandle;
+use tauri_plugin_store::StoreExt;
+
+const QUICK_ACTIONS_STORE_KEY: &str = "quick_actions";
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct QuickAction {
+ pub id: String,
+ pub name: String,
+ pub prompt: String,
+ pub icon: String,
+ pub is_default: bool,
+ pub created_at: DateTime,
+ pub updated_at: DateTime,
+}
+
+fn get_default_quick_actions() -> Vec {
+ let now = Utc::now();
+ vec![
+ QuickAction {
+ id: "default-review-pr".to_string(),
+ name: "Review PR".to_string(),
+ prompt: "Please review this pull request and provide feedback on code quality, potential issues, and suggestions for improvement.".to_string(),
+ icon: "git-pull-request".to_string(),
+ is_default: true,
+ created_at: now,
+ updated_at: now,
+ },
+ QuickAction {
+ id: "default-run-tests".to_string(),
+ name: "Run Tests".to_string(),
+ prompt: "Please run the test suite for this project and report any failures or issues.".to_string(),
+ icon: "play".to_string(),
+ is_default: true,
+ created_at: now,
+ updated_at: now,
+ },
+ QuickAction {
+ id: "default-explain-file".to_string(),
+ name: "Explain File".to_string(),
+ prompt: "Please explain what this file does, its purpose, and how it fits into the overall project structure.".to_string(),
+ icon: "file-text".to_string(),
+ is_default: true,
+ created_at: now,
+ updated_at: now,
+ },
+ QuickAction {
+ id: "default-fix-error".to_string(),
+ name: "Fix Error".to_string(),
+ prompt: "I'm getting an error. Can you help me identify the cause and fix it?".to_string(),
+ icon: "alert-circle".to_string(),
+ is_default: true,
+ created_at: now,
+ updated_at: now,
+ },
+ QuickAction {
+ id: "default-write-tests".to_string(),
+ name: "Write Tests".to_string(),
+ prompt: "Please write comprehensive unit tests for the current code with good coverage.".to_string(),
+ icon: "check-square".to_string(),
+ is_default: true,
+ created_at: now,
+ updated_at: now,
+ },
+ QuickAction {
+ id: "default-refactor".to_string(),
+ name: "Refactor".to_string(),
+ prompt: "Please refactor this code to improve readability, maintainability, and performance.".to_string(),
+ icon: "refresh-cw".to_string(),
+ is_default: true,
+ created_at: now,
+ updated_at: now,
+ },
+ ]
+}
+
+fn load_all_quick_actions(app: &AppHandle) -> Result, String> {
+ let store = app
+ .store("hikari-quick-actions.json")
+ .map_err(|e| e.to_string())?;
+
+ match store.get(QUICK_ACTIONS_STORE_KEY) {
+ Some(value) => {
+ let mut actions: Vec =
+ serde_json::from_value(value.clone()).map_err(|e| e.to_string())?;
+
+ let defaults = get_default_quick_actions();
+ for default in defaults {
+ if !actions.iter().any(|a| a.id == default.id) {
+ actions.push(default);
+ }
+ }
+
+ Ok(actions)
+ }
+ None => Ok(get_default_quick_actions()),
+ }
+}
+
+fn save_all_quick_actions(app: &AppHandle, actions: &[QuickAction]) -> Result<(), String> {
+ let store = app
+ .store("hikari-quick-actions.json")
+ .map_err(|e| e.to_string())?;
+
+ let value = serde_json::to_value(actions).map_err(|e| e.to_string())?;
+ store.set(QUICK_ACTIONS_STORE_KEY, value);
+ store.save().map_err(|e| e.to_string())?;
+
+ Ok(())
+}
+
+#[tauri::command]
+pub async fn list_quick_actions(app: AppHandle) -> Result, String> {
+ let mut actions = load_all_quick_actions(&app)?;
+
+ actions.sort_by(|a, b| {
+ let default_cmp = b.is_default.cmp(&a.is_default);
+ if default_cmp == std::cmp::Ordering::Equal {
+ a.name.cmp(&b.name)
+ } else {
+ default_cmp
+ }
+ });
+
+ Ok(actions)
+}
+
+#[tauri::command]
+pub async fn save_quick_action(app: AppHandle, action: QuickAction) -> Result<(), String> {
+ let mut actions = load_all_quick_actions(&app)?;
+
+ if let Some(existing) = actions.iter_mut().find(|a| a.id == action.id) {
+ let mut updated = action;
+ updated.is_default = existing.is_default;
+ *existing = updated;
+ } else {
+ actions.push(action);
+ }
+
+ save_all_quick_actions(&app, &actions)
+}
+
+#[tauri::command]
+pub async fn delete_quick_action(app: AppHandle, action_id: String) -> Result<(), String> {
+ let mut actions = load_all_quick_actions(&app)?;
+
+ if actions
+ .iter()
+ .any(|a| a.id == action_id && a.is_default)
+ {
+ return Err("Cannot delete default quick actions".to_string());
+ }
+
+ actions.retain(|a| a.id != action_id);
+ save_all_quick_actions(&app, &actions)
+}
+
+#[tauri::command]
+pub async fn reset_default_quick_actions(app: AppHandle) -> Result<(), String> {
+ let mut actions = load_all_quick_actions(&app)?;
+
+ actions.retain(|a| !a.is_default);
+ actions.extend(get_default_quick_actions());
+
+ save_all_quick_actions(&app, &actions)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_default_quick_actions_exist() {
+ let defaults = get_default_quick_actions();
+ assert!(!defaults.is_empty());
+ assert!(defaults.iter().all(|a| a.is_default));
+ }
+
+ #[test]
+ fn test_default_quick_actions_have_required_fields() {
+ let defaults = get_default_quick_actions();
+ for action in defaults {
+ assert!(!action.id.is_empty());
+ assert!(!action.name.is_empty());
+ assert!(!action.prompt.is_empty());
+ assert!(!action.icon.is_empty());
+ }
+ }
+}
diff --git a/src-tauri/src/sessions.rs b/src-tauri/src/sessions.rs
new file mode 100644
index 0000000..d8c0c54
--- /dev/null
+++ b/src-tauri/src/sessions.rs
@@ -0,0 +1,167 @@
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+use tauri::AppHandle;
+use tauri_plugin_store::StoreExt;
+
+const SESSIONS_STORE_KEY: &str = "sessions";
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SavedSession {
+ pub id: String,
+ pub name: String,
+ pub created_at: DateTime,
+ pub last_activity_at: DateTime,
+ pub working_directory: String,
+ pub message_count: usize,
+ pub preview: String, // First ~100 chars of conversation for preview
+ pub messages: Vec,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SavedMessage {
+ pub id: String,
+ #[serde(rename = "type")]
+ pub message_type: String,
+ pub content: String,
+ pub timestamp: DateTime,
+ pub tool_name: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SessionListItem {
+ pub id: String,
+ pub name: String,
+ pub created_at: DateTime,
+ pub last_activity_at: DateTime,
+ pub working_directory: String,
+ pub message_count: usize,
+ pub preview: String,
+}
+
+impl From<&SavedSession> for SessionListItem {
+ fn from(session: &SavedSession) -> Self {
+ SessionListItem {
+ id: session.id.clone(),
+ name: session.name.clone(),
+ created_at: session.created_at,
+ last_activity_at: session.last_activity_at,
+ working_directory: session.working_directory.clone(),
+ message_count: session.message_count,
+ preview: session.preview.clone(),
+ }
+ }
+}
+
+fn load_all_sessions(app: &AppHandle) -> Result, String> {
+ let store = app
+ .store("hikari-sessions.json")
+ .map_err(|e| e.to_string())?;
+
+ match store.get(SESSIONS_STORE_KEY) {
+ Some(value) => serde_json::from_value(value.clone()).map_err(|e| e.to_string()),
+ None => Ok(Vec::new()),
+ }
+}
+
+fn save_all_sessions(app: &AppHandle, sessions: &[SavedSession]) -> Result<(), String> {
+ let store = app
+ .store("hikari-sessions.json")
+ .map_err(|e| e.to_string())?;
+
+ let value = serde_json::to_value(sessions).map_err(|e| e.to_string())?;
+ store.set(SESSIONS_STORE_KEY, value);
+ store.save().map_err(|e| e.to_string())?;
+
+ Ok(())
+}
+
+#[tauri::command]
+pub async fn list_sessions(app: AppHandle) -> Result, String> {
+ let sessions = load_all_sessions(&app)?;
+ let mut items: Vec = sessions.iter().map(SessionListItem::from).collect();
+
+ // Sort by last activity, most recent first
+ items.sort_by(|a, b| b.last_activity_at.cmp(&a.last_activity_at));
+
+ Ok(items)
+}
+
+#[tauri::command]
+pub async fn save_session(app: AppHandle, session: SavedSession) -> Result<(), String> {
+ let mut sessions = load_all_sessions(&app)?;
+
+ // Update existing or add new
+ if let Some(existing) = sessions.iter_mut().find(|s| s.id == session.id) {
+ *existing = session;
+ } else {
+ sessions.push(session);
+ }
+
+ save_all_sessions(&app, &sessions)
+}
+
+#[tauri::command]
+pub async fn load_session(app: AppHandle, session_id: String) -> Result
-