diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 35736ef..4020083 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -3357,6 +3357,8 @@ const HERMES_DISPLAY_FINAL_RESPONSE_MARKDOWN_VALUES = new Set(['render', 'strip' 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_DISPLAY_SKINS = new Set(['default', 'ares', 'mono', 'slate', 'daylight', 'warm-lightmode', 'poseidon', 'sisyphus', 'charizard']) const HERMES_RUNTIME_FOOTER_FIELDS = new Set(['model', 'context_pct', 'cwd', 'duration', 'tokens', 'cost']) +const HERMES_TUI_STATUS_INDICATORS = new Set(['kaomoji', 'emoji', 'unicode', 'ascii']) +const HERMES_COPY_SHORTCUTS = new Set(['auto', 'ctrl_c', 'ctrl_shift_c', 'disabled']) const HERMES_HOOK_EVENTS = new Set([ 'pre_tool_call', 'post_tool_call', @@ -3817,6 +3819,20 @@ function normalizeHermesDisplayFinalResponseMarkdown(value, strict = false) { return 'strip' } +function normalizeHermesTuiStatusIndicator(value, strict = false) { + const mode = String(value ?? '').trim().toLowerCase() || 'kaomoji' + if (HERMES_TUI_STATUS_INDICATORS.has(mode)) return mode + if (strict) throw new Error('display.tui_status_indicator 必须是 kaomoji、emoji、unicode 或 ascii') + return 'kaomoji' +} + +function normalizeHermesCopyShortcut(value, strict = false) { + const mode = String(value ?? '').trim().toLowerCase() || 'auto' + if (HERMES_COPY_SHORTCUTS.has(mode)) return mode + if (strict) throw new Error('display.copy_shortcut 必须是 auto、ctrl_c、ctrl_shift_c 或 disabled') + return 'auto' +} + function normalizeHermesDisplayLanguage(value, strict = false) { const language = String(value ?? '').trim().toLowerCase() || 'en' if (HERMES_DISPLAY_LANGUAGE_VALUES.has(language)) return language @@ -3869,6 +3885,9 @@ export function buildHermesDisplayConfigValues(config = {}) { const runtimeFooter = display.runtime_footer && typeof display.runtime_footer === 'object' && !Array.isArray(display.runtime_footer) ? display.runtime_footer : {} + const userMessagePreview = display.user_message_preview && typeof display.user_message_preview === 'object' && !Array.isArray(display.user_message_preview) + ? display.user_message_preview + : {} return { displayCompact: readHermesBool(display.compact, false), displaySkin: normalizeHermesDisplaySkin(display.skin, false), @@ -3893,6 +3912,13 @@ export function buildHermesDisplayConfigValues(config = {}) { displayBellOnComplete: readHermesBool(display.bell_on_complete, false), displayPersistentOutput: readHermesBool(display.persistent_output, true), displayPersistentOutputMaxLines: parseHermesInteger(display.persistent_output_max_lines, 'display.persistent_output_max_lines', 200, 0, 100000, false), + displayInlineDiffs: readHermesBool(display.inline_diffs, true), + displayTuiAutoResumeRecent: readHermesBool(display.tui_auto_resume_recent, false), + displayTuiStatusIndicator: normalizeHermesTuiStatusIndicator(display.tui_status_indicator, false), + displayUserMessagePreviewFirstLines: parseHermesInteger(userMessagePreview.first_lines, 'display.user_message_preview.first_lines', 2, 1, 100, false), + displayUserMessagePreviewLastLines: parseHermesInteger(userMessagePreview.last_lines, 'display.user_message_preview.last_lines', 2, 0, 100, false), + displayEphemeralSystemTtl: parseHermesInteger(display.ephemeral_system_ttl, 'display.ephemeral_system_ttl', 0, 0, 86400, false), + displayCopyShortcut: normalizeHermesCopyShortcut(display.copy_shortcut, false), } } @@ -3905,6 +3931,9 @@ export function mergeHermesDisplayConfig(config = {}, form = {}) { const runtimeFooter = display.runtime_footer && typeof display.runtime_footer === 'object' && !Array.isArray(display.runtime_footer) ? mergeConfigsPreservingFields(display.runtime_footer, {}) : {} + const userMessagePreview = display.user_message_preview && typeof display.user_message_preview === 'object' && !Array.isArray(display.user_message_preview) + ? mergeConfigsPreservingFields(display.user_message_preview, {}) + : {} display.compact = formHermesBool(form, 'displayCompact', currentValues.displayCompact) display.skin = normalizeHermesDisplaySkin(Object.hasOwn(form, 'displaySkin') ? form.displaySkin : currentValues.displaySkin, true) @@ -3929,6 +3958,14 @@ export function mergeHermesDisplayConfig(config = {}, form = {}) { display.bell_on_complete = formHermesBool(form, 'displayBellOnComplete', currentValues.displayBellOnComplete) display.persistent_output = formHermesBool(form, 'displayPersistentOutput', currentValues.displayPersistentOutput) display.persistent_output_max_lines = parseHermesInteger(Object.hasOwn(form, 'displayPersistentOutputMaxLines') ? form.displayPersistentOutputMaxLines : currentValues.displayPersistentOutputMaxLines, 'display.persistent_output_max_lines', 200, 0, 100000, true) + display.inline_diffs = formHermesBool(form, 'displayInlineDiffs', currentValues.displayInlineDiffs) + display.tui_auto_resume_recent = formHermesBool(form, 'displayTuiAutoResumeRecent', currentValues.displayTuiAutoResumeRecent) + display.tui_status_indicator = normalizeHermesTuiStatusIndicator(Object.hasOwn(form, 'displayTuiStatusIndicator') ? form.displayTuiStatusIndicator : currentValues.displayTuiStatusIndicator, true) + userMessagePreview.first_lines = parseHermesInteger(Object.hasOwn(form, 'displayUserMessagePreviewFirstLines') ? form.displayUserMessagePreviewFirstLines : currentValues.displayUserMessagePreviewFirstLines, 'display.user_message_preview.first_lines', 2, 1, 100, true) + userMessagePreview.last_lines = parseHermesInteger(Object.hasOwn(form, 'displayUserMessagePreviewLastLines') ? form.displayUserMessagePreviewLastLines : currentValues.displayUserMessagePreviewLastLines, 'display.user_message_preview.last_lines', 2, 0, 100, true) + display.user_message_preview = userMessagePreview + display.ephemeral_system_ttl = parseHermesInteger(Object.hasOwn(form, 'displayEphemeralSystemTtl') ? form.displayEphemeralSystemTtl : currentValues.displayEphemeralSystemTtl, 'display.ephemeral_system_ttl', 0, 0, 86400, true) + display.copy_shortcut = normalizeHermesCopyShortcut(Object.hasOwn(form, 'displayCopyShortcut') ? form.displayCopyShortcut : currentValues.displayCopyShortcut, true) next.display = display const dashboard = next.dashboard && typeof next.dashboard === 'object' && !Array.isArray(next.dashboard) ? mergeConfigsPreservingFields(next.dashboard, {}) diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index d3aa0ce..45f0fbe 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -6463,6 +6463,8 @@ const HERMES_DISPLAY_LANGUAGE_VALUES: &[&str] = &[ const HERMES_DISPLAY_BUSY_INPUT_MODES: &[&str] = &["interrupt", "queue", "steer"]; const HERMES_DISPLAY_BACKGROUND_PROCESS_NOTIFICATIONS: &[&str] = &["off", "result", "error", "all"]; const HERMES_DISPLAY_FINAL_RESPONSE_MARKDOWN_VALUES: &[&str] = &["render", "strip", "raw"]; +const HERMES_TUI_STATUS_INDICATORS: &[&str] = &["kaomoji", "emoji", "unicode", "ascii"]; +const HERMES_COPY_SHORTCUTS: &[&str] = &["auto", "ctrl_c", "ctrl_shift_c", "disabled"]; const HERMES_DISPLAY_SKINS: &[&str] = &[ "default", "ares", @@ -6586,6 +6588,41 @@ fn normalize_hermes_display_final_response_markdown( } } +fn normalize_hermes_tui_status_indicator( + value: Option, + strict: bool, +) -> Result { + let mode = value.unwrap_or_default().trim().to_ascii_lowercase(); + let mode = if mode.is_empty() { + "kaomoji".to_string() + } else { + mode + }; + if HERMES_TUI_STATUS_INDICATORS.contains(&mode.as_str()) { + Ok(mode) + } else if strict { + Err("display.tui_status_indicator 必须是 kaomoji、emoji、unicode 或 ascii".to_string()) + } else { + Ok("kaomoji".to_string()) + } +} + +fn normalize_hermes_copy_shortcut(value: Option, strict: bool) -> Result { + let mode = value.unwrap_or_default().trim().to_ascii_lowercase(); + let mode = if mode.is_empty() { + "auto".to_string() + } else { + mode + }; + if HERMES_COPY_SHORTCUTS.contains(&mode.as_str()) { + Ok(mode) + } else if strict { + Err("display.copy_shortcut 必须是 auto、ctrl_c、ctrl_shift_c 或 disabled".to_string()) + } else { + Ok("auto".to_string()) + } +} + fn normalize_hermes_runtime_footer_fields_text( value: Option, strict: bool, @@ -6668,6 +6705,8 @@ fn build_hermes_display_config_values(config: &serde_yaml::Value) -> Value { let display = root.and_then(|map| yaml_get_mapping(map, "display")); let dashboard = root.and_then(|map| yaml_get_mapping(map, "dashboard")); let runtime_footer = display.and_then(|map| yaml_get_mapping(map, "runtime_footer")); + let user_message_preview = + display.and_then(|map| yaml_get_mapping(map, "user_message_preview")); let runtime_footer_fields = normalize_hermes_runtime_footer_fields( runtime_footer.and_then(|map| yaml_get(map, "fields")), false, @@ -6733,6 +6772,25 @@ fn build_hermes_display_config_values(config: &serde_yaml::Value) -> Value { "displayPersistentOutputMaxLines": display .map(|map| bounded_hermes_i64(yaml_i64_field(map, "persistent_output_max_lines"), 200, 0, 100000)) .unwrap_or(200), + "displayInlineDiffs": display.and_then(|map| yaml_bool_field(map, "inline_diffs")).unwrap_or(true), + "displayTuiAutoResumeRecent": display.and_then(|map| yaml_bool_field(map, "tui_auto_resume_recent")).unwrap_or(false), + "displayTuiStatusIndicator": normalize_hermes_tui_status_indicator( + display.and_then(|map| yaml_string_field(map, "tui_status_indicator")), + false, + ).unwrap_or_else(|_| "kaomoji".to_string()), + "displayUserMessagePreviewFirstLines": user_message_preview + .map(|map| bounded_hermes_i64(yaml_i64_field(map, "first_lines"), 2, 1, 100)) + .unwrap_or(2), + "displayUserMessagePreviewLastLines": user_message_preview + .map(|map| bounded_hermes_i64(yaml_i64_field(map, "last_lines"), 2, 0, 100)) + .unwrap_or(2), + "displayEphemeralSystemTtl": display + .map(|map| bounded_hermes_i64(yaml_i64_field(map, "ephemeral_system_ttl"), 0, 0, 86400)) + .unwrap_or(0), + "displayCopyShortcut": normalize_hermes_copy_shortcut( + display.and_then(|map| yaml_string_field(map, "copy_shortcut")), + false, + ).unwrap_or_else(|_| "auto".to_string()), }) } @@ -6773,6 +6831,30 @@ fn merge_hermes_display_config(config: &mut serde_yaml::Value, form: &Value) -> 0, 100000, )?; + let user_message_preview_first_lines = validate_hermes_i64( + form_i64(form, "displayUserMessagePreviewFirstLines") + .or_else(|| current["displayUserMessagePreviewFirstLines"].as_i64()), + "display.user_message_preview.first_lines", + 2, + 1, + 100, + )?; + let user_message_preview_last_lines = validate_hermes_i64( + form_i64(form, "displayUserMessagePreviewLastLines") + .or_else(|| current["displayUserMessagePreviewLastLines"].as_i64()), + "display.user_message_preview.last_lines", + 2, + 0, + 100, + )?; + let ephemeral_system_ttl = validate_hermes_i64( + form_i64(form, "displayEphemeralSystemTtl") + .or_else(|| current["displayEphemeralSystemTtl"].as_i64()), + "display.ephemeral_system_ttl", + 0, + 0, + 86400, + )?; let tool_preview_length = validate_hermes_i64( form_i64(form, "displayToolPreviewLength") .or_else(|| current["displayToolPreviewLength"].as_i64()), @@ -6938,6 +7020,58 @@ fn merge_hermes_display_config(config: &mut serde_yaml::Value, form: &Value) -> yaml_key("persistent_output_max_lines"), serde_yaml::Value::Number(serde_yaml::Number::from(persistent_output_max_lines)), ); + display.insert( + yaml_key("inline_diffs"), + serde_yaml::Value::Bool( + form_bool(form, "displayInlineDiffs") + .unwrap_or_else(|| current["displayInlineDiffs"].as_bool().unwrap_or(true)), + ), + ); + display.insert( + yaml_key("tui_auto_resume_recent"), + serde_yaml::Value::Bool( + form_bool(form, "displayTuiAutoResumeRecent").unwrap_or_else(|| { + current["displayTuiAutoResumeRecent"] + .as_bool() + .unwrap_or(false) + }), + ), + ); + display.insert( + yaml_key("tui_status_indicator"), + serde_yaml::Value::String(normalize_hermes_tui_status_indicator( + form_string(form, "displayTuiStatusIndicator").or_else(|| { + current["displayTuiStatusIndicator"] + .as_str() + .map(ToString::to_string) + }), + true, + )?), + ); + display.insert( + yaml_key("ephemeral_system_ttl"), + serde_yaml::Value::Number(serde_yaml::Number::from(ephemeral_system_ttl)), + ); + display.insert( + yaml_key("copy_shortcut"), + serde_yaml::Value::String(normalize_hermes_copy_shortcut( + form_string(form, "displayCopyShortcut").or_else(|| { + current["displayCopyShortcut"] + .as_str() + .map(ToString::to_string) + }), + true, + )?), + ); + let user_message_preview = yaml_child_object(display, "user_message_preview")?; + user_message_preview.insert( + yaml_key("first_lines"), + serde_yaml::Value::Number(serde_yaml::Number::from(user_message_preview_first_lines)), + ); + user_message_preview.insert( + yaml_key("last_lines"), + serde_yaml::Value::Number(serde_yaml::Number::from(user_message_preview_last_lines)), + ); let runtime_footer = yaml_child_object(display, "runtime_footer")?; runtime_footer.insert( yaml_key("enabled"), @@ -22816,6 +22950,13 @@ mod hermes_display_config_tests { assert_eq!(values["displayBellOnComplete"], false); assert_eq!(values["displayPersistentOutput"], true); assert_eq!(values["displayPersistentOutputMaxLines"], 200); + assert_eq!(values["displayInlineDiffs"], true); + assert_eq!(values["displayTuiAutoResumeRecent"], false); + assert_eq!(values["displayTuiStatusIndicator"], "kaomoji"); + assert_eq!(values["displayUserMessagePreviewFirstLines"], 2); + assert_eq!(values["displayUserMessagePreviewLastLines"], 2); + assert_eq!(values["displayEphemeralSystemTtl"], 0); + assert_eq!(values["displayCopyShortcut"], "auto"); } #[test] @@ -22849,6 +22990,14 @@ display: bell_on_complete: true persistent_output: false persistent_output_max_lines: 80 + inline_diffs: false + tui_auto_resume_recent: true + tui_status_indicator: EMOJI + user_message_preview: + first_lines: 3 + last_lines: 1 + ephemeral_system_ttl: 120 + copy_shortcut: CTRL_SHIFT_C dashboard: show_token_analytics: true "#, @@ -22881,6 +23030,13 @@ dashboard: assert_eq!(values["displayBellOnComplete"], true); assert_eq!(values["displayPersistentOutput"], false); assert_eq!(values["displayPersistentOutputMaxLines"], 80); + assert_eq!(values["displayInlineDiffs"], false); + assert_eq!(values["displayTuiAutoResumeRecent"], true); + assert_eq!(values["displayTuiStatusIndicator"], "emoji"); + assert_eq!(values["displayUserMessagePreviewFirstLines"], 3); + assert_eq!(values["displayUserMessagePreviewLastLines"], 1); + assert_eq!(values["displayEphemeralSystemTtl"], 120); + assert_eq!(values["displayCopyShortcut"], "ctrl_shift_c"); } #[test] @@ -22894,9 +23050,12 @@ display: runtime_footer: enabled: false custom_flag: keep-footer + user_message_preview: + custom_flag: keep-preview platforms: telegram: tool_progress: new + custom_flag: keep-display dashboard: custom_flag: keep-dashboard memory: @@ -22931,6 +23090,13 @@ memory: "displayBellOnComplete": true, "displayPersistentOutput": false, "displayPersistentOutputMaxLines": 120, + "displayInlineDiffs": false, + "displayTuiAutoResumeRecent": true, + "displayTuiStatusIndicator": "ascii", + "displayUserMessagePreviewFirstLines": 4, + "displayUserMessagePreviewLastLines": 0, + "displayEphemeralSystemTtl": 360, + "displayCopyShortcut": "disabled", }), ) .unwrap(); @@ -23010,6 +23176,39 @@ memory: config["display"]["persistent_output_max_lines"].as_i64(), Some(120) ); + assert_eq!(config["display"]["inline_diffs"].as_bool(), Some(false)); + assert_eq!( + config["display"]["tui_auto_resume_recent"].as_bool(), + Some(true) + ); + assert_eq!( + config["display"]["tui_status_indicator"].as_str(), + Some("ascii") + ); + assert_eq!( + config["display"]["user_message_preview"]["first_lines"].as_i64(), + Some(4) + ); + assert_eq!( + config["display"]["user_message_preview"]["last_lines"].as_i64(), + Some(0) + ); + assert_eq!( + config["display"]["user_message_preview"]["custom_flag"].as_str(), + Some("keep-preview") + ); + assert_eq!( + config["display"]["ephemeral_system_ttl"].as_i64(), + Some(360) + ); + assert_eq!( + config["display"]["copy_shortcut"].as_str(), + Some("disabled") + ); + assert_eq!( + config["display"]["custom_flag"].as_str(), + Some("keep-display") + ); } #[test] @@ -23081,6 +23280,39 @@ memory: ) .unwrap_err(); assert!(err.contains("display.tool_preview_length")); + + let err = merge_hermes_display_config( + &mut config, + &json!({ "displayTuiStatusIndicator": "rainbow" }), + ) + .unwrap_err(); + assert!(err.contains("display.tui_status_indicator")); + + let err = + merge_hermes_display_config(&mut config, &json!({ "displayCopyShortcut": "cmd_c" })) + .unwrap_err(); + assert!(err.contains("display.copy_shortcut")); + + let err = merge_hermes_display_config( + &mut config, + &json!({ "displayUserMessagePreviewFirstLines": 0 }), + ) + .unwrap_err(); + assert!(err.contains("display.user_message_preview.first_lines")); + + let err = merge_hermes_display_config( + &mut config, + &json!({ "displayUserMessagePreviewLastLines": 101 }), + ) + .unwrap_err(); + assert!(err.contains("display.user_message_preview.last_lines")); + + let err = merge_hermes_display_config( + &mut config, + &json!({ "displayEphemeralSystemTtl": 86401 }), + ) + .unwrap_err(); + assert!(err.contains("display.ephemeral_system_ttl")); } } diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index a8687d0..2d6f8c9 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -211,6 +211,13 @@ const DISPLAY_DEFAULTS = { displayBellOnComplete: false, displayPersistentOutput: true, displayPersistentOutputMaxLines: 200, + displayInlineDiffs: true, + displayTuiAutoResumeRecent: false, + displayTuiStatusIndicator: 'kaomoji', + displayUserMessagePreviewFirstLines: 2, + displayUserMessagePreviewLastLines: 2, + displayEphemeralSystemTtl: 0, + displayCopyShortcut: 'auto', } const HUMAN_DELAY_DEFAULTS = { @@ -419,6 +426,8 @@ const DISPLAY_RESUME_VALUES = ['full', 'minimal'] const DISPLAY_BUSY_INPUT_MODES = ['interrupt', 'queue', 'steer'] const DISPLAY_BACKGROUND_PROCESS_NOTIFICATIONS = ['off', 'result', 'error', 'all'] const DISPLAY_FINAL_RESPONSE_MARKDOWN_VALUES = ['render', 'strip', 'raw'] +const DISPLAY_TUI_STATUS_INDICATORS = ['kaomoji', 'emoji', 'unicode', 'ascii'] +const DISPLAY_COPY_SHORTCUTS = ['auto', 'ctrl_c', 'ctrl_shift_c', 'disabled'] const HUMAN_DELAY_MODES = ['off', 'natural', 'custom'] const APPROVAL_MODES = ['manual', 'smart', 'off'] const APPROVAL_CRON_MODES = ['deny', 'approve'] @@ -1786,6 +1795,30 @@ export function render() { ${t('engine.hermesDisplayConfigPersistentOutputMaxLines')} + + + + + + +
${t('engine.hermesDisplayConfigFootnote')}
@@ -4512,6 +4553,13 @@ export function render() { displayBellOnComplete: !!el.querySelector('#hm-display-bell-on-complete')?.checked, displayPersistentOutput: !!el.querySelector('#hm-display-persistent-output')?.checked, displayPersistentOutputMaxLines: el.querySelector('#hm-display-persistent-output-max-lines')?.value || '200', + displayInlineDiffs: !!el.querySelector('#hm-display-inline-diffs')?.checked, + displayTuiAutoResumeRecent: !!el.querySelector('#hm-display-tui-auto-resume-recent')?.checked, + displayTuiStatusIndicator: el.querySelector('#hm-display-tui-status-indicator')?.value || 'kaomoji', + displayUserMessagePreviewFirstLines: el.querySelector('#hm-display-user-message-preview-first-lines')?.value || '2', + displayUserMessagePreviewLastLines: el.querySelector('#hm-display-user-message-preview-last-lines')?.value || '2', + displayEphemeralSystemTtl: el.querySelector('#hm-display-ephemeral-system-ttl')?.value || '0', + displayCopyShortcut: el.querySelector('#hm-display-copy-shortcut')?.value || 'auto', } displaySaving = true displayError = null diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index 67d3b60..415356b 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -1130,7 +1130,7 @@ export default { hermesUnauthorizedDmConfigBehavior_ignore: _('静默忽略', 'Silently ignore', '靜默忽略'), hermesUnauthorizedDmConfigFootnote: _('pair 是默认值,会拒绝访问但在私信中回复一次性配对码;ignore 会静默丢弃陌生私信。平台级覆盖仍可在渠道配置或 raw YAML 中单独设置。', 'pair is the default: Hermes denies access but replies with a one-time pairing code in DMs. ignore silently drops unknown DMs. Platform-level overrides can still be set in channel settings or raw YAML.', 'pair 是預設值,會拒絕存取但在私訊中回覆一次性配對碼;ignore 會靜默丟棄陌生私訊。平台級覆蓋仍可在頻道設定或 raw YAML 中單獨設定。'), hermesDisplayConfigTitle: _('全局显示与可靠性', 'Global display and reliability', '全域顯示與可靠性'), - hermesDisplayConfigDesc: _('控制消息平台和 CLI 的默认进度展示、工具预览、工具输出前缀、推理展示、进度清理、横幅紧凑模式、显示皮肤、最终回复 Markdown、时间戳、完成提醒、终端输出恢复、忙时输入、后台进程通知、静态提示语言、运行信息页脚,以及文件写入失败校验。', 'Control default progress display, tool previews, tool output prefix, reasoning visibility, progress cleanup, compact banner mode, display skin, final-response Markdown, timestamps, completion bell, terminal output recovery, busy input handling, background process notifications, static prompt language, runtime footer, and failed file-mutation verification for messaging platforms and CLI.', '控制訊息平台和 CLI 的預設進度顯示、工具預覽、工具輸出前綴、推理展示、進度清理、橫幅緊湊模式、顯示皮膚、最終回覆 Markdown、時間戳、完成提醒、終端輸出恢復、忙時輸入、背景程序通知、靜態提示語言、執行資訊頁腳,以及檔案寫入失敗校驗。'), + hermesDisplayConfigDesc: _('控制消息平台和 CLI 的默认进度展示、工具预览、工具输出前缀、推理展示、进度清理、横幅紧凑模式、显示皮肤、最终回复 Markdown、时间戳、完成提醒、终端输出恢复、忙时输入、TUI 状态、消息预览、复制快捷键、后台进程通知、静态提示语言、运行信息页脚,以及文件写入失败校验。', 'Control default progress display, tool previews, tool output prefix, reasoning visibility, progress cleanup, compact banner mode, display skin, final-response Markdown, timestamps, completion bell, terminal output recovery, busy input handling, TUI status, message previews, copy shortcuts, background process notifications, static prompt language, runtime footer, and failed file-mutation verification for messaging platforms and CLI.', '控制訊息平台和 CLI 的預設進度顯示、工具預覽、工具輸出前綴、推理展示、進度清理、橫幅緊湊模式、顯示皮膚、最終回覆 Markdown、時間戳、完成提醒、終端輸出恢復、忙時輸入、TUI 狀態、訊息預覽、複製快捷鍵、背景程序通知、靜態提示語言、執行資訊頁腳,以及檔案寫入失敗校驗。'), hermesDisplayConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), hermesDisplayConfigSave: _('保存显示设置', 'Save display settings', '儲存顯示設定'), hermesDisplayConfigSaveSuccess: _('显示与可靠性配置已保存,建议重启 Hermes Gateway 生效', 'Display and reliability settings saved. Restart Hermes Gateway to take effect.', '顯示與可靠性設定已儲存,建議重啟 Hermes Gateway 生效'), @@ -1200,7 +1200,22 @@ export default { hermesDisplayConfigBellOnComplete: _('任务完成时播放提示音', 'Play bell when runs complete', '任務完成時播放提示音'), hermesDisplayConfigPersistentOutput: _('保留终端输出用于恢复显示', 'Keep terminal output for display recovery', '保留終端輸出用於恢復顯示'), hermesDisplayConfigPersistentOutputMaxLines: _('保留输出最大行数', 'Max retained output lines', '保留輸出最大行數'), - hermesDisplayConfigFootnote: _('这里写入全局 display 配置;平台级覆盖仍在渠道页管理。工具预览长度为 0 时使用 Hermes 默认长度;清理进度只会在支持消息删除的平台成功完成后生效。紧凑横幅和显示皮肤影响 CLI 启动展示。忙时输入控制长跑期间新消息如何处理,后台进程通知控制 messaging watcher 噪音。最终回复 Markdown、时间戳、完成铃声和持久输出影响 CLI 可读性与终端重绘恢复。display.streaming 是 CLI-only,本面板不会把它误写成 Gateway 全局流式设置。运行信息字段支持 model、context_pct、cwd、duration、tokens、cost。成本与 Token 分析是本地 lower-bound 估算,不等同于 provider 账单。', 'This writes global display settings; per-platform overrides remain in channel settings. Tool preview length 0 lets Hermes use its default length; progress cleanup only applies after successful turns on platforms that support message deletion. Compact banner and display skin affect CLI startup presentation. Busy input controls how new messages are handled during long runs, and background process notifications tune messaging watcher noise. Final-response Markdown, timestamps, completion bell, and persistent output affect CLI readability and terminal redraw recovery. display.streaming is CLI-only, so this panel does not write it as a global Gateway streaming setting. Runtime footer fields support model, context_pct, cwd, duration, tokens, and cost. Cost and token analytics are local lower-bound estimates, not provider billing records.', '這裡寫入全域 display 設定;平台級覆蓋仍在頻道頁管理。工具預覽長度為 0 時使用 Hermes 預設長度;清理進度只會在支援訊息刪除的平台成功完成後生效。緊湊橫幅和顯示皮膚會影響 CLI 啟動展示。忙時輸入控制長跑期間新訊息如何處理,背景程序通知控制 messaging watcher 噪音。最終回覆 Markdown、時間戳、完成鈴聲和持久輸出會影響 CLI 可讀性與終端重繪恢復。display.streaming 是 CLI-only,本面板不會把它誤寫成 Gateway 全域串流設定。執行資訊欄位支援 model、context_pct、cwd、duration、tokens、cost。成本與 Token 分析是本地 lower-bound 估算,不等同於 provider 帳單。'), + hermesDisplayConfigInlineDiffs: _('在 CLI 中显示写入差异预览', 'Show inline write diff previews in CLI', '在 CLI 中顯示寫入差異預覽'), + hermesDisplayConfigTuiAutoResumeRecent: _('TUI 自动恢复最近会话', 'TUI auto-resumes the recent session', 'TUI 自動恢復最近會話'), + hermesDisplayConfigTuiStatusIndicator: _('TUI 状态指示样式', 'TUI status indicator style', 'TUI 狀態指示樣式'), + hermesDisplayConfigTuiStatusIndicator_kaomoji: _('颜文字', 'Kaomoji', '顏文字'), + hermesDisplayConfigTuiStatusIndicator_emoji: _('Emoji', 'Emoji', 'Emoji'), + hermesDisplayConfigTuiStatusIndicator_unicode: _('Unicode 符号', 'Unicode symbols', 'Unicode 符號'), + hermesDisplayConfigTuiStatusIndicator_ascii: _('ASCII 字符', 'ASCII characters', 'ASCII 字元'), + hermesDisplayConfigUserMessagePreviewFirstLines: _('用户消息预览开头行数', 'User preview first lines', '使用者訊息預覽開頭行數'), + hermesDisplayConfigUserMessagePreviewLastLines: _('用户消息预览结尾行数', 'User preview last lines', '使用者訊息預覽結尾行數'), + hermesDisplayConfigEphemeralSystemTtl: _('临时系统消息保留秒数', 'Ephemeral system message TTL seconds', '臨時系統訊息保留秒數'), + hermesDisplayConfigCopyShortcut: _('终端复制快捷键', 'Terminal copy shortcut', '終端複製快捷鍵'), + hermesDisplayConfigCopyShortcut_auto: _('跟随平台默认', 'Use platform default', '跟隨平台預設'), + hermesDisplayConfigCopyShortcut_ctrl_c: _('Ctrl+C', 'Ctrl+C', 'Ctrl+C'), + hermesDisplayConfigCopyShortcut_ctrl_shift_c: _('Ctrl+Shift+C', 'Ctrl+Shift+C', 'Ctrl+Shift+C'), + hermesDisplayConfigCopyShortcut_disabled: _('禁用快捷键', 'Disable shortcut', '停用快捷鍵'), + hermesDisplayConfigFootnote: _('这里写入全局 display 配置;平台级覆盖仍在渠道页管理。工具预览长度为 0 时使用 Hermes 默认长度;清理进度只会在支持消息删除的平台成功完成后生效。紧凑横幅、显示皮肤、TUI 状态、用户消息预览和复制快捷键影响 CLI/TUI 操作体验;临时系统消息保留秒数影响 Gateway 系统消息,不会修改模型输出内容。display.streaming 是 CLI-only,本面板不会把它误写成 Gateway 全局流式设置。运行信息字段支持 model、context_pct、cwd、duration、tokens、cost。成本与 Token 分析是本地 lower-bound 估算,不等同于 provider 账单。', 'This writes global display settings; per-platform overrides remain in channel settings. Tool preview length 0 lets Hermes use its default length; progress cleanup only applies after successful turns on platforms that support message deletion. Compact banner, display skin, TUI status, user-message preview, and copy shortcut affect CLI/TUI operation. Ephemeral system message TTL affects Gateway system messages and does not change model output content. display.streaming is CLI-only, so this panel does not write it as a global Gateway streaming setting. Runtime footer fields support model, context_pct, cwd, duration, tokens, and cost. Cost and token analytics are local lower-bound estimates, not provider billing records.', '這裡寫入全域 display 設定;平台級覆蓋仍在頻道頁管理。工具預覽長度為 0 時使用 Hermes 預設長度;清理進度只會在支援訊息刪除的平台成功完成後生效。緊湊橫幅、顯示皮膚、TUI 狀態、使用者訊息預覽和複製快捷鍵會影響 CLI/TUI 操作體驗;臨時系統訊息保留秒數會影響 Gateway 系統訊息,不會修改模型輸出內容。display.streaming 是 CLI-only,本面板不會把它誤寫成 Gateway 全域串流設定。執行資訊欄位支援 model、context_pct、cwd、duration、tokens、cost。成本與 Token 分析是本地 lower-bound 估算,不等同於 provider 帳單。'), hermesHumanDelayConfigTitle: _('响应节奏', 'Response pacing', '回應節奏'), hermesHumanDelayConfigDesc: _('控制消息平台回复分块之间的等待时间,降低刷屏或模拟更自然发送节奏。', 'Control the wait time between reply chunks on messaging platforms to reduce flooding or mimic a more natural sending rhythm.', '控制訊息平台回覆分塊之間的等待時間,降低刷屏或模擬更自然的傳送節奏。'), hermesHumanDelayConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), diff --git a/tests/hermes-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js index 76500a7..f65eaf8 100644 --- a/tests/hermes-config-page-ui.test.js +++ b/tests/hermes-config-page-ui.test.js @@ -285,6 +285,13 @@ test('Hermes 配置页会暴露全局显示与可靠性结构化配置字段', ( 'hm-display-timestamps', 'hm-display-bell-on-complete', 'hm-display-persistent-output', + 'hm-display-inline-diffs', + 'hm-display-tui-auto-resume-recent', + 'hm-display-tui-status-indicator', + 'hm-display-user-message-preview-first-lines', + 'hm-display-user-message-preview-last-lines', + 'hm-display-ephemeral-system-ttl', + 'hm-display-copy-shortcut', ]) { assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`) } diff --git a/tests/hermes-display-config.test.js b/tests/hermes-display-config.test.js index fe823fd..02f8997 100644 --- a/tests/hermes-display-config.test.js +++ b/tests/hermes-display-config.test.js @@ -33,6 +33,13 @@ test('Hermes 显示配置读取会提供上游默认值', () => { displayBellOnComplete: false, displayPersistentOutput: true, displayPersistentOutputMaxLines: 200, + displayInlineDiffs: true, + displayTuiAutoResumeRecent: false, + displayTuiStatusIndicator: 'kaomoji', + displayUserMessagePreviewFirstLines: 2, + displayUserMessagePreviewLastLines: 2, + displayEphemeralSystemTtl: 0, + displayCopyShortcut: 'auto', }) }) @@ -63,6 +70,15 @@ test('Hermes 显示配置读取会规范化已有字段', () => { bell_on_complete: true, persistent_output: false, persistent_output_max_lines: 80, + inline_diffs: false, + tui_auto_resume_recent: true, + tui_status_indicator: 'EMOJI', + user_message_preview: { + first_lines: 3, + last_lines: 1, + }, + ephemeral_system_ttl: 120, + copy_shortcut: 'CTRL_SHIFT_C', }, dashboard: { show_token_analytics: true, @@ -92,6 +108,13 @@ test('Hermes 显示配置读取会规范化已有字段', () => { assert.equal(values.displayBellOnComplete, true) assert.equal(values.displayPersistentOutput, false) assert.equal(values.displayPersistentOutputMaxLines, 80) + assert.equal(values.displayInlineDiffs, false) + assert.equal(values.displayTuiAutoResumeRecent, true) + assert.equal(values.displayTuiStatusIndicator, 'emoji') + assert.equal(values.displayUserMessagePreviewFirstLines, 3) + assert.equal(values.displayUserMessagePreviewLastLines, 1) + assert.equal(values.displayEphemeralSystemTtl, 120) + assert.equal(values.displayCopyShortcut, 'ctrl_shift_c') }) test('Hermes 显示配置保存会保留未知 YAML 并写入 display', () => { @@ -103,9 +126,13 @@ test('Hermes 显示配置保存会保留未知 YAML 并写入 display', () => { enabled: false, custom_flag: 'keep-footer', }, + user_message_preview: { + custom_flag: 'keep-preview', + }, platforms: { telegram: { tool_progress: 'new' }, }, + custom_flag: 'keep-display', }, dashboard: { custom_flag: 'keep-dashboard', @@ -135,6 +162,13 @@ test('Hermes 显示配置保存会保留未知 YAML 并写入 display', () => { displayBellOnComplete: true, displayPersistentOutput: false, displayPersistentOutputMaxLines: 120, + displayInlineDiffs: false, + displayTuiAutoResumeRecent: true, + displayTuiStatusIndicator: 'ascii', + displayUserMessagePreviewFirstLines: 4, + displayUserMessagePreviewLastLines: 0, + displayEphemeralSystemTtl: 360, + displayCopyShortcut: 'disabled', }) assert.deepEqual(next.model, { provider: 'anthropic' }) @@ -165,6 +199,15 @@ test('Hermes 显示配置保存会保留未知 YAML 并写入 display', () => { assert.equal(next.display.bell_on_complete, true) assert.equal(next.display.persistent_output, false) assert.equal(next.display.persistent_output_max_lines, 120) + assert.equal(next.display.inline_diffs, false) + assert.equal(next.display.tui_auto_resume_recent, true) + assert.equal(next.display.tui_status_indicator, 'ascii') + assert.equal(next.display.user_message_preview.first_lines, 4) + assert.equal(next.display.user_message_preview.last_lines, 0) + assert.equal(next.display.user_message_preview.custom_flag, 'keep-preview') + assert.equal(next.display.ephemeral_system_ttl, 360) + assert.equal(next.display.copy_shortcut, 'disabled') + assert.equal(next.display.custom_flag, 'keep-display') }) test('Hermes 显示配置保存会拒绝非法枚举和页脚字段', () => { @@ -212,4 +255,24 @@ test('Hermes 显示配置保存会拒绝非法枚举和页脚字段', () => { () => mergeHermesDisplayConfig({}, { displayToolPreviewLength: '200001' }), /display\.tool_preview_length/, ) + assert.throws( + () => mergeHermesDisplayConfig({}, { displayTuiStatusIndicator: 'rainbow' }), + /display\.tui_status_indicator/, + ) + assert.throws( + () => mergeHermesDisplayConfig({}, { displayCopyShortcut: 'cmd_c' }), + /display\.copy_shortcut/, + ) + assert.throws( + () => mergeHermesDisplayConfig({}, { displayUserMessagePreviewFirstLines: '0' }), + /display\.user_message_preview\.first_lines/, + ) + assert.throws( + () => mergeHermesDisplayConfig({}, { displayUserMessagePreviewLastLines: '101' }), + /display\.user_message_preview\.last_lines/, + ) + assert.throws( + () => mergeHermesDisplayConfig({}, { displayEphemeralSystemTtl: '86401' }), + /display\.ephemeral_system_ttl/, + ) })