diff --git a/scripts/dev-api.js b/scripts/dev-api.js index cb66ca5..f72d21b 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -5203,6 +5203,7 @@ export function buildHermesSessionRuntimeConfigValues(config = {}) { atHour: parseHermesInteger(sessionReset.at_hour, 'at_hour', 4, 0, 23, false), groupSessionsPerUser: readHermesBool(root.group_sessions_per_user, true), threadSessionsPerUser: readHermesBool(root.thread_sessions_per_user, false), + worktreeEnabled: readHermesBool(root.worktree, false), } } @@ -5224,6 +5225,7 @@ export function mergeHermesSessionRuntimeConfig(config = {}, form = {}) { next.session_reset = sessionReset next.group_sessions_per_user = formHermesBool(form, 'groupSessionsPerUser', currentValues.groupSessionsPerUser) next.thread_sessions_per_user = formHermesBool(form, 'threadSessionsPerUser', currentValues.threadSessionsPerUser) + next.worktree = formHermesBool(form, 'worktreeEnabled', currentValues.worktreeEnabled) return next } diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 775db4d..1a23147 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -7908,6 +7908,9 @@ fn build_hermes_session_runtime_config_values(config: &serde_yaml::Value) -> Val let thread_sessions_per_user = root .and_then(|map| yaml_bool_field(map, "thread_sessions_per_user")) .unwrap_or(false); + let worktree_enabled = root + .and_then(|map| yaml_bool_field(map, "worktree")) + .unwrap_or(false); serde_json::json!({ "sessionResetMode": mode, @@ -7915,6 +7918,7 @@ fn build_hermes_session_runtime_config_values(config: &serde_yaml::Value) -> Val "atHour": at_hour, "groupSessionsPerUser": group_sessions_per_user, "threadSessionsPerUser": thread_sessions_per_user, + "worktreeEnabled": worktree_enabled, }) } @@ -7960,6 +7964,8 @@ fn merge_hermes_session_runtime_config( .unwrap_or_else(|| current["groupSessionsPerUser"].as_bool().unwrap_or(true)); let thread_sessions_per_user = form_bool(form, "threadSessionsPerUser") .unwrap_or_else(|| current["threadSessionsPerUser"].as_bool().unwrap_or(false)); + let worktree_enabled = form_bool(form, "worktreeEnabled") + .unwrap_or_else(|| current["worktreeEnabled"].as_bool().unwrap_or(false)); let root = ensure_yaml_object(config)?; let session_reset = yaml_child_object(root, "session_reset")?; @@ -7980,6 +7986,10 @@ fn merge_hermes_session_runtime_config( yaml_key("thread_sessions_per_user"), serde_yaml::Value::Bool(thread_sessions_per_user), ); + root.insert( + yaml_key("worktree"), + serde_yaml::Value::Bool(worktree_enabled), + ); Ok(()) } @@ -14379,6 +14389,31 @@ mod hermes_session_runtime_config_tests { assert_eq!(values["atHour"], 4); assert_eq!(values["groupSessionsPerUser"], true); assert_eq!(values["threadSessionsPerUser"], false); + assert_eq!(values["worktreeEnabled"], false); + } + + #[test] + fn session_runtime_values_read_worktree_flag() { + let config: serde_yaml::Value = serde_yaml::from_str( + r#" +session_reset: + mode: daily + idle_minutes: 720 + at_hour: 3 +group_sessions_per_user: false +thread_sessions_per_user: true +worktree: true +"#, + ) + .unwrap(); + let values = build_hermes_session_runtime_config_values(&config); + + assert_eq!(values["sessionResetMode"], "daily"); + assert_eq!(values["idleMinutes"], 720); + assert_eq!(values["atHour"], 3); + assert_eq!(values["groupSessionsPerUser"], false); + assert_eq!(values["threadSessionsPerUser"], true); + assert_eq!(values["worktreeEnabled"], true); } #[test] @@ -14406,6 +14441,7 @@ streaming: "atHour": "6", "groupSessionsPerUser": false, "threadSessionsPerUser": true, + "worktreeEnabled": true, }), ) .unwrap(); @@ -14421,6 +14457,7 @@ streaming: ); assert_eq!(config["group_sessions_per_user"].as_bool(), Some(false)); assert_eq!(config["thread_sessions_per_user"].as_bool(), Some(true)); + assert_eq!(config["worktree"].as_bool(), Some(true)); } #[test] diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index 5056376..6009e0d 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -12,6 +12,7 @@ const SESSION_RUNTIME_DEFAULTS = { atHour: 4, groupSessionsPerUser: true, threadSessionsPerUser: false, + worktreeEnabled: false, } const COMPRESSION_DEFAULTS = { @@ -498,6 +499,10 @@ export function render() { ${t('engine.hermesThreadSessionsPerUser')} +
${t('engine.hermesSessionRuntimeFootnote')}
@@ -2727,6 +2732,7 @@ export function render() { atHour: el.querySelector('#hm-session-at-hour')?.value || '4', groupSessionsPerUser: !!el.querySelector('#hm-group-sessions-per-user')?.checked, threadSessionsPerUser: !!el.querySelector('#hm-thread-sessions-per-user')?.checked, + worktreeEnabled: !!el.querySelector('#hm-worktree-enabled')?.checked, } runtimeSaving = true runtimeError = null diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index 88b5b5c..3db1d88 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -497,7 +497,8 @@ export default { hermesSessionAtHour: _('每日换新小时', 'Daily reset hour', '每日換新小時'), hermesGroupSessionsPerUser: _('群聊按成员隔离会话', 'Isolate group sessions per user', '群聊依成員隔離會話'), hermesThreadSessionsPerUser: _('线程也按成员隔离', 'Isolate thread sessions per user', '討論串也依成員隔離'), - hermesSessionRuntimeFootnote: _('推荐保持群聊隔离开启。关闭后,同一群/频道会共用上下文和中断状态。', 'Keeping group isolation on is recommended. Turning it off shares context and interrupt state across the same group or channel.', '建議保持群聊隔離開啟。關閉後,同一群組/頻道會共用上下文和中斷狀態。'), + hermesWorktreeEnabled: _('CLI 会话默认使用 Git worktree 隔离', 'Use Git worktree isolation for CLI sessions by default', 'CLI 會話預設使用 Git worktree 隔離'), + hermesSessionRuntimeFootnote: _('推荐保持群聊隔离开启;多人或多 Agent 同仓库长跑时,可开启 worktree 隔离来减少文件冲突。', 'Keeping group isolation on is recommended. For multi-user or multi-agent long runs in the same repository, enable worktree isolation to reduce file conflicts.', '建議保持群聊隔離開啟;多人或多 Agent 同倉庫長跑時,可啟用 worktree 隔離以減少檔案衝突。'), hermesTerminalConfigTitle: _('终端执行', 'Terminal execution', '終端執行'), hermesTerminalConfigDesc: _('控制 Hermes 工具命令的执行环境、工作目录、超时和容器资源,避免长任务卡死或沙箱范围误配。', 'Control command execution backend, working directory, timeouts, and container resources to avoid stuck runs or sandbox misconfiguration.', '控制 Hermes 工具命令的執行環境、工作目錄、逾時和容器資源,避免長任務卡死或沙箱範圍誤配。'), hermesTerminalConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), diff --git a/tests/hermes-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js index 3d2d52c..0f236bc 100644 --- a/tests/hermes-config-page-ui.test.js +++ b/tests/hermes-config-page-ui.test.js @@ -9,6 +9,20 @@ function extractEngineKeys() { return [...source.matchAll(/['"](engine\.[A-Za-z0-9_.-]+)['"]/g)].map(match => match[1]) } +test('Hermes 配置页会暴露会话安全结构化配置字段', () => { + for (const id of [ + 'hm-runtime-save', + 'hm-session-reset-mode', + 'hm-session-idle-minutes', + 'hm-session-at-hour', + 'hm-group-sessions-per-user', + 'hm-thread-sessions-per-user', + 'hm-worktree-enabled', + ]) { + assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`) + } +}) + test('Hermes 配置页会暴露工具循环防护结构化配置字段', () => { for (const id of [ 'hm-tool-guardrails-save', @@ -430,7 +444,8 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => { key.includes('CheckpointsConfig') || key.includes('ApprovalsConfig') || key.includes('CronConfig') || - key.includes('LoggingConfig') + key.includes('LoggingConfig') || + key.includes('Worktree') ))) assert.ok(keys.size > 0, '应能提取新增结构化配置用到的 engine 翻译 key') diff --git a/tests/hermes-session-runtime-config.test.js b/tests/hermes-session-runtime-config.test.js index 35a7293..d35fe07 100644 --- a/tests/hermes-session-runtime-config.test.js +++ b/tests/hermes-session-runtime-config.test.js @@ -15,6 +15,7 @@ test('Hermes 会话运行时配置读取会提供稳定表单默认值', () => { atHour: 4, groupSessionsPerUser: true, threadSessionsPerUser: false, + worktreeEnabled: false, }) }) @@ -27,6 +28,7 @@ test('Hermes 会话运行时配置读取会回显 session_reset 与隔离开关' }, group_sessions_per_user: false, thread_sessions_per_user: true, + worktree: true, }) assert.equal(values.sessionResetMode, 'daily') @@ -34,6 +36,7 @@ test('Hermes 会话运行时配置读取会回显 session_reset 与隔离开关' assert.equal(values.atHour, 3) assert.equal(values.groupSessionsPerUser, false) assert.equal(values.threadSessionsPerUser, true) + assert.equal(values.worktreeEnabled, true) }) test('Hermes 会话运行时配置保存会保留无关 YAML 并写入 snake_case 字段', () => { @@ -51,6 +54,7 @@ test('Hermes 会话运行时配置保存会保留无关 YAML 并写入 snake_cas atHour: '6', groupSessionsPerUser: false, threadSessionsPerUser: true, + worktreeEnabled: true, }) assert.deepEqual(next.model, { provider: 'anthropic', default: 'claude-sonnet-4-6' }) @@ -61,6 +65,7 @@ test('Hermes 会话运行时配置保存会保留无关 YAML 并写入 snake_cas assert.equal(next.session_reset.custom_flag, 'keep-me') assert.equal(next.group_sessions_per_user, false) assert.equal(next.thread_sessions_per_user, true) + assert.equal(next.worktree, true) }) test('Hermes 会话运行时配置保存会拒绝非法模式和范围', () => {