generated from nhcarrigan/template
f173892aaa
## Summary This PR includes major feature additions, bug fixes, comprehensive testing improvements, and responsive design enhancements! ## New Features โจ ### Plugin & MCP Management (#133, #134) - **Plugin Management Panel**: Install, uninstall, enable/disable, and update plugins - **MCP Server Management Panel**: Add/remove MCP servers, view detailed configuration - **Marketplace Management**: Add/remove plugin marketplaces from GitHub - Backend commands for full CLI integration (`list_plugins`, `install_plugin`, `add_mcp_server`, etc.) - Beautiful UI with proper loading states, error handling, and theme support ### Visual Todo List Panel (#132) - Real-time todo list display when Hikari uses the `TodoWrite` tool - Shows pending/in-progress/completed status with visual indicators - Progress bar and completion count - Automatically clears on disconnect - Theme-aware styling ### Clear Session History Button (#130) - "Clear All Sessions" button in Session History panel - Confirmation dialog with session count - Keyboard support and accessibility features - Gives users control over disk usage ### CLI Version Display (#131) - Displays Claude CLI version in status bar - Auto-polls every 30 seconds for updates - Useful for debugging and feature compatibility ## Bug Fixes ๐ ### Stats Panel Scrolling (#136) - **Fixed stats panel overflow**: Added scrollable container with `max-height` constraint - Stats panel now scrolls when content (Tools Used, Historical Costs, Budget sections) gets too long - Prevents content from overflowing off screen ### Agent Monitor Fixes (#122) - **Fixed agents stuck in "running" state**: Added `SubagentStop` hook parsing - **Fixed agents persisting after disconnect**: Call `clearConversation()` on disconnect - **Fixed "Kill All" button**: Now properly marks all agents as errored - **Fixed badge persisting after tab close**: Cleanup agents when conversation is deleted - Comprehensive tests for agent lifecycle management ### Discord RPC Cleanup (#129) - Removed file-based logging for Discord RPC - Replaced with proper `tracing` framework usage - Reduces disk usage and eliminates maintenance burden ### Close Modal Bug Fix (#128) - Fixed close confirmation modal not triggering after Discord RPC refactor - Removed frontend calls to deleted `log_discord_rpc` command - Modal now works correctly after all operations ### Responsive Design Fixes (#118) - Fixed top navigation icons getting cut off at small screen widths - Fixed Connect button disappearing on narrow screens - Fixed bottom status info (clock, CLI version) getting cut off - Added flex-wrap and mobile-optimised layouts - Icons-only mode on screens < 640px - Vertical stacking on screens < 768px ## Testing Improvements ๐งช ### Comprehensive Test Coverage (#114) - **417 backend tests** (up from 408) - **387 frontend tests** (up from 363) - **61%+ backend code coverage** - Added E2E integration tests for cross-platform notification commands - New test files: `agents.test.ts`, comprehensive CLI parsing tests - Tests for `debug_logger.rs`, `bridge_manager.rs`, `notifications.rs` - Console mocking for cleaner test output - Fixed flaky frontend tests ### Testing Documentation - Updated CLAUDE.md with comprehensive testing guidelines - Documented mocking approaches (console mocking, E2E command structure testing) - Added step-by-step guide for adding tests to new features - Goal to maintain ~100% test coverage documented ## Closes Closes #114 Closes #118 Closes #122 Closes #128 Closes #129 Closes #130 Closes #131 Closes #132 Closes #133 Closes #134 Closes #136 ## Technical Details - All new backend commands properly registered in `lib.rs` - CLI output parsing with comprehensive test coverage - Cross-platform compatibility verified through E2E tests (Linux CI can test Windows commands) - Theme-aware UI components using CSS variables throughout - Proper TypeScript types for all new stores and components - ESLint and Prettier compliant - All Clippy warnings addressed โจ This PR was created with help from Hikari~ ๐ธ Reviewed-on: #135 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
179 lines
6.2 KiB
Rust
179 lines
6.2 KiB
Rust
use discord_rich_presence::activity::{Activity, Assets, Timestamps};
|
|
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
|
|
use parking_lot::RwLock;
|
|
use std::sync::Arc;
|
|
|
|
pub struct DiscordRpcManager {
|
|
client: Arc<RwLock<Option<DiscordIpcClient>>>,
|
|
session_name: Arc<RwLock<String>>,
|
|
model: Arc<RwLock<String>>,
|
|
started_at: Arc<RwLock<i64>>,
|
|
}
|
|
|
|
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)),
|
|
}
|
|
}
|
|
|
|
pub fn init(&self, initial_session_name: String, initial_model: String, started_at: i64) -> Result<(), String> {
|
|
tracing::debug!("Attempting to initialize Discord RPC...");
|
|
tracing::debug!("Application ID: 1391117878182281316");
|
|
tracing::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);
|
|
tracing::error!("{}", error_msg);
|
|
error_msg
|
|
})?;
|
|
|
|
tracing::debug!("DiscordIpcClient created successfully");
|
|
|
|
client
|
|
.connect()
|
|
.map_err(|e| {
|
|
let error_msg = format!("Failed to connect to Discord RPC: {} (ensure Discord is running)", e);
|
|
tracing::error!("{}", error_msg);
|
|
error_msg
|
|
})?;
|
|
|
|
tracing::debug!("Connected to Discord IPC socket");
|
|
|
|
// Set initial activity immediately after connecting
|
|
tracing::debug!("Building initial activity...");
|
|
let state_text = format!("Model: {}", initial_model);
|
|
let assets = Assets::new()
|
|
.large_image("hikari")
|
|
.large_text("Hikari - Claude Code Assistant");
|
|
|
|
tracing::debug!("Assets created - large_image: 'hikari', large_text: 'Hikari - Claude Code Assistant'");
|
|
|
|
let timestamps = Timestamps::new()
|
|
.start(started_at);
|
|
|
|
tracing::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);
|
|
|
|
tracing::debug!("Activity created - details: '{}', state: '{}'",
|
|
initial_session_name, state_text);
|
|
|
|
tracing::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);
|
|
tracing::error!("{}", error_msg);
|
|
error_msg
|
|
})?;
|
|
|
|
tracing::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;
|
|
|
|
tracing::info!("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> {
|
|
tracing::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;
|
|
|
|
tracing::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();
|
|
tracing::error!("{}", error_msg);
|
|
error_msg
|
|
})?;
|
|
|
|
tracing::debug!("Client lock acquired");
|
|
|
|
let state_text = format!("Model: {}", model);
|
|
let assets = Assets::new()
|
|
.large_image("hikari")
|
|
.large_text("Hikari - Claude Code Assistant");
|
|
|
|
tracing::debug!("Assets created - large_image: 'hikari', large_text: 'Hikari - Claude Code Assistant'");
|
|
|
|
let timestamps = Timestamps::new()
|
|
.start(started_at);
|
|
|
|
tracing::debug!("Timestamps created - start: {}", started_at);
|
|
|
|
let activity = Activity::new()
|
|
.details(session_name.as_str())
|
|
.state(state_text.as_str())
|
|
.assets(assets)
|
|
.timestamps(timestamps);
|
|
|
|
tracing::debug!("Activity created - details: '{}', state: '{}'",
|
|
session_name, state_text);
|
|
|
|
tracing::debug!("Attempting to set activity...");
|
|
client
|
|
.set_activity(activity)
|
|
.map_err(|e| {
|
|
let error_msg = format!("Failed to update Discord RPC: {}", e);
|
|
tracing::error!("{}", error_msg);
|
|
error_msg
|
|
})?;
|
|
|
|
tracing::info!("Updated Discord RPC: session='{}', model='{}'", session_name, model);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn stop(&self) -> Result<(), String> {
|
|
tracing::debug!("stop() called");
|
|
|
|
let mut client_guard = self.client.write();
|
|
if let Some(mut client) = client_guard.take() {
|
|
tracing::debug!("Client found, attempting to close...");
|
|
client
|
|
.close()
|
|
.map_err(|e| {
|
|
let error_msg = format!("Failed to close Discord RPC: {}", e);
|
|
tracing::error!("{}", error_msg);
|
|
error_msg
|
|
})?;
|
|
tracing::info!("Discord RPC stopped successfully");
|
|
} else {
|
|
tracing::debug!("No client to stop (already stopped or never initialized)");
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Default for DiscordRpcManager {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|