generated from nhcarrigan/template
3f30997f0e
## Explanation This PR bundles several user-facing improvements and feature additions for the v0.3.0 release, including quality-of-life improvements to the UI, new slash commands, better state persistence, and auto-update checking. ## Included Changes - **Resizable chat input** with drag handle (#58 partial) - **Arrow key navigation fix** - cursor keys now navigate text when user has typed input (#58) - **Scroll position persistence** per conversation tab - **/skill command** for invoking Claude Code skills (#57) - **Stats persistence fix** - stats now persist across session changes, only reset on disconnect (#59) - **Auto-update checker** on startup (#17) - **Resizable character panel** with full-height sprites (#10) - **Font size and zoom settings** with keyboard shortcuts (Ctrl++/Ctrl+-/Ctrl+0) (#19) ## Closes Closes #10, #17, #19, #57, #58, #59 ## Attestations - [x] I have read and agree to the Code of Conduct - [x] I have read and agree to the Community Guidelines - [x] My contribution complies with the Contributor Covenant - [x] I have run the linter and resolved any errors - [x] My pull request uses an appropriate title, matching the conventional commit standards - [x] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request - [x] All new and existing tests pass locally with my changes - [x] Code coverage remains at or above the configured threshold ## Documentation N/A - Internal app features ## Versioning Minor - My pull request introduces new non-breaking features. --- ✨ This PR was created with help from Hikari~ 🌸 Co-authored-by: Hikari <hikari@nhcarrigan.com> Reviewed-on: #61 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
170 lines
5.0 KiB
Rust
170 lines
5.0 KiB
Rust
use parking_lot::Mutex;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use tauri::AppHandle;
|
|
|
|
use crate::config::ClaudeStartOptions;
|
|
use crate::stats::UsageStats;
|
|
use crate::wsl_bridge::WslBridge;
|
|
|
|
pub struct BridgeManager {
|
|
bridges: HashMap<String, WslBridge>,
|
|
app_handle: Option<AppHandle>,
|
|
}
|
|
|
|
impl BridgeManager {
|
|
pub fn new() -> Self {
|
|
BridgeManager {
|
|
bridges: HashMap::new(),
|
|
app_handle: None,
|
|
}
|
|
}
|
|
|
|
pub fn set_app_handle(&mut self, app: AppHandle) {
|
|
self.app_handle = Some(app);
|
|
}
|
|
|
|
pub fn start_claude(
|
|
&mut self,
|
|
conversation_id: &str,
|
|
options: ClaudeStartOptions,
|
|
) -> Result<(), String> {
|
|
// Check if a bridge already exists and is running for this conversation
|
|
if self
|
|
.bridges
|
|
.get(conversation_id)
|
|
.map(|b| b.is_running())
|
|
.unwrap_or(false)
|
|
{
|
|
return Err("Claude is already running for this conversation".to_string());
|
|
}
|
|
|
|
let app = self
|
|
.app_handle
|
|
.as_ref()
|
|
.ok_or_else(|| "App handle not set".to_string())?
|
|
.clone();
|
|
|
|
// Reuse existing bridge if it exists (preserves stats across reconnects)
|
|
// Only create a new bridge if one doesn't exist for this conversation
|
|
let bridge = self
|
|
.bridges
|
|
.entry(conversation_id.to_string())
|
|
.or_insert_with(|| WslBridge::new_with_conversation_id(conversation_id.to_string()));
|
|
|
|
// Start the Claude process
|
|
bridge.start(app, options)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn stop_claude(&mut self, conversation_id: &str) -> Result<(), String> {
|
|
if let Some(bridge) = self.bridges.get_mut(conversation_id) {
|
|
let app = self
|
|
.app_handle
|
|
.as_ref()
|
|
.ok_or_else(|| "App handle not set".to_string())?;
|
|
bridge.stop(app);
|
|
Ok(())
|
|
} else {
|
|
Err("No Claude instance found for this conversation".to_string())
|
|
}
|
|
}
|
|
|
|
pub fn interrupt_claude(&mut self, conversation_id: &str) -> Result<(), String> {
|
|
if let Some(bridge) = self.bridges.get_mut(conversation_id) {
|
|
let app = self
|
|
.app_handle
|
|
.as_ref()
|
|
.ok_or_else(|| "App handle not set".to_string())?;
|
|
bridge.interrupt(app)
|
|
} else {
|
|
Err("No Claude instance found for this conversation".to_string())
|
|
}
|
|
}
|
|
|
|
pub fn send_prompt(&mut self, conversation_id: &str, message: String) -> Result<(), String> {
|
|
if let Some(bridge) = self.bridges.get_mut(conversation_id) {
|
|
bridge.send_message(&message)
|
|
} else {
|
|
Err("No Claude instance found for this conversation".to_string())
|
|
}
|
|
}
|
|
|
|
pub fn send_tool_result(
|
|
&mut self,
|
|
conversation_id: &str,
|
|
tool_use_id: &str,
|
|
result: serde_json::Value,
|
|
) -> Result<(), String> {
|
|
if let Some(bridge) = self.bridges.get_mut(conversation_id) {
|
|
bridge.send_tool_result(tool_use_id, result)
|
|
} else {
|
|
Err("No Claude instance found for this conversation".to_string())
|
|
}
|
|
}
|
|
|
|
pub fn is_claude_running(&self, conversation_id: &str) -> bool {
|
|
self.bridges
|
|
.get(conversation_id)
|
|
.map(|b| b.is_running())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
pub fn get_working_directory(&self, conversation_id: &str) -> Result<String, String> {
|
|
self.bridges
|
|
.get(conversation_id)
|
|
.map(|b| b.get_working_directory().to_string())
|
|
.ok_or_else(|| "No Claude instance found for this conversation".to_string())
|
|
}
|
|
|
|
pub fn get_usage_stats(&self, conversation_id: &str) -> Result<UsageStats, String> {
|
|
self.bridges
|
|
.get(conversation_id)
|
|
.map(|b| b.get_stats())
|
|
.ok_or_else(|| "No Claude instance found for this conversation".to_string())
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn cleanup_stopped_bridges(&mut self) {
|
|
// Remove bridges that are no longer running
|
|
self.bridges.retain(|_, bridge| bridge.is_running());
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn stop_all(&mut self) {
|
|
if let Some(app) = &self.app_handle {
|
|
for (_, bridge) in self.bridges.iter_mut() {
|
|
bridge.stop(app);
|
|
}
|
|
}
|
|
self.bridges.clear();
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn get_active_conversations(&self) -> Vec<String> {
|
|
self.bridges
|
|
.keys()
|
|
.filter(|id| {
|
|
self.bridges
|
|
.get(*id)
|
|
.map(|b| b.is_running())
|
|
.unwrap_or(false)
|
|
})
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
impl Default for BridgeManager {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
pub type SharedBridgeManager = Arc<Mutex<BridgeManager>>;
|
|
|
|
pub fn create_shared_bridge_manager() -> SharedBridgeManager {
|
|
Arc::new(Mutex::new(BridgeManager::new()))
|
|
}
|