From a22b5b503dd3ce08b11e5b97e13ad9547c78e584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Mon, 25 May 2026 03:49:41 +0800 Subject: [PATCH] feat(hermes): add display run controls --- scripts/dev-api.js | 20 ++++++ src-tauri/src/commands/hermes.rs | 96 +++++++++++++++++++++++++++++ src/engines/hermes/pages/config.js | 18 ++++++ src/locales/modules/engine.js | 13 +++- tests/hermes-config-page-ui.test.js | 2 + tests/hermes-display-config.test.js | 18 ++++++ 6 files changed, 165 insertions(+), 2 deletions(-) diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 7d5d62c..5ea3ac9 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -3332,6 +3332,8 @@ const HERMES_AGENT_IMAGE_INPUT_MODES = new Set(['auto', 'native', 'text']) const HERMES_DISPLAY_TOOL_PROGRESS_VALUES = new Set(['off', 'new', 'all', 'verbose']) const HERMES_DISPLAY_STREAMING_VALUES = new Set(['inherit', 'true', 'false']) const HERMES_DISPLAY_RESUME_VALUES = new Set(['full', 'minimal']) +const HERMES_DISPLAY_BUSY_INPUT_MODES = new Set(['interrupt', 'queue', 'steer']) +const HERMES_DISPLAY_BACKGROUND_PROCESS_NOTIFICATIONS = new Set(['off', 'result', 'error', 'all']) const HERMES_DISPLAY_LANGUAGE_VALUES = new Set(['en', 'zh', 'zh-hant', 'ja', 'de', 'es', 'fr', 'tr', 'uk', 'af', 'ko', 'it', 'ga', 'pt', 'ru', 'hu']) const HERMES_RUNTIME_FOOTER_FIELDS = new Set(['model', 'context_pct', 'cwd', 'duration', 'tokens', 'cost']) @@ -3463,6 +3465,20 @@ function normalizeHermesDisplayResume(value, strict = false) { return 'full' } +function normalizeHermesDisplayBusyInputMode(value, strict = false) { + const mode = String(value ?? '').trim().toLowerCase() || 'interrupt' + if (HERMES_DISPLAY_BUSY_INPUT_MODES.has(mode)) return mode + if (strict) throw new Error('display.busy_input_mode 必须是 interrupt、queue 或 steer') + return 'interrupt' +} + +function normalizeHermesDisplayBackgroundProcessNotifications(value, strict = false) { + const mode = String(value ?? '').trim().toLowerCase() || 'all' + if (HERMES_DISPLAY_BACKGROUND_PROCESS_NOTIFICATIONS.has(mode)) return mode + if (strict) throw new Error('display.background_process_notifications 必须是 off、result、error 或 all') + return 'all' +} + function normalizeHermesDisplayLanguage(value, strict = false) { const language = String(value ?? '').trim().toLowerCase() || 'en' if (HERMES_DISPLAY_LANGUAGE_VALUES.has(language)) return language @@ -3514,6 +3530,8 @@ export function buildHermesDisplayConfigValues(config = {}) { displayFileMutationVerifier: readHermesBool(display.file_mutation_verifier, true), displayLanguage: normalizeHermesDisplayLanguage(display.language, false), displayResumeDisplay: normalizeHermesDisplayResume(display.resume_display, false), + displayBusyInputMode: normalizeHermesDisplayBusyInputMode(display.busy_input_mode, false), + displayBackgroundProcessNotifications: normalizeHermesDisplayBackgroundProcessNotifications(display.background_process_notifications, false), } } @@ -3536,6 +3554,8 @@ export function mergeHermesDisplayConfig(config = {}, form = {}) { display.file_mutation_verifier = formHermesBool(form, 'displayFileMutationVerifier', currentValues.displayFileMutationVerifier) display.language = normalizeHermesDisplayLanguage(Object.hasOwn(form, 'displayLanguage') ? form.displayLanguage : currentValues.displayLanguage, true) display.resume_display = normalizeHermesDisplayResume(Object.hasOwn(form, 'displayResumeDisplay') ? form.displayResumeDisplay : currentValues.displayResumeDisplay, true) + display.busy_input_mode = normalizeHermesDisplayBusyInputMode(Object.hasOwn(form, 'displayBusyInputMode') ? form.displayBusyInputMode : currentValues.displayBusyInputMode, true) + display.background_process_notifications = normalizeHermesDisplayBackgroundProcessNotifications(Object.hasOwn(form, 'displayBackgroundProcessNotifications') ? form.displayBackgroundProcessNotifications : currentValues.displayBackgroundProcessNotifications, true) next.display = display return next } diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 8775a0f..1102dc9 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -4232,6 +4232,9 @@ const HERMES_DISPLAY_LANGUAGE_VALUES: &[&str] = &[ "hu", ]; +const HERMES_DISPLAY_BUSY_INPUT_MODES: &[&str] = &["interrupt", "queue", "steer"]; +const HERMES_DISPLAY_BACKGROUND_PROCESS_NOTIFICATIONS: &[&str] = &["off", "result", "error", "all"]; + const HERMES_RUNTIME_FOOTER_FIELDS: &[&str] = &["model", "context_pct", "cwd", "duration", "tokens", "cost"]; @@ -4270,6 +4273,44 @@ fn normalize_hermes_display_resume(value: Option, strict: bool) -> Resul } } +fn normalize_hermes_display_busy_input_mode( + value: Option, + strict: bool, +) -> Result { + let mode = value.unwrap_or_default().trim().to_ascii_lowercase(); + let mode = if mode.is_empty() { + "interrupt".to_string() + } else { + mode + }; + if HERMES_DISPLAY_BUSY_INPUT_MODES.contains(&mode.as_str()) { + Ok(mode) + } else if strict { + Err("display.busy_input_mode 必须是 interrupt、queue 或 steer".to_string()) + } else { + Ok("interrupt".to_string()) + } +} + +fn normalize_hermes_display_background_process_notifications( + value: Option, + strict: bool, +) -> Result { + let mode = value.unwrap_or_default().trim().to_ascii_lowercase(); + let mode = if mode.is_empty() { + "all".to_string() + } else { + mode + }; + if HERMES_DISPLAY_BACKGROUND_PROCESS_NOTIFICATIONS.contains(&mode.as_str()) { + Ok(mode) + } else if strict { + Err("display.background_process_notifications 必须是 off、result、error 或 all".to_string()) + } else { + Ok("all".to_string()) + } +} + fn normalize_hermes_runtime_footer_fields_text( value: Option, strict: bool, @@ -4382,6 +4423,14 @@ fn build_hermes_display_config_values(config: &serde_yaml::Value) -> Value { display.and_then(|map| yaml_string_field(map, "resume_display")), false, ).unwrap_or_else(|_| "full".to_string()), + "displayBusyInputMode": normalize_hermes_display_busy_input_mode( + display.and_then(|map| yaml_string_field(map, "busy_input_mode")), + false, + ).unwrap_or_else(|_| "interrupt".to_string()), + "displayBackgroundProcessNotifications": normalize_hermes_display_background_process_notifications( + display.and_then(|map| yaml_string_field(map, "background_process_notifications")), + false, + ).unwrap_or_else(|_| "all".to_string()), }) } @@ -4461,6 +4510,28 @@ fn merge_hermes_display_config(config: &mut serde_yaml::Value, form: &Value) -> true, )?), ); + display.insert( + yaml_key("busy_input_mode"), + serde_yaml::Value::String(normalize_hermes_display_busy_input_mode( + form_string(form, "displayBusyInputMode").or_else(|| { + current["displayBusyInputMode"] + .as_str() + .map(ToString::to_string) + }), + true, + )?), + ); + display.insert( + yaml_key("background_process_notifications"), + serde_yaml::Value::String(normalize_hermes_display_background_process_notifications( + form_string(form, "displayBackgroundProcessNotifications").or_else(|| { + current["displayBackgroundProcessNotifications"] + .as_str() + .map(ToString::to_string) + }), + true, + )?), + ); let runtime_footer = yaml_child_object(display, "runtime_footer")?; runtime_footer.insert( yaml_key("enabled"), @@ -14213,6 +14284,8 @@ mod hermes_display_config_tests { assert_eq!(values["displayFileMutationVerifier"], true); assert_eq!(values["displayLanguage"], "en"); assert_eq!(values["displayResumeDisplay"], "full"); + assert_eq!(values["displayBusyInputMode"], "interrupt"); + assert_eq!(values["displayBackgroundProcessNotifications"], "all"); } #[test] @@ -14232,6 +14305,8 @@ display: file_mutation_verifier: false language: ZH resume_display: minimal + busy_input_mode: QUEUE + background_process_notifications: ERROR "#, ) .unwrap(); @@ -14247,6 +14322,8 @@ display: assert_eq!(values["displayFileMutationVerifier"], false); assert_eq!(values["displayLanguage"], "zh"); assert_eq!(values["displayResumeDisplay"], "minimal"); + assert_eq!(values["displayBusyInputMode"], "queue"); + assert_eq!(values["displayBackgroundProcessNotifications"], "error"); } #[test] @@ -14280,6 +14357,8 @@ memory: "displayFileMutationVerifier": true, "displayLanguage": "zh-hant", "displayResumeDisplay": "minimal", + "displayBusyInputMode": "steer", + "displayBackgroundProcessNotifications": "result", }), ) .unwrap(); @@ -14326,6 +14405,11 @@ memory: config["display"]["resume_display"].as_str(), Some("minimal") ); + assert_eq!(config["display"]["busy_input_mode"].as_str(), Some("steer")); + assert_eq!( + config["display"]["background_process_notifications"].as_str(), + Some("result") + ); } #[test] @@ -14353,6 +14437,18 @@ memory: ) .unwrap_err(); assert!(err.contains("display.runtime_footer.fields")); + + let err = + merge_hermes_display_config(&mut config, &json!({ "displayBusyInputMode": "replace" })) + .unwrap_err(); + assert!(err.contains("display.busy_input_mode")); + + let err = merge_hermes_display_config( + &mut config, + &json!({ "displayBackgroundProcessNotifications": "silent" }), + ) + .unwrap_err(); + assert!(err.contains("display.background_process_notifications")); } } diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index a3e5cc3..fc4a8e1 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -88,6 +88,8 @@ const DISPLAY_DEFAULTS = { displayFileMutationVerifier: true, displayLanguage: 'en', displayResumeDisplay: 'full', + displayBusyInputMode: 'interrupt', + displayBackgroundProcessNotifications: 'all', } const HUMAN_DELAY_DEFAULTS = { @@ -191,6 +193,8 @@ const IMAGE_INPUT_MODES = ['auto', 'native', 'text'] const DISPLAY_TOOL_PROGRESS_VALUES = ['off', 'new', 'all', 'verbose'] const DISPLAY_LANGUAGE_VALUES = ['en', 'zh', 'zh-hant', 'ja', 'de', 'es', 'fr', 'tr', 'uk', 'af', 'ko', 'it', 'ga', 'pt', 'ru', 'hu'] const DISPLAY_RESUME_VALUES = ['full', 'minimal'] +const DISPLAY_BUSY_INPUT_MODES = ['interrupt', 'queue', 'steer'] +const DISPLAY_BACKGROUND_PROCESS_NOTIFICATIONS = ['off', 'result', 'error', 'all'] const HUMAN_DELAY_MODES = ['off', 'natural', 'custom'] const APPROVAL_MODES = ['manual', 'smart', 'off'] const APPROVAL_CRON_MODES = ['deny', 'approve'] @@ -781,6 +785,18 @@ export function render() { ${DISPLAY_RESUME_VALUES.map(mode => option(`engine.hermesDisplayConfigResumeDisplay_${mode}`, mode, displayValues.displayResumeDisplay)).join('')} + +