Enhanced code review and frontend validation with intelligent triggers: Code Review Agent Enhancement: - Added Sequential Thinking MCP integration for complex issues - Triggers on 2+ rejections or 3+ critical issues - New escalation format with root cause analysis - Comprehensive solution strategies with trade-off evaluation - Educational feedback to break rejection cycles - Files: .claude/agents/code-review.md (+308 lines) - Docs: CODE_REVIEW_ST_ENHANCEMENT.md, CODE_REVIEW_ST_TESTING.md Frontend Design Skill Enhancement: - Automatic invocation for ANY UI change - Comprehensive validation checklist (200+ checkpoints) - 8 validation categories (visual, interactive, responsive, a11y, etc.) - 3 validation levels (quick, standard, comprehensive) - Integration with code review workflow - Files: .claude/skills/frontend-design/SKILL.md (+120 lines) - Docs: UI_VALIDATION_CHECKLIST.md (462 lines), AUTOMATIC_VALIDATION_ENHANCEMENT.md (587 lines) Settings Optimization: - Repaired .claude/settings.local.json (fixed m365 pattern) - Reduced permissions from 49 to 33 (33% reduction) - Removed duplicates, sorted alphabetically - Created SETTINGS_PERMISSIONS.md documentation Checkpoint Command Enhancement: - Dual checkpoint system (git + database) - Saves session context to API for cross-machine recall - Includes git metadata in database context - Files: .claude/commands/checkpoint.md (+139 lines) Decision Rationale: - Sequential Thinking MCP breaks rejection cycles by identifying root causes - Automatic frontend validation catches UI issues before code review - Dual checkpoints enable complete project memory across machines - Settings optimization improves maintainability Total: 1,200+ lines of documentation and enhancements Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
296 lines
13 KiB
Plaintext
296 lines
13 KiB
Plaintext
1→//! Main tray application logic
|
|
2→
|
|
3→use anyhow::{Context, Result};
|
|
4→use std::sync::Arc;
|
|
5→use tokio::sync::{mpsc, RwLock};
|
|
6→use tray_icon::{
|
|
7→ menu::MenuEvent,
|
|
8→ Icon, TrayIcon, TrayIconBuilder,
|
|
9→};
|
|
10→use tracing::{debug, error, info, warn};
|
|
11→use winit::event_loop::{ControlFlow, EventLoop};
|
|
12→
|
|
13→use crate::ipc::{AgentStatus, ConnectionState, IpcClient, IpcRequest, TrayPolicy};
|
|
14→use crate::menu::{self, MenuAction};
|
|
15→
|
|
16→/// Icon state
|
|
17→#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
18→pub enum IconState {
|
|
19→ Connected,
|
|
20→ Reconnecting,
|
|
21→ Error,
|
|
22→ Disabled,
|
|
23→}
|
|
24→
|
|
25→/// Embedded icon data (will be replaced with actual icons)
|
|
26→mod icons {
|
|
27→ // Placeholder: 16x16 RGBA icons
|
|
28→ // In production, these would be loaded from the assets folder
|
|
29→
|
|
30→ pub fn connected() -> Vec<u8> {
|
|
31→ // Green circle placeholder (16x16 RGBA)
|
|
32→ create_circle_icon(0x22, 0xc5, 0x5e, 0xff)
|
|
33→ }
|
|
34→
|
|
35→ pub fn reconnecting() -> Vec<u8> {
|
|
36→ // Yellow circle placeholder
|
|
37→ create_circle_icon(0xea, 0xb3, 0x08, 0xff)
|
|
38→ }
|
|
39→
|
|
40→ pub fn error() -> Vec<u8> {
|
|
41→ // Red circle placeholder
|
|
42→ create_circle_icon(0xef, 0x44, 0x44, 0xff)
|
|
43→ }
|
|
44→
|
|
45→ pub fn disabled() -> Vec<u8> {
|
|
46→ // Gray circle placeholder
|
|
47→ create_circle_icon(0x6b, 0x72, 0x80, 0xff)
|
|
48→ }
|
|
49→
|
|
50→ fn create_circle_icon(r: u8, g: u8, b: u8, a: u8) -> Vec<u8> {
|
|
51→ let size = 32;
|
|
52→ let mut data = vec![0u8; size * size * 4];
|
|
53→ let center = size as f32 / 2.0;
|
|
54→ let radius = center - 2.0;
|
|
55→
|
|
56→ for y in 0..size {
|
|
57→ for x in 0..size {
|
|
58→ let dx = x as f32 - center;
|
|
59→ let dy = y as f32 - center;
|
|
60→ let dist = (dx * dx + dy * dy).sqrt();
|
|
61→
|
|
62→ let idx = (y * size + x) * 4;
|
|
63→ if dist <= radius {
|
|
64→ data[idx] = r;
|
|
65→ data[idx + 1] = g;
|
|
66→ data[idx + 2] = b;
|
|
67→ data[idx + 3] = a;
|
|
68→ } else if dist <= radius + 1.0 {
|
|
69→ // Anti-aliased edge
|
|
70→ let alpha = ((radius + 1.0 - dist) * a as f32) as u8;
|
|
71→ data[idx] = r;
|
|
72→ data[idx + 1] = g;
|
|
73→ data[idx + 2] = b;
|
|
74→ data[idx + 3] = alpha;
|
|
75→ }
|
|
76→ }
|
|
77→ }
|
|
78→
|
|
79→ data
|
|
80→ }
|
|
81→}
|
|
82→
|
|
83→/// Main tray application
|
|
84→pub struct TrayApp {
|
|
85→ /// Tokio runtime for async operations
|
|
86→ runtime: tokio::runtime::Runtime,
|
|
87→
|
|
88→ /// IPC client state
|
|
89→ connection_state: Arc<RwLock<ConnectionState>>,
|
|
90→ status: Arc<RwLock<AgentStatus>>,
|
|
91→ policy: Arc<RwLock<TrayPolicy>>,
|
|
92→
|
|
93→ /// Request sender
|
|
94→ request_tx: mpsc::Sender<IpcRequest>,
|
|
95→}
|
|
96→
|
|
97→impl TrayApp {
|
|
98→ /// Create a new tray application
|
|
99→ pub fn new() -> Result<Self> {
|
|
100→ let runtime = tokio::runtime::Builder::new_multi_thread()
|
|
101→ .enable_all()
|
|
102→ .build()
|
|
103→ .context("Failed to create tokio runtime")?;
|
|
104→
|
|
105→ let connection_state = Arc::new(RwLock::new(ConnectionState::Disconnected));
|
|
106→ let status = Arc::new(RwLock::new(AgentStatus::default()));
|
|
107→ let policy = Arc::new(RwLock::new(TrayPolicy::default_permissive()));
|
|
108→
|
|
109→ let (request_tx, request_rx) = mpsc::channel(32);
|
|
110→ let (update_tx, _update_rx) = mpsc::channel(32);
|
|
111→
|
|
112→ // Spawn IPC connection task
|
|
113→ let conn_state = Arc::clone(&connection_state);
|
|
114→ let conn_status = Arc::clone(&status);
|
|
115→ let conn_policy = Arc::clone(&policy);
|
|
116→
|
|
117→ runtime.spawn(async move {
|
|
118→ crate::ipc::connection::run_connection(
|
|
119→ conn_state,
|
|
120→ conn_status,
|
|
121→ conn_policy,
|
|
122→ request_rx,
|
|
123→ update_tx,
|
|
124→ )
|
|
125→ .await;
|
|
126→ });
|
|
127→
|
|
128→ Ok(Self {
|
|
129→ runtime,
|
|
130→ connection_state,
|
|
131→ status,
|
|
132→ policy,
|
|
133→ request_tx,
|
|
134→ })
|
|
135→ }
|
|
136→
|
|
137→ /// Run the tray application (blocking)
|
|
138→ pub fn run(self) -> Result<()> {
|
|
139→ let event_loop = EventLoop::new().context("Failed to create event loop")?;
|
|
140→
|
|
141→ // Create initial icon
|
|
142→ let icon = create_icon(IconState::Reconnecting)?;
|
|
143→
|
|
144→ // Get initial status and policy for menu
|
|
145→ let (status, policy) = self.runtime.block_on(async {
|
|
146→ let s = self.status.read().await.clone();
|
|
147→ let p = self.policy.read().await.clone();
|
|
148→ (s, p)
|
|
149→ });
|
|
150→
|
|
151→ // Build initial menu
|
|
152→ let menu = menu::build_menu(&status, &policy);
|
|
153→
|
|
154→ // Create tray icon
|
|
155→ let tooltip = policy
|
|
156→ .tooltip_text
|
|
157→ .clone()
|
|
158→ .unwrap_or_else(|| "GuruRMM Agent".to_string());
|
|
159→
|
|
160→ let tray_icon = TrayIconBuilder::new()
|
|
161→ .with_menu(Box::new(menu))
|
|
162→ .with_tooltip(&tooltip)
|
|
163→ .with_icon(icon)
|
|
164→ .build()
|
|
165→ .context("Failed to create tray icon")?;
|
|
166→
|
|
167→ info!("Tray icon created");
|
|
168→
|
|
169→ // Menu event receiver
|
|
170→ let menu_channel = MenuEvent::receiver();
|
|
171→
|
|
172→ // Track last known state for icon updates
|
|
173→ let mut last_icon_state = IconState::Reconnecting;
|
|
174→ let mut last_connected = false;
|
|
175→
|
|
176→ // Run event loop
|
|
177→ event_loop.run(move |_event, event_loop| {
|
|
178→ event_loop.set_control_flow(ControlFlow::Wait);
|
|
179→
|
|
180→ // Check for menu events (non-blocking)
|
|
181→ if let Ok(event) = menu_channel.try_recv() {
|
|
182→ let action = menu::handle_menu_event(event);
|
|
183→ debug!("Menu action: {:?}", action);
|
|
184→
|
|
185→ match action {
|
|
186→ MenuAction::ForceCheckin => {
|
|
187→ let tx = self.request_tx.clone();
|
|
188→ self.runtime.spawn(async move {
|
|
189→ if let Err(e) = tx.send(IpcRequest::ForceCheckin).await {
|
|
190→ error!("Failed to send force checkin: {}", e);
|
|
191→ }
|
|
192→ });
|
|
193→ }
|
|
194→ MenuAction::ViewLogs => {
|
|
195→ // Open log file location
|
|
196→ #[cfg(windows)]
|
|
197→ {
|
|
198→ let _ = std::process::Command::new("explorer")
|
|
199→ .arg(r"C:\ProgramData\GuruRMM\logs")
|
|
200→ .spawn();
|
|
201→ }
|
|
202→ }
|
|
203→ MenuAction::OpenDashboard => {
|
|
204→ let policy = self.runtime.block_on(async {
|
|
205→ self.policy.read().await.clone()
|
|
206→ });
|
|
207→ if let Some(url) = policy.dashboard_url {
|
|
208→ let _ = open::that(&url);
|
|
209→ }
|
|
210→ }
|
|
211→ MenuAction::StopAgent => {
|
|
212→ // Show confirmation dialog before stopping
|
|
213→ // For now, just send the request
|
|
214→ let tx = self.request_tx.clone();
|
|
215→ self.runtime.spawn(async move {
|
|
216→ if let Err(e) = tx.send(IpcRequest::StopAgent).await {
|
|
217→ error!("Failed to send stop agent: {}", e);
|
|
218→ }
|
|
219→ });
|
|
220→ }
|
|
221→ MenuAction::ExitTray => {
|
|
222→ info!("Exit requested");
|
|
223→ event_loop.exit();
|
|
224→ }
|
|
225→ MenuAction::Unknown(id) => {
|
|
226→ warn!("Unknown menu action: {}", id);
|
|
227→ }
|
|
228→ }
|
|
229→ }
|
|
230→
|
|
231→ // Periodically update icon and menu based on state
|
|
232→ let (conn_state, status, policy) = self.runtime.block_on(async {
|
|
233→ let c = *self.connection_state.read().await;
|
|
234→ let s = self.status.read().await.clone();
|
|
235→ let p = self.policy.read().await.clone();
|
|
236→ (c, s, p)
|
|
237→ });
|
|
238→
|
|
239→ // Determine icon state
|
|
240→ let icon_state = match conn_state {
|
|
241→ ConnectionState::Connected if status.connected => IconState::Connected,
|
|
242→ ConnectionState::Connected => IconState::Error,
|
|
243→ ConnectionState::Connecting => IconState::Reconnecting,
|
|
244→ ConnectionState::Disconnected => IconState::Disabled,
|
|
245→ };
|
|
246→
|
|
247→ // Update icon if state changed
|
|
248→ if icon_state != last_icon_state {
|
|
249→ last_icon_state = icon_state;
|
|
250→ if let Ok(new_icon) = create_icon(icon_state) {
|
|
251→ if let Err(e) = tray_icon.set_icon(Some(new_icon)) {
|
|
252→ warn!("Failed to update icon: {}", e);
|
|
253→ }
|
|
254→ }
|
|
255→
|
|
256→ // Update tooltip
|
|
257→ let tooltip = match icon_state {
|
|
258→ IconState::Connected => format!("GuruRMM - Connected to {}",
|
|
259→ status.server_url.split('/').nth(2).unwrap_or(&status.server_url)),
|
|
260→ IconState::Reconnecting => "GuruRMM - Reconnecting...".to_string(),
|
|
261→ IconState::Error => format!("GuruRMM - Error: {}",
|
|
262→ status.error.as_deref().unwrap_or("Unknown")),
|
|
263→ IconState::Disabled => "GuruRMM - Disabled".to_string(),
|
|
264→ };
|
|
265→ let _ = tray_icon.set_tooltip(Some(&tooltip));
|
|
266→ }
|
|
267→
|
|
268→ // Rebuild menu if connection state changed
|
|
269→ if status.connected != last_connected {
|
|
270→ last_connected = status.connected;
|
|
271→ let new_menu = menu::build_menu(&status, &policy);
|
|
272→ tray_icon.set_menu(Some(Box::new(new_menu)));
|
|
273→ }
|
|
274→ })?;
|
|
275→
|
|
276→ Ok(())
|
|
277→ }
|
|
278→}
|
|
279→
|
|
280→/// Create an icon for the given state
|
|
281→fn create_icon(state: IconState) -> Result<Icon> {
|
|
282→ let data = match state {
|
|
283→ IconState::Connected => icons::connected(),
|
|
284→ IconState::Reconnecting => icons::reconnecting(),
|
|
285→ IconState::Error => icons::error(),
|
|
286→ IconState::Disabled => icons::disabled(),
|
|
287→ };
|
|
288→
|
|
289→ Icon::from_rgba(data, 32, 32).context("Failed to create icon")
|
|
290→}
|
|
291→
|
|
|
|
<system-reminder>
|
|
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
|
|
</system-reminder>
|