generated from nhcarrigan/template
feat: add discord rich presence
This commit is contained in:
Generated
+30
-7
@@ -437,7 +437,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"fnv",
|
"fnv",
|
||||||
"uuid",
|
"uuid 1.19.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -788,6 +788,19 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "discord-rich-presence"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a75db747ecd252c01bfecaf709b07fcb4c634adf0edb5fed47bc9c3052e7076b"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"uuid 0.8.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1602,9 +1615,10 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hikari-desktop"
|
name = "hikari-desktop"
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"discord-rich-presence",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1622,7 +1636,7 @@ dependencies = [
|
|||||||
"tauri-plugin-store",
|
"tauri-plugin-store",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid 1.19.0",
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3578,7 +3592,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid 1.19.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4261,7 +4275,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"time",
|
"time",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid 1.19.0",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4557,7 +4571,7 @@ dependencies = [
|
|||||||
"toml 0.9.11+spec-1.1.0",
|
"toml 0.9.11+spec-1.1.0",
|
||||||
"url",
|
"url",
|
||||||
"urlpattern",
|
"urlpattern",
|
||||||
"uuid",
|
"uuid 1.19.0",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5099,6 +5113,15 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
@@ -6127,7 +6150,7 @@ dependencies = [
|
|||||||
"serde_repr",
|
"serde_repr",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uds_windows",
|
"uds_windows",
|
||||||
"uuid",
|
"uuid 1.19.0",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
"winnow 0.7.14",
|
"winnow 0.7.14",
|
||||||
"zbus_macros",
|
"zbus_macros",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ tauri-plugin-fs = "2"
|
|||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
semver = "1"
|
semver = "1"
|
||||||
chrono = { version = "0.4.43", features = ["serde"] }
|
chrono = { version = "0.4.43", features = ["serde"] }
|
||||||
|
discord-rich-presence = "0.2"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.62", features = [
|
windows = { version = "0.62", features = [
|
||||||
|
|||||||
@@ -647,6 +647,42 @@ async fn save_cost_history(app: &AppHandle, history: &crate::cost_tracking::Cost
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn init_discord_rpc(
|
||||||
|
discord_rpc: State<'_, std::sync::Arc<crate::discord_rpc::DiscordRpcManager>>,
|
||||||
|
session_name: String,
|
||||||
|
model: String,
|
||||||
|
started_at: i64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
discord_rpc.init(session_name, model, started_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_discord_rpc(
|
||||||
|
discord_rpc: State<'_, std::sync::Arc<crate::discord_rpc::DiscordRpcManager>>,
|
||||||
|
session_name: String,
|
||||||
|
model: String,
|
||||||
|
started_at: i64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
discord_rpc.update(session_name, model, started_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn stop_discord_rpc(
|
||||||
|
discord_rpc: State<'_, std::sync::Arc<crate::discord_rpc::DiscordRpcManager>>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
discord_rpc.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn log_discord_rpc(
|
||||||
|
discord_rpc: State<'_, std::sync::Arc<crate::discord_rpc::DiscordRpcManager>>,
|
||||||
|
message: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
discord_rpc.log(&message);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ pub struct HikariConfig {
|
|||||||
|
|
||||||
#[serde(default = "default_budget_warning_threshold")]
|
#[serde(default = "default_budget_warning_threshold")]
|
||||||
pub budget_warning_threshold: f32,
|
pub budget_warning_threshold: f32,
|
||||||
|
|
||||||
|
#[serde(default = "default_discord_rpc_enabled")]
|
||||||
|
pub discord_rpc_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HikariConfig {
|
impl Default for HikariConfig {
|
||||||
@@ -144,6 +147,7 @@ impl Default for HikariConfig {
|
|||||||
session_cost_budget: None,
|
session_cost_budget: None,
|
||||||
budget_action: BudgetAction::Warn,
|
budget_action: BudgetAction::Warn,
|
||||||
budget_warning_threshold: 0.8,
|
budget_warning_threshold: 0.8,
|
||||||
|
discord_rpc_enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,6 +180,10 @@ fn default_budget_warning_threshold() -> f32 {
|
|||||||
0.8
|
0.8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_discord_rpc_enabled() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum BudgetAction {
|
pub enum BudgetAction {
|
||||||
@@ -247,6 +255,7 @@ mod tests {
|
|||||||
assert!(config.session_cost_budget.is_none());
|
assert!(config.session_cost_budget.is_none());
|
||||||
assert_eq!(config.budget_action, BudgetAction::Warn);
|
assert_eq!(config.budget_action, BudgetAction::Warn);
|
||||||
assert!((config.budget_warning_threshold - 0.8).abs() < f32::EPSILON);
|
assert!((config.budget_warning_threshold - 0.8).abs() < f32::EPSILON);
|
||||||
|
assert!(config.discord_rpc_enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -279,6 +288,7 @@ mod tests {
|
|||||||
session_cost_budget: Some(1.50),
|
session_cost_budget: Some(1.50),
|
||||||
budget_action: BudgetAction::Block,
|
budget_action: BudgetAction::Block,
|
||||||
budget_warning_threshold: 0.75,
|
budget_warning_threshold: 0.75,
|
||||||
|
discord_rpc_enabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string(&config).unwrap();
|
let json = serde_json::to_string(&config).unwrap();
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
use discord_rich_presence::activity::{Activity, Assets, Timestamps};
|
||||||
|
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
pub struct DiscordRpcManager {
|
||||||
|
client: Arc<RwLock<Option<DiscordIpcClient>>>,
|
||||||
|
session_name: Arc<RwLock<String>>,
|
||||||
|
model: Arc<RwLock<String>>,
|
||||||
|
started_at: Arc<RwLock<i64>>,
|
||||||
|
log_path: Arc<RwLock<Option<PathBuf>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiscordRpcManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
client: Arc::new(RwLock::new(None)),
|
||||||
|
session_name: Arc::new(RwLock::new(String::new())),
|
||||||
|
model: Arc::new(RwLock::new(String::new())),
|
||||||
|
started_at: Arc::new(RwLock::new(0)),
|
||||||
|
log_path: Arc::new(RwLock::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_app_handle(&self, app_handle: &AppHandle) {
|
||||||
|
if let Ok(app_data_dir) = app_handle.path().app_data_dir() {
|
||||||
|
// Ensure the directory exists
|
||||||
|
if let Err(e) = std::fs::create_dir_all(&app_data_dir) {
|
||||||
|
eprintln!("Failed to create app data directory: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let log_path = app_data_dir.join("hikari_discord_rpc.log");
|
||||||
|
*self.log_path.write() = Some(log_path.clone());
|
||||||
|
self.log(&format!(
|
||||||
|
"Log file initialised at: {}",
|
||||||
|
log_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log(&self, message: &str) {
|
||||||
|
let log_path_guard = self.log_path.read();
|
||||||
|
let path = match log_path_guard.as_ref() {
|
||||||
|
Some(p) => p.clone(),
|
||||||
|
None => PathBuf::from("hikari_discord_rpc.log"),
|
||||||
|
};
|
||||||
|
drop(log_path_guard);
|
||||||
|
|
||||||
|
if let Ok(mut file) = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&path)
|
||||||
|
{
|
||||||
|
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||||
|
let _ = writeln!(file, "[{}] {}", timestamp, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(&self, initial_session_name: String, initial_model: String, started_at: i64) -> Result<(), String> {
|
||||||
|
self.log("Attempting to initialize Discord RPC...");
|
||||||
|
self.log(&format!("DEBUG: Application ID: 1391117878182281316"));
|
||||||
|
self.log(&format!("DEBUG: Initial session: '{}', model: '{}', timestamp: {}",
|
||||||
|
initial_session_name, initial_model, started_at));
|
||||||
|
|
||||||
|
let mut client = DiscordIpcClient::new("1391117878182281316")
|
||||||
|
.map_err(|e| {
|
||||||
|
let error_msg = format!("Failed to create Discord RPC client: {} (is Discord running?)", e);
|
||||||
|
self.log(&format!("ERROR: {}", error_msg));
|
||||||
|
error_msg
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.log("DEBUG: DiscordIpcClient created successfully");
|
||||||
|
|
||||||
|
client
|
||||||
|
.connect()
|
||||||
|
.map_err(|e| {
|
||||||
|
let error_msg = format!("Failed to connect to Discord RPC: {} (ensure Discord is running)", e);
|
||||||
|
self.log(&format!("ERROR: {}", error_msg));
|
||||||
|
error_msg
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.log("DEBUG: Connected to Discord IPC socket");
|
||||||
|
|
||||||
|
// Set initial activity immediately after connecting
|
||||||
|
self.log("DEBUG: Building initial activity...");
|
||||||
|
let state_text = format!("Model: {}", initial_model);
|
||||||
|
let assets = Assets::new()
|
||||||
|
.large_image("hikari")
|
||||||
|
.large_text("Hikari - Claude Code Assistant");
|
||||||
|
|
||||||
|
self.log(&format!("DEBUG: Assets created - large_image: 'hikari', large_text: 'Hikari - Claude Code Assistant'"));
|
||||||
|
|
||||||
|
let timestamps = Timestamps::new()
|
||||||
|
.start(started_at);
|
||||||
|
|
||||||
|
self.log(&format!("DEBUG: Timestamps created - start: {}", started_at));
|
||||||
|
|
||||||
|
let activity = Activity::new()
|
||||||
|
.details(initial_session_name.as_str())
|
||||||
|
.state(state_text.as_str())
|
||||||
|
.assets(assets)
|
||||||
|
.timestamps(timestamps);
|
||||||
|
|
||||||
|
self.log(&format!("DEBUG: Activity created - details: '{}', state: '{}'",
|
||||||
|
initial_session_name, state_text));
|
||||||
|
|
||||||
|
self.log("DEBUG: Attempting to set initial activity...");
|
||||||
|
client
|
||||||
|
.set_activity(activity)
|
||||||
|
.map_err(|e| {
|
||||||
|
let error_msg = format!("Failed to set initial Discord RPC activity: {}", e);
|
||||||
|
self.log(&format!("ERROR: {}", error_msg));
|
||||||
|
error_msg
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.log("DEBUG: Initial activity set successfully!");
|
||||||
|
|
||||||
|
// Store the client and initial state
|
||||||
|
*self.client.write() = Some(client);
|
||||||
|
*self.session_name.write() = initial_session_name.clone();
|
||||||
|
*self.model.write() = initial_model.clone();
|
||||||
|
*self.started_at.write() = started_at;
|
||||||
|
|
||||||
|
self.log(&format!("Discord RPC connected successfully with initial activity: session='{}', model='{}'",
|
||||||
|
initial_session_name, initial_model));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
&self,
|
||||||
|
session_name: String,
|
||||||
|
model: String,
|
||||||
|
started_at: i64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
self.log(&format!("DEBUG: update() called with session='{}', model='{}', timestamp={}",
|
||||||
|
session_name, model, started_at));
|
||||||
|
|
||||||
|
*self.session_name.write() = session_name.clone();
|
||||||
|
*self.model.write() = model.clone();
|
||||||
|
*self.started_at.write() = started_at;
|
||||||
|
|
||||||
|
self.log("DEBUG: State variables updated");
|
||||||
|
|
||||||
|
let mut client_guard = self.client.write();
|
||||||
|
let client = client_guard
|
||||||
|
.as_mut()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
let error_msg = "Discord RPC client not initialized".to_string();
|
||||||
|
self.log(&format!("ERROR: {}", error_msg));
|
||||||
|
error_msg
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.log("DEBUG: Client lock acquired");
|
||||||
|
|
||||||
|
let state_text = format!("Model: {}", model);
|
||||||
|
let assets = Assets::new()
|
||||||
|
.large_image("hikari")
|
||||||
|
.large_text("Hikari - Claude Code Assistant");
|
||||||
|
|
||||||
|
self.log(&format!("DEBUG: Assets created - large_image: 'hikari', large_text: 'Hikari - Claude Code Assistant'"));
|
||||||
|
|
||||||
|
let timestamps = Timestamps::new()
|
||||||
|
.start(started_at);
|
||||||
|
|
||||||
|
self.log(&format!("DEBUG: Timestamps created - start: {}", started_at));
|
||||||
|
|
||||||
|
let activity = Activity::new()
|
||||||
|
.details(session_name.as_str())
|
||||||
|
.state(state_text.as_str())
|
||||||
|
.assets(assets)
|
||||||
|
.timestamps(timestamps);
|
||||||
|
|
||||||
|
self.log(&format!("DEBUG: Activity created - details: '{}', state: '{}'",
|
||||||
|
session_name, state_text));
|
||||||
|
|
||||||
|
self.log("DEBUG: Attempting to set activity...");
|
||||||
|
client
|
||||||
|
.set_activity(activity)
|
||||||
|
.map_err(|e| {
|
||||||
|
let error_msg = format!("Failed to update Discord RPC: {}", e);
|
||||||
|
self.log(&format!("ERROR: {}", error_msg));
|
||||||
|
error_msg
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.log(&format!("Updated Discord RPC: session='{}', model='{}'", session_name, model));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) -> Result<(), String> {
|
||||||
|
self.log("DEBUG: stop() called");
|
||||||
|
|
||||||
|
let mut client_guard = self.client.write();
|
||||||
|
if let Some(mut client) = client_guard.take() {
|
||||||
|
self.log("DEBUG: Client found, attempting to close...");
|
||||||
|
client
|
||||||
|
.close()
|
||||||
|
.map_err(|e| {
|
||||||
|
let error_msg = format!("Failed to close Discord RPC: {}", e);
|
||||||
|
self.log(&format!("ERROR: {}", error_msg));
|
||||||
|
error_msg
|
||||||
|
})?;
|
||||||
|
self.log("Discord RPC stopped successfully");
|
||||||
|
} else {
|
||||||
|
self.log("DEBUG: No client to stop (already stopped or never initialized)");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DiscordRpcManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ mod clipboard;
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod cost_tracking;
|
mod cost_tracking;
|
||||||
|
mod discord_rpc;
|
||||||
mod git;
|
mod git;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
mod quick_actions;
|
mod quick_actions;
|
||||||
@@ -23,11 +24,13 @@ use bridge_manager::create_shared_bridge_manager;
|
|||||||
use clipboard::*;
|
use clipboard::*;
|
||||||
use commands::load_saved_achievements;
|
use commands::load_saved_achievements;
|
||||||
use commands::*;
|
use commands::*;
|
||||||
|
use discord_rpc::DiscordRpcManager;
|
||||||
use git::*;
|
use git::*;
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
use quick_actions::*;
|
use quick_actions::*;
|
||||||
use sessions::*;
|
use sessions::*;
|
||||||
use snippets::*;
|
use snippets::*;
|
||||||
|
use std::sync::Arc;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use temp_manager::create_shared_temp_manager;
|
use temp_manager::create_shared_temp_manager;
|
||||||
use tray::{setup_tray, should_minimize_to_tray};
|
use tray::{setup_tray, should_minimize_to_tray};
|
||||||
@@ -39,6 +42,7 @@ use wsl_notifications::*;
|
|||||||
pub fn run() {
|
pub fn run() {
|
||||||
let bridge_manager = create_shared_bridge_manager();
|
let bridge_manager = create_shared_bridge_manager();
|
||||||
let temp_manager = create_shared_temp_manager().expect("Failed to create temp file manager");
|
let temp_manager = create_shared_temp_manager().expect("Failed to create temp file manager");
|
||||||
|
let discord_rpc = Arc::new(DiscordRpcManager::new());
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
@@ -52,10 +56,14 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.manage(bridge_manager.clone())
|
.manage(bridge_manager.clone())
|
||||||
.manage(temp_manager.clone())
|
.manage(temp_manager.clone())
|
||||||
|
.manage(discord_rpc.clone())
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
// Initialize the app handle in the bridge manager
|
// Initialize the app handle in the bridge manager
|
||||||
bridge_manager.lock().set_app_handle(app.handle().clone());
|
bridge_manager.lock().set_app_handle(app.handle().clone());
|
||||||
|
|
||||||
|
// Initialize the app handle in the Discord RPC manager for logging
|
||||||
|
discord_rpc.set_app_handle(app.handle());
|
||||||
|
|
||||||
// Clean up any orphaned temp files from previous sessions
|
// Clean up any orphaned temp files from previous sessions
|
||||||
if let Ok(count) = temp_manager.lock().cleanup_orphaned_files() {
|
if let Ok(count) = temp_manager.lock().cleanup_orphaned_files() {
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
@@ -169,6 +177,10 @@ pub fn run() {
|
|||||||
get_today_cost,
|
get_today_cost,
|
||||||
get_week_cost,
|
get_week_cost,
|
||||||
get_month_cost,
|
get_month_cost,
|
||||||
|
init_discord_rpc,
|
||||||
|
update_discord_rpc,
|
||||||
|
stop_discord_rpc,
|
||||||
|
log_discord_rpc,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { get } from "svelte/store";
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { claudeStore } from "$lib/stores/claude";
|
import { claudeStore } from "$lib/stores/claude";
|
||||||
import { characterState } from "$lib/stores/character";
|
import { characterState } from "$lib/stores/character";
|
||||||
import { setSkipNextGreeting } from "$lib/tauri";
|
import { setSkipNextGreeting, updateDiscordRpc } from "$lib/tauri";
|
||||||
import { searchState } from "$lib/stores/search";
|
import { searchState } from "$lib/stores/search";
|
||||||
|
import { conversationsStore } from "$lib/stores/conversations";
|
||||||
|
import { configStore } from "$lib/stores/config";
|
||||||
|
|
||||||
export interface SlashCommand {
|
export interface SlashCommand {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -51,6 +53,17 @@ async function changeDirectory(path: string): Promise<void> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update Discord RPC when reconnecting after directory change
|
||||||
|
const config = configStore.getConfig();
|
||||||
|
const activeConversation = get(conversationsStore.activeConversation);
|
||||||
|
if (activeConversation) {
|
||||||
|
await updateDiscordRpc(
|
||||||
|
activeConversation.name,
|
||||||
|
config.model || "claude",
|
||||||
|
activeConversation.startedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for connection to establish
|
// Wait for connection to establish
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
@@ -105,6 +118,17 @@ async function startNewConversation(): Promise<void> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update Discord RPC when starting new conversation
|
||||||
|
const config = configStore.getConfig();
|
||||||
|
const activeConversation = get(conversationsStore.activeConversation);
|
||||||
|
if (activeConversation) {
|
||||||
|
await updateDiscordRpc(
|
||||||
|
activeConversation.name,
|
||||||
|
config.model || "claude",
|
||||||
|
activeConversation.startedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
claudeStore.addLine("system", "New conversation started!");
|
claudeStore.addLine("system", "New conversation started!");
|
||||||
characterState.setState("idle");
|
characterState.setState("idle");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
session_cost_budget: null,
|
session_cost_budget: null,
|
||||||
budget_action: "warn",
|
budget_action: "warn",
|
||||||
budget_warning_threshold: 0.8,
|
budget_warning_threshold: 0.8,
|
||||||
|
discord_rpc_enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let showCustomThemeEditor = $state(false);
|
let showCustomThemeEditor = $state(false);
|
||||||
@@ -967,6 +968,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Discord Rich Presence Section -->
|
||||||
|
<section class="pt-6 pb-6 border-t border-[var(--border-color)]">
|
||||||
|
<h3 class="text-lg font-semibold text-[var(--accent-primary)] mb-4 flex items-center gap-2">
|
||||||
|
<span>🎮</span>
|
||||||
|
<span>Discord Rich Presence</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Enable/Disable Discord RPC -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={config.discord_rpc_enabled}
|
||||||
|
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-[var(--text-primary)]">Show activity in Discord</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-[var(--text-tertiary)]">
|
||||||
|
Display your current conversation session name and model in Discord when enabled.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Save Button -->
|
<!-- Save Button -->
|
||||||
<div class="sticky bottom-0 pt-4 pb-2 bg-[var(--bg-secondary)]">
|
<div class="sticky bottom-0 pt-4 pb-2 bg-[var(--bg-secondary)]">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
||||||
import { characterState } from "$lib/stores/character";
|
import { characterState } from "$lib/stores/character";
|
||||||
import { handleNewUserMessage } from "$lib/notifications/rules";
|
import { handleNewUserMessage } from "$lib/notifications/rules";
|
||||||
import { setSkipNextGreeting } from "$lib/tauri";
|
import { setSkipNextGreeting, updateDiscordRpc } from "$lib/tauri";
|
||||||
import { clipboardStore } from "$lib/stores/clipboard";
|
import { clipboardStore } from "$lib/stores/clipboard";
|
||||||
import {
|
import {
|
||||||
setShouldRestoreHistory,
|
setShouldRestoreHistory,
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
type SlashCommand,
|
type SlashCommand,
|
||||||
} from "$lib/commands/slashCommands";
|
} from "$lib/commands/slashCommands";
|
||||||
import { configStore, isStreamerMode } from "$lib/stores/config";
|
import { configStore, isStreamerMode } from "$lib/stores/config";
|
||||||
|
import { conversationsStore } from "$lib/stores/conversations";
|
||||||
import { stats, estimateMessageCost, formatTokenCount } from "$lib/stores/stats";
|
import { stats, estimateMessageCost, formatTokenCount } from "$lib/stores/stats";
|
||||||
import AttachmentPreview from "$lib/components/AttachmentPreview.svelte";
|
import AttachmentPreview from "$lib/components/AttachmentPreview.svelte";
|
||||||
import SnippetLibraryPanel from "$lib/components/SnippetLibraryPanel.svelte";
|
import SnippetLibraryPanel from "$lib/components/SnippetLibraryPanel.svelte";
|
||||||
@@ -350,6 +351,17 @@ User: ${formattedMessage}`;
|
|||||||
working_dir: workingDir,
|
working_dir: workingDir,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update Discord RPC when reconnecting
|
||||||
|
const config = configStore.getConfig();
|
||||||
|
const activeConversation = get(conversationsStore.activeConversation);
|
||||||
|
if (activeConversation) {
|
||||||
|
await updateDiscordRpc(
|
||||||
|
activeConversation.name,
|
||||||
|
config.model || "claude",
|
||||||
|
activeConversation.startedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (reconnectError) {
|
} catch (reconnectError) {
|
||||||
console.error("Failed to auto-reconnect:", reconnectError);
|
console.error("Failed to auto-reconnect:", reconnectError);
|
||||||
claudeStore.addLine("error", `Failed to reconnect: ${reconnectError}`);
|
claudeStore.addLine("error", `Failed to reconnect: ${reconnectError}`);
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
import { claudeStore, hasPermissionPending } from "$lib/stores/claude";
|
import { claudeStore, hasPermissionPending } from "$lib/stores/claude";
|
||||||
import { characterState } from "$lib/stores/character";
|
import { characterState } from "$lib/stores/character";
|
||||||
import type { PermissionRequest } from "$lib/types/messages";
|
import type { PermissionRequest } from "$lib/types/messages";
|
||||||
|
import { updateDiscordRpc } from "$lib/tauri";
|
||||||
|
import { conversationsStore } from "$lib/stores/conversations";
|
||||||
|
import { configStore } from "$lib/stores/config";
|
||||||
|
|
||||||
let isVisible = $state(false);
|
let isVisible = $state(false);
|
||||||
let permission: PermissionRequest | null = $state(null);
|
let permission: PermissionRequest | null = $state(null);
|
||||||
@@ -64,6 +67,17 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update Discord RPC when reconnecting after permission grant
|
||||||
|
const config = configStore.getConfig();
|
||||||
|
const activeConversation = get(conversationsStore.activeConversation);
|
||||||
|
if (activeConversation) {
|
||||||
|
await updateDiscordRpc(
|
||||||
|
activeConversation.name,
|
||||||
|
config.model || "claude",
|
||||||
|
activeConversation.startedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for connection to establish
|
// Wait for connection to establish
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
createSummary,
|
createSummary,
|
||||||
sanitizeForJson,
|
sanitizeForJson,
|
||||||
} from "$lib/utils/conversationUtils";
|
} from "$lib/utils/conversationUtils";
|
||||||
|
import { updateDiscordRpc } from "$lib/tauri";
|
||||||
|
|
||||||
const DISCORD_URL = "https://chat.nhcarrigan.com";
|
const DISCORD_URL = "https://chat.nhcarrigan.com";
|
||||||
const DONATE_URL = "https://donate.nhcarrigan.com";
|
const DONATE_URL = "https://donate.nhcarrigan.com";
|
||||||
@@ -165,6 +166,16 @@
|
|||||||
allowed_tools: allAllowedTools,
|
allowed_tools: allAllowedTools,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update Discord RPC when a new session starts
|
||||||
|
const activeConversation = get(conversationsStore.activeConversation);
|
||||||
|
if (activeConversation) {
|
||||||
|
await updateDiscordRpc(
|
||||||
|
activeConversation.name,
|
||||||
|
currentConfig.model || "claude",
|
||||||
|
activeConversation.startedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start Claude:", error);
|
console.error("Failed to start Claude:", error);
|
||||||
claudeStore.addLine("error", `Connection failed: ${error}`);
|
claudeStore.addLine("error", `Connection failed: ${error}`);
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
import { claudeStore, hasQuestionPending } from "$lib/stores/claude";
|
import { claudeStore, hasQuestionPending } from "$lib/stores/claude";
|
||||||
import { characterState } from "$lib/stores/character";
|
import { characterState } from "$lib/stores/character";
|
||||||
import type { UserQuestionEvent } from "$lib/types/messages";
|
import type { UserQuestionEvent } from "$lib/types/messages";
|
||||||
|
import { updateDiscordRpc } from "$lib/tauri";
|
||||||
|
import { conversationsStore } from "$lib/stores/conversations";
|
||||||
|
import { configStore } from "$lib/stores/config";
|
||||||
|
|
||||||
let isVisible = $state(false);
|
let isVisible = $state(false);
|
||||||
let question: UserQuestionEvent | null = $state(null);
|
let question: UserQuestionEvent | null = $state(null);
|
||||||
@@ -98,6 +101,17 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update Discord RPC when reconnecting after answering question
|
||||||
|
const config = configStore.getConfig();
|
||||||
|
const activeConversation = get(conversationsStore.activeConversation);
|
||||||
|
if (activeConversation) {
|
||||||
|
await updateDiscordRpc(
|
||||||
|
activeConversation.name,
|
||||||
|
config.model || "claude",
|
||||||
|
activeConversation.startedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
if (conversationHistory) {
|
if (conversationHistory) {
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export interface HikariConfig {
|
|||||||
session_cost_budget: number | null;
|
session_cost_budget: number | null;
|
||||||
budget_action: BudgetAction;
|
budget_action: BudgetAction;
|
||||||
budget_warning_threshold: number;
|
budget_warning_threshold: number;
|
||||||
|
// Discord RPC settings
|
||||||
|
discord_rpc_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig: HikariConfig = {
|
const defaultConfig: HikariConfig = {
|
||||||
@@ -83,6 +85,7 @@ const defaultConfig: HikariConfig = {
|
|||||||
session_cost_budget: null,
|
session_cost_budget: null,
|
||||||
budget_action: "warn",
|
budget_action: "warn",
|
||||||
budget_warning_threshold: 0.8,
|
budget_warning_threshold: 0.8,
|
||||||
|
discord_rpc_enabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
function createConfigStore() {
|
function createConfigStore() {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface Conversation {
|
|||||||
lastActivityAt: Date;
|
lastActivityAt: Date;
|
||||||
attachments: Attachment[];
|
attachments: Attachment[];
|
||||||
summary: ConversationSummary | null;
|
summary: ConversationSummary | null;
|
||||||
|
startedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createConversationsStore() {
|
function createConversationsStore() {
|
||||||
@@ -72,6 +73,7 @@ function createConversationsStore() {
|
|||||||
lastActivityAt: new Date(),
|
lastActivityAt: new Date(),
|
||||||
attachments: [],
|
attachments: [],
|
||||||
summary: null,
|
summary: null,
|
||||||
|
startedAt: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -373,3 +373,73 @@ export function cleanupTauriListeners() {
|
|||||||
// Cleanup notification rules
|
// Cleanup notification rules
|
||||||
cleanupNotificationRules();
|
cleanupNotificationRules();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function initializeDiscordRpc() {
|
||||||
|
const config = configStore.getConfig();
|
||||||
|
if (config.discord_rpc_enabled) {
|
||||||
|
try {
|
||||||
|
const startedAt = new Date();
|
||||||
|
const startedAtUnixSeconds = Math.floor(startedAt.getTime() / 1000);
|
||||||
|
const model = config.model || "claude";
|
||||||
|
|
||||||
|
await invoke("log_discord_rpc", {
|
||||||
|
message: `[FRONTEND] Attempting to initialize Discord RPC: session='Idle', model='${model}', timestamp=${startedAtUnixSeconds}`
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Initializing Discord RPC with initial activity:", {
|
||||||
|
session_name: "Idle",
|
||||||
|
model,
|
||||||
|
started_at: startedAtUnixSeconds
|
||||||
|
});
|
||||||
|
|
||||||
|
await invoke("init_discord_rpc", {
|
||||||
|
sessionName: "Idle",
|
||||||
|
model,
|
||||||
|
startedAt: startedAtUnixSeconds,
|
||||||
|
});
|
||||||
|
|
||||||
|
await invoke("log_discord_rpc", {
|
||||||
|
message: "[FRONTEND] Discord RPC initialized successfully!"
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Discord RPC initialized successfully with initial presence");
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
await invoke("log_discord_rpc", {
|
||||||
|
message: `[FRONTEND] ERROR: Failed to initialize Discord RPC: ${errorMessage}`
|
||||||
|
});
|
||||||
|
console.error("Failed to initialize Discord RPC:", error);
|
||||||
|
console.warn("Discord RPC will be unavailable. Make sure Discord is running.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await invoke("log_discord_rpc", {
|
||||||
|
message: "[FRONTEND] Discord RPC is disabled in config, skipping initialization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateDiscordRpc(sessionName: string, model: string, startedAt: Date) {
|
||||||
|
const config = configStore.getConfig();
|
||||||
|
if (!config.discord_rpc_enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startedAtUnixSeconds = Math.floor(startedAt.getTime() / 1000);
|
||||||
|
await invoke("update_discord_rpc", {
|
||||||
|
sessionName: sessionName,
|
||||||
|
model,
|
||||||
|
startedAt: startedAtUnixSeconds,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update Discord RPC:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stopDiscordRpc() {
|
||||||
|
try {
|
||||||
|
await invoke("stop_discord_rpc");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to stop Discord RPC:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+23
-1
@@ -2,7 +2,7 @@
|
|||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { initializeTauriListeners, cleanupTauriListeners } from "$lib/tauri";
|
import { initializeTauriListeners, cleanupTauriListeners, initializeDiscordRpc, stopDiscordRpc, updateDiscordRpc } from "$lib/tauri";
|
||||||
import { configStore, applyTheme, applyFontSize, isCompactMode } from "$lib/stores/config";
|
import { configStore, applyTheme, applyFontSize, isCompactMode } from "$lib/stores/config";
|
||||||
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
|
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
|
||||||
import { conversationsStore } from "$lib/stores/conversations";
|
import { conversationsStore } from "$lib/stores/conversations";
|
||||||
@@ -57,6 +57,24 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get reactive references to conversation stores
|
||||||
|
const activeConversationId = conversationsStore.activeConversationId;
|
||||||
|
const conversations = conversationsStore.conversations;
|
||||||
|
|
||||||
|
// Update Discord RPC when active conversation or model changes
|
||||||
|
$effect(() => {
|
||||||
|
// Access stores directly (without get()) to create reactive dependencies
|
||||||
|
const activeId = $activeConversationId;
|
||||||
|
const convs = $conversations;
|
||||||
|
const activeConv = activeId ? convs.get(activeId) : null;
|
||||||
|
const config = configStore.getConfig();
|
||||||
|
const model = config.model || "claude";
|
||||||
|
|
||||||
|
if (activeConv && config.discord_rpc_enabled) {
|
||||||
|
updateDiscordRpc(activeConv.name, model, activeConv.startedAt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Window size constants
|
// Window size constants
|
||||||
const COMPACT_WIDTH = 280;
|
const COMPACT_WIDTH = 280;
|
||||||
const COMPACT_HEIGHT = 400;
|
const COMPACT_HEIGHT = 400;
|
||||||
@@ -356,6 +374,9 @@
|
|||||||
const window = getCurrentWindow();
|
const window = getCurrentWindow();
|
||||||
await window.setSize(new LogicalSize(COMPACT_WIDTH, COMPACT_HEIGHT));
|
await window.setSize(new LogicalSize(COMPACT_WIDTH, COMPACT_HEIGHT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize Discord RPC
|
||||||
|
await initializeDiscordRpc();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -363,6 +384,7 @@
|
|||||||
if (initialized) {
|
if (initialized) {
|
||||||
cleanupTauriListeners();
|
cleanupTauriListeners();
|
||||||
cleanupNotificationSync();
|
cleanupNotificationSync();
|
||||||
|
stopDiscordRpc();
|
||||||
window.removeEventListener("keydown", handleGlobalKeydown);
|
window.removeEventListener("keydown", handleGlobalKeydown);
|
||||||
initialized = false;
|
initialized = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user