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+.
This commit is contained in:
2026-05-06 14:22:37 -07:00
committed by Naomi Carrigan
parent f3a2f8a491
commit cac521d28e
2 changed files with 68 additions and 0 deletions
+42
View File
@@ -98,6 +98,10 @@ pub enum ClaudeMessage {
/// Output style hint from Claude Code (v2.1.81+). Informational only.
#[serde(default)]
output_style: Option<String>,
/// Plugin errors from Claude Code (v2.1.111+). Populated when plugins are demoted
/// due to unsatisfied dependencies.
#[serde(default)]
plugin_errors: Option<serde_json::Value>,
},
#[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"}"#;
+26
View File
@@ -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());
}
}