From cac521d28edf501ca2756a75951671e695751bc6 Mon Sep 17 00:00:00 2001 From: Hikari Date: Wed, 6 May 2026 14:22:37 -0700 Subject: [PATCH] feat: parse plugin_errors from stream-json init event (#270) Adds plugin_errors field to the System init message type and emits a claude:output error event for each failed plugin, giving users visibility into plugin load failures within Hikari Desktop. Requires Claude Code v2.1.111+. --- src-tauri/src/types.rs | 42 +++++++++++++++++++++++++++++++++++++ src-tauri/src/wsl_bridge.rs | 26 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 6f6058e..0fbdb5e 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -98,6 +98,10 @@ pub enum ClaudeMessage { /// Output style hint from Claude Code (v2.1.81+). Informational only. #[serde(default)] output_style: Option, + /// Plugin errors from Claude Code (v2.1.111+). Populated when plugins are demoted + /// due to unsatisfied dependencies. + #[serde(default)] + plugin_errors: Option, }, #[serde(rename = "assistant")] Assistant { @@ -977,6 +981,44 @@ mod tests { } } + #[test] + fn test_system_init_with_plugin_errors() { + let json = r#"{"type":"system","subtype":"init","session_id":"sess-1","plugin_errors":["Plugin 'foo' requires 'bar' which is not installed","Plugin 'baz' failed to load"]}"#; + let msg: ClaudeMessage = serde_json::from_str(json).unwrap(); + if let ClaudeMessage::System { plugin_errors, .. } = msg { + let errors = plugin_errors.expect("plugin_errors should be present"); + let arr = errors.as_array().expect("plugin_errors should be an array"); + assert_eq!(arr.len(), 2); + assert_eq!(arr[0].as_str(), Some("Plugin 'foo' requires 'bar' which is not installed")); + } else { + panic!("Expected System variant"); + } + } + + #[test] + fn test_system_init_without_plugin_errors() { + let json = r#"{"type":"system","subtype":"init","session_id":"sess-1"}"#; + let msg: ClaudeMessage = serde_json::from_str(json).unwrap(); + if let ClaudeMessage::System { plugin_errors, .. } = msg { + assert!(plugin_errors.is_none()); + } else { + panic!("Expected System variant"); + } + } + + #[test] + fn test_system_init_with_empty_plugin_errors() { + let json = r#"{"type":"system","subtype":"init","session_id":"sess-1","plugin_errors":[]}"#; + let msg: ClaudeMessage = serde_json::from_str(json).unwrap(); + if let ClaudeMessage::System { plugin_errors, .. } = msg { + let errors = plugin_errors.expect("plugin_errors should be present"); + let arr = errors.as_array().expect("plugin_errors should be an array"); + assert!(arr.is_empty()); + } else { + panic!("Expected System variant"); + } + } + #[test] fn test_result_message_with_fast_mode_state() { let json = r#"{"type":"result","subtype":"success","fast_mode_state":"enabled"}"#; diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index 9b023c5..197de82 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -1850,6 +1850,7 @@ fn process_json_line( subtype, session_id, cwd, + plugin_errors, .. } => { if subtype == "init" { @@ -1874,6 +1875,31 @@ fn process_json_line( }, ); } + + // Warn about any plugins that failed to load (v2.1.111+) + if let Some(errors) = plugin_errors { + if let Some(arr) = errors.as_array() { + for error in arr { + let msg = if let Some(s) = error.as_str() { + s.to_string() + } else { + error.to_string() + }; + let _ = app.emit( + "claude:output", + OutputEvent { + line_type: "error".to_string(), + content: format!("Plugin error: {}", msg), + tool_name: None, + conversation_id: conversation_id.clone(), + cost: None, + parent_tool_use_id: None, + }, + ); + } + } + } + emit_state_change(app, CharacterState::Idle, None, conversation_id.clone()); } }