diff options
| -rw-r--r-- | justfile | 4 | ||||
| -rw-r--r-- | scripts/test_hermes_integration_asset.py | 90 | ||||
| -rw-r--r-- | src/detect/manifests/hermes.toml | 94 | ||||
| -rw-r--r-- | src/detect/mod.rs | 12 | ||||
| -rw-r--r-- | src/integration/assets/hermes/__init__.py | 63 | ||||
| -rw-r--r-- | src/integration/tests.rs | 8 | ||||
| -rw-r--r-- | src/terminal/state.rs | 152 | ||||
| -rw-r--r-- | website/agent-detection/hermes.toml | 94 |
8 files changed, 426 insertions, 91 deletions
@@ -3,7 +3,7 @@ # Run tests test: cargo nextest run --locked --status-level fail --final-status-level fail --failure-output final --success-output never - python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_preview scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty + python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_hermes_integration_asset scripts.test_preview scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty just integration-assets-test just plugin-marketplace-test @@ -29,7 +29,7 @@ windows-lint: # Check formatting + run unit tests + Windows target lint + maintenance script tests check: ci windows-lint - python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_preview scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty + python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_hermes_integration_asset scripts.test_preview scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty @echo "docs reminder: if this changes user-facing behavior, make sure the relevant release docs are updated or called out before release." # Install repo-local git hooks diff --git a/scripts/test_hermes_integration_asset.py b/scripts/test_hermes_integration_asset.py new file mode 100644 index 00000000..f3778214 --- /dev/null +++ b/scripts/test_hermes_integration_asset.py @@ -0,0 +1,90 @@ +import importlib.util +import unittest +from pathlib import Path + + +ASSET = Path(__file__).parents[1] / "src/integration/assets/hermes/__init__.py" + + +def load_asset(): + spec = importlib.util.spec_from_file_location("herdr_hermes_integration", ASSET) + if spec is None or spec.loader is None: + raise RuntimeError("could not load Hermes integration asset") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class FakeContext: + def __init__(self): + self.hooks = {} + + def register_hook(self, name, callback): + self.hooks[name] = callback + + +class HermesIntegrationAssetTests(unittest.TestCase): + def test_reports_only_root_session_identity(self): + module = load_asset() + calls = [] + module._send = lambda method, params: calls.append((method, params)) + context = FakeContext() + module.register(context) + + self.assertEqual( + set(context.hooks), + {"on_session_start", "on_session_reset", "pre_llm_call"}, + ) + + context.hooks["on_session_start"](session_id="root-1", platform="tui") + context.hooks["pre_llm_call"](session_id="root-1", platform="tui") + context.hooks["pre_llm_call"](session_id="child", platform="subagent") + context.hooks["on_session_reset"](session_id="root-2", platform="tui") + context.hooks["pre_llm_call"]( + session_id="background", platform="tui" + ) + + self.assertEqual( + calls, + [ + ( + "pane.report_agent_session", + { + "agent_session_id": "root-1", + "session_start_source": "startup", + }, + ), + ( + "pane.report_agent_session", + { + "agent_session_id": "root-2", + "session_start_source": "new", + }, + ), + ], + ) + + def test_first_turn_recovers_resumed_session_identity(self): + module = load_asset() + calls = [] + module._send = lambda method, params: calls.append((method, params)) + context = FakeContext() + module.register(context) + + context.hooks["pre_llm_call"](session_id="resumed", platform="cli") + + self.assertEqual( + calls, + [ + ( + "pane.report_agent_session", + { + "agent_session_id": "resumed", + "session_start_source": "resume", + }, + ) + ], + ) + +if __name__ == "__main__": + unittest.main() diff --git a/src/detect/manifests/hermes.toml b/src/detect/manifests/hermes.toml index d45fe32a..17542184 100644 --- a/src/detect/manifests/hermes.toml +++ b/src/detect/manifests/hermes.toml @@ -1,30 +1,102 @@ id = "hermes" -version = "2026.06.10.1" -min_engine_version = 1 -updated_at = "2026-06-10T00:00:00Z" +version = "2026.07.24.1" +min_engine_version = 2 +updated_at = "2026-07-24T19:12:54Z" aliases = ["hermes-agent"] [[rules]] +id = "osc_title_blocked" +state = "blocked" +priority = 1100 +region = "osc_title" +visible_blocker = true +regex = ['^⚠[\u{fe0e}\u{fe0f}]?(?:\s|$)'] + +[[rules]] +id = "osc_title_working" +state = "working" +priority = 1050 +region = "osc_title" +visible_working = true +regex = ['^⏳[\u{fe0e}\u{fe0f}]?(?:\s|$)'] + +[[rules]] id = "dangerous_command_approval" state = "blocked" -priority = 300 -region = "whole_recent" +priority = 900 +region = "bottom_non_empty_lines(14)" +visible_blocker = true +any = [ + { contains = ["dangerous"] }, + { contains = ["approval"] }, + { contains = ["allow once", "deny"] }, + { line_regex = ['(?i)^\s*[▸>]?\s*1\.\s*allow'] }, +] +all = [ + { any = [{ contains = ["enter confirm"] }, { contains = ["enter to confirm"] }, { contains = ["↑/↓ to select"] }, { contains = ["show full command"] }] }, +] + +[[rules]] +id = "clarification_prompt" +state = "blocked" +priority = 900 +region = "bottom_non_empty_lines(14)" +visible_blocker = true +any = [ + { contains = ["hermes needs your"] }, + { line_regex = ['^\s*ask\s+\S'] }, + { contains = ["type your answer"] }, +] +all = [ + { any = [{ contains = ["enter confirm"] }, { contains = ["enter to confirm"] }, { contains = ["enter send"] }, { contains = ["press enter"] }, { contains = ["↑/↓ select"] }, { contains = ["↑/↓ to select"] }, { contains = ["other (type"] }] }, +] + +[[rules]] +id = "credential_prompt" +state = "blocked" +priority = 900 +region = "bottom_non_empty_lines(14)" visible_blocker = true any = [ - { contains = ["dangerous command"] }, - { contains = ["allow once", "allow for this session", "deny"] }, + { contains = ["sudo password"] }, + { contains = ["skill setup"] }, + { contains = ["🔑", "for "] }, ] + +[[rules]] +id = "confirmation_prompt" +state = "blocked" +priority = 900 +region = "bottom_non_empty_lines(14)" +visible_blocker = true all = [ - { any = [{ contains = ["enter to confirm"] }, { contains = ["↑/↓ to select"] }, { contains = ["show full command"] }] }, + { any = [{ contains = ["approve once", "cancel"] }, { contains = ["start a new session", "keep going"] }] }, + { any = [{ contains = ["enter to confirm"] }, { contains = ["enter confirm"] }, { contains = ["type 1/2/3"] }, { contains = ["y/n quick"] }] }, ] [[rules]] id = "interrupt_status_working" state = "working" -priority = 100 -region = "whole_recent" +priority = 950 +region = "bottom_non_empty_lines(5)" visible_working = true any = [ { contains = ["msg=interrupt"] }, - { contains = ["ctrl+c cancel"] }, + { contains = ["ctrl+c to interrupt"] }, ] + +[[rules]] +id = "classic_cancel_working" +state = "working" +priority = 500 +region = "bottom_non_empty_lines(5)" +visible_working = true +contains = ["ctrl+c cancel"] + +[[rules]] +id = "osc_title_idle" +state = "idle" +priority = 100 +region = "osc_title" +visible_idle = true +regex = ['^✓[\u{fe0e}\u{fe0f}]?(?:\s|$)'] diff --git a/src/detect/mod.rs b/src/detect/mod.rs index b8eb8f60..33c4361c 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -286,13 +286,16 @@ pub(crate) fn full_lifecycle_hook_authority(source: &str, agent_label: &str) -> ("herdr:pi", "pi") | ("herdr:omp", "omp") | ("herdr:mastracode", "mastracode") - | ("herdr:hermes", "hermes") | ("herdr:opencode", "opencode") | ("herdr:kilo", "kilo") | ("herdr:kimi", "kimi") ) } +pub(crate) fn session_identity_only_integration(source: &str, agent_label: &str) -> bool { + (source, agent_label) == ("herdr:hermes", "hermes") +} + // --------------------------------------------------------------------------- // Process identification (platform-specific) // --------------------------------------------------------------------------- @@ -768,6 +771,13 @@ mod tests { } #[test] + fn hermes_session_integration_leaves_state_to_screen_detection() { + assert!(!full_lifecycle_hook_authority("herdr:hermes", "hermes")); + assert!(session_identity_only_integration("herdr:hermes", "hermes")); + assert!(Agent::SCREEN_MANIFEST_AGENTS.contains(&Agent::Hermes)); + } + + #[test] fn identify_unknown_processes() { assert_eq!(identify_agent("bash"), None); assert_eq!(identify_agent("zsh"), None); diff --git a/src/integration/assets/hermes/__init__.py b/src/integration/assets/hermes/__init__.py index 5474e5e3..9b1d51e2 100644 --- a/src/integration/assets/hermes/__init__.py +++ b/src/integration/assets/hermes/__init__.py @@ -1,4 +1,4 @@ -"""Hermes plugin installed by Herdr to report agent lifecycle state.""" +"""Hermes plugin installed by Herdr to report resumable session identity.""" # HERDR_INTEGRATION_ID=hermes # HERDR_INTEGRATION_VERSION=4 @@ -13,6 +13,7 @@ import time _SOURCE = "herdr:hermes" _AGENT = "hermes" +_INTERACTIVE_PLATFORMS = {"cli", "tui", "desktop", "acp"} def _base_params() -> tuple[str, str] | None: @@ -56,53 +57,35 @@ def _send(method: str, params: dict) -> None: pass -def _session_id(kwargs: dict) -> str | None: - value = kwargs.get("session_id") - if isinstance(value, str) and value: - return value - return None - - -def _report(state: str, **kwargs) -> None: - params = {"state": state} - session_id = _session_id(kwargs) - if session_id: - params["agent_session_id"] = session_id - _send("pane.report_agent", params) +def _report_session(start_source: str, **kwargs) -> None: + if kwargs.get("platform") not in _INTERACTIVE_PLATFORMS: + return + session_id = kwargs.get("session_id") + if not isinstance(session_id, str) or not session_id: + return + _send( + "pane.report_agent_session", + { + "agent_session_id": session_id, + "session_start_source": start_source, + }, + ) def _session_started(**kwargs) -> None: - session_id = _session_id(kwargs) - if session_id: - _send( - "pane.report_agent_session", - { - "agent_session_id": session_id, - "session_start_source": "startup", - }, - ) - _idle(**kwargs) - - -def _working(**kwargs) -> None: - _report("working", **kwargs) + _report_session("startup", **kwargs) -def _blocked(**kwargs) -> None: - _report("blocked", **kwargs) +def _session_reset(**kwargs) -> None: + _report_session("new", **kwargs) -def _idle(**kwargs) -> None: - _report("idle", **kwargs) +def _session_observed(**kwargs) -> None: + if kwargs.get("platform") == "cli": + _report_session("resume", **kwargs) def register(ctx): ctx.register_hook("on_session_start", _session_started) - ctx.register_hook("pre_llm_call", _working) - ctx.register_hook("pre_api_request", _working) - ctx.register_hook("pre_tool_call", _working) - ctx.register_hook("post_tool_call", _working) - ctx.register_hook("pre_approval_request", _blocked) - ctx.register_hook("post_approval_response", _working) - ctx.register_hook("post_llm_call", _idle) - ctx.register_hook("on_session_end", _idle) + ctx.register_hook("on_session_reset", _session_reset) + ctx.register_hook("pre_llm_call", _session_observed) diff --git a/src/integration/tests.rs b/src/integration/tests.rs index f695bf52..6e3c756c 100644 --- a/src/integration/tests.rs +++ b/src/integration/tests.rs @@ -2747,14 +2747,6 @@ fn bundled_integration_assets_report_session_refs() { assert!(KILO_PLUGIN_ASSET.contains("session_start_source: \"startup\"")); assert!(KILO_PLUGIN_ASSET.contains("reportState")); assert!(!KILO_PLUGIN_ASSET.contains("pane.release_agent")); - assert!(HERMES_PLUGIN_INIT_ASSET.contains("session_id = _session_id(kwargs)")); - assert!(HERMES_PLUGIN_INIT_ASSET.contains("agent_session_id")); - assert!(HERMES_PLUGIN_INIT_ASSET.contains("pane.report_agent_session\",")); - assert!(HERMES_PLUGIN_INIT_ASSET.contains("\"session_start_source\": \"startup\"")); - assert!(HERMES_PLUGIN_INIT_ASSET.contains("pane.report_agent\",")); - assert!(HERMES_PLUGIN_INIT_ASSET.contains("on_session_end")); - assert!(!HERMES_PLUGIN_INIT_ASSET.contains("on_session_finalize")); - assert!(!HERMES_PLUGIN_INIT_ASSET.contains("pane.release_agent")); assert!(QODERCLI_HOOK_ASSET.contains("HERDR_PANE_ID")); assert!(QODERCLI_HOOK_ASSET.contains("session_id")); assert!(QODERCLI_HOOK_ASSET.contains("report-agent-session")); diff --git a/src/terminal/state.rs b/src/terminal/state.rs index 4485c773..e806967e 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -605,6 +605,9 @@ impl TerminalState { seq: Option<u64>, now: Instant, ) -> Option<TerminalStateMutation> { + if crate::detect::session_identity_only_integration(&source, &agent_label) { + return None; + } if !crate::detect::full_lifecycle_hook_authority(&source, &agent_label) && self.recent_agent_process_exit.is_some_and(|exit| { crate::detect::parse_agent_label(&agent_label) == Some(exit.agent) @@ -1280,6 +1283,7 @@ impl TerminalState { "codex", Some("startup" | "clear" | "resume" | "compact") ) | ("herdr:mastracode", "mastracode", Some("startup")) + | ("herdr:hermes", "hermes", Some("startup" | "new" | "resume")) | ("herdr:opencode", "opencode", Some("new")) | ("herdr:pi", "pi", Some("new" | "resume" | "fork")) | ( @@ -1414,6 +1418,21 @@ impl TerminalState { &agent_label, session_start_source.as_deref(), ); + let replacing_hermes_session = + crate::detect::session_identity_only_integration(&source, &agent_label) + && session_replacement_allowed + && self.current_session_identity_for_persistence().is_some_and( + |(current_source, current_agent, current_kind, current_value)| { + current_source == source + && current_agent == agent_label + && current_kind == crate::agent_resume::AgentSessionRefKind::Id + && session_ref.kind == crate::agent_resume::AgentSessionRefKind::Id + && current_value != session_ref.value + }, + ); + if replacing_hermes_session && !process_present { + return None; + } let owner_conflicts = self.current_session_owner_conflicts(&source, &agent_label); let foreground_takeover_allowed = owner_conflicts && self.foreground_agent_confirms_different_owner_takeover( @@ -2256,7 +2275,6 @@ mod tests { for (agent, source, label) in [ (Agent::Kimi, "herdr:kimi", "kimi"), (Agent::Kilo, "herdr:kilo", "kilo"), - (Agent::Hermes, "herdr:hermes", "hermes"), ] { let mut terminal = test_terminal(); terminal.set_detected_state(Some(agent), AgentState::Idle); @@ -2291,6 +2309,104 @@ mod tests { } #[test] + fn hermes_session_claim_leaves_state_to_detection() { + let mut terminal = test_terminal(); + terminal.set_detected_state(Some(Agent::Hermes), AgentState::Idle); + let session_ref = crate::agent_resume::AgentSessionRef::id("hermes-root").unwrap(); + + let session = terminal.set_agent_session_ref_for_session_start( + "herdr:hermes".into(), + "hermes".into(), + Some(session_ref.clone()), + Some(10), + Some("startup".into()), + ); + + assert!(session.is_some()); + assert!(terminal.hook_authority.is_none()); + assert_eq!( + terminal + .persisted_agent_session + .as_ref() + .map(|session| &session.session_ref), + Some(&session_ref) + ); + + terminal.set_detected_state(Some(Agent::Hermes), AgentState::Working); + + assert_eq!(terminal.state, AgentState::Working); + assert!(terminal.hook_authority.is_none()); + + let replacement_ref = + crate::agent_resume::AgentSessionRef::id("hermes-replacement").unwrap(); + let replacement = terminal.set_agent_session_ref_for_session_start( + "herdr:hermes".into(), + "hermes".into(), + Some(replacement_ref.clone()), + Some(11), + Some("startup".into()), + ); + + assert!(replacement.is_some()); + assert_eq!(terminal.state, AgentState::Working); + assert!(terminal.hook_authority.is_none()); + assert_eq!( + terminal + .persisted_agent_session + .as_ref() + .map(|session| &session.session_ref), + Some(&replacement_ref) + ); + + let legacy_state = terminal.set_hook_authority_with_session_ref( + "herdr:hermes".into(), + "hermes".into(), + AgentState::Blocked, + None, + Some(replacement_ref.clone()), + Some(12), + ); + assert!(legacy_state.is_none()); + assert_eq!(terminal.state, AgentState::Working); + assert!(terminal.hook_authority.is_none()); + + terminal.set_detected_state(None, AgentState::Unknown); + let background_replacement = terminal.set_agent_session_ref_for_session_start( + "herdr:hermes".into(), + "hermes".into(), + crate::agent_resume::AgentSessionRef::id("hermes-background"), + Some(13), + Some("resume".into()), + ); + assert!(background_replacement.is_none()); + assert_eq!( + terminal + .persisted_agent_session + .as_ref() + .map(|session| &session.session_ref), + Some(&replacement_ref) + ); + + terminal.set_detected_state(Some(Agent::Hermes), AgentState::Idle); + let retried_ref = crate::agent_resume::AgentSessionRef::id("hermes-background").unwrap(); + let retried_replacement = terminal.set_agent_session_ref_for_session_start( + "herdr:hermes".into(), + "hermes".into(), + Some(retried_ref.clone()), + Some(14), + Some("resume".into()), + ); + assert!(retried_replacement.is_some()); + assert_eq!( + terminal + .persisted_agent_session + .as_ref() + .map(|session| &session.session_ref), + Some(&retried_ref) + ); + } + + #[test] fn pi_session_replacement_reports_reanchor_full_lifecycle_authority() { for reason in ["new", "resume", "fork"] { let mut terminal = test_terminal(); @@ -3425,17 +3541,17 @@ mod tests { fn visible_working_does_not_override_full_lifecycle_hook_idle() { let now = Instant::now(); let mut terminal = test_terminal(); - terminal.set_detected_state(Some(Agent::Hermes), AgentState::Idle); + terminal.set_detected_state(Some(Agent::Kimi), AgentState::Idle); anchor_full_lifecycle_session( &mut terminal, - Agent::Hermes, - "herdr:hermes", - "hermes", - crate::agent_resume::AgentSessionRef::id("hermes-root").unwrap(), + Agent::Kimi, + "herdr:kimi", + "kimi", + crate::agent_resume::AgentSessionRef::id("kimi-root").unwrap(), ); terminal.set_hook_authority_at( - "herdr:hermes".into(), - "hermes".into(), + "herdr:kimi".into(), + "kimi".into(), AgentState::Idle, None, None, @@ -3444,7 +3560,7 @@ mod tests { ); let change = terminal.set_detected_state_with_screen_signals_at( - Some(Agent::Hermes), + Some(Agent::Kimi), AgentState::Working, false, false, @@ -5167,20 +5283,20 @@ mod tests { #[test] fn detected_agent_disappearance_does_not_clear_full_lifecycle_hook_session_ref() { let mut terminal = test_terminal(); - terminal.set_detected_state(Some(Agent::Hermes), AgentState::Idle); + terminal.set_detected_state(Some(Agent::Kimi), AgentState::Idle); anchor_full_lifecycle_session( &mut terminal, - Agent::Hermes, - "herdr:hermes", - "hermes", - crate::agent_resume::AgentSessionRef::id("hermes-session").unwrap(), + Agent::Kimi, + "herdr:kimi", + "kimi", + crate::agent_resume::AgentSessionRef::id("kimi-session").unwrap(), ); terminal.set_hook_authority_with_session_ref( - "herdr:hermes".into(), - "hermes".into(), + "herdr:kimi".into(), + "kimi".into(), AgentState::Working, None, - crate::agent_resume::AgentSessionRef::id("hermes-session"), + crate::agent_resume::AgentSessionRef::id("kimi-session"), Some(20), ); @@ -5189,7 +5305,7 @@ mod tests { assert!(!mutation.session_ref_changed); assert!(terminal.hook_authority.is_some()); assert!(terminal.persisted_agent_session.is_none()); - assert_eq!(terminal.effective_agent_label(), Some("hermes")); + assert_eq!(terminal.effective_agent_label(), Some("kimi")); } #[test] diff --git a/website/agent-detection/hermes.toml b/website/agent-detection/hermes.toml index d45fe32a..17542184 100644 --- a/website/agent-detection/hermes.toml +++ b/website/agent-detection/hermes.toml @@ -1,30 +1,102 @@ id = "hermes" -version = "2026.06.10.1" -min_engine_version = 1 -updated_at = "2026-06-10T00:00:00Z" +version = "2026.07.24.1" +min_engine_version = 2 +updated_at = "2026-07-24T19:12:54Z" aliases = ["hermes-agent"] [[rules]] +id = "osc_title_blocked" +state = "blocked" +priority = 1100 +region = "osc_title" +visible_blocker = true +regex = ['^⚠[\u{fe0e}\u{fe0f}]?(?:\s|$)'] + +[[rules]] +id = "osc_title_working" +state = "working" +priority = 1050 +region = "osc_title" +visible_working = true +regex = ['^⏳[\u{fe0e}\u{fe0f}]?(?:\s|$)'] + +[[rules]] id = "dangerous_command_approval" state = "blocked" -priority = 300 -region = "whole_recent" +priority = 900 +region = "bottom_non_empty_lines(14)" +visible_blocker = true +any = [ + { contains = ["dangerous"] }, + { contains = ["approval"] }, + { contains = ["allow once", "deny"] }, + { line_regex = ['(?i)^\s*[▸>]?\s*1\.\s*allow'] }, +] +all = [ + { any = [{ contains = ["enter confirm"] }, { contains = ["enter to confirm"] }, { contains = ["↑/↓ to select"] }, { contains = ["show full command"] }] }, +] + +[[rules]] +id = "clarification_prompt" +state = "blocked" +priority = 900 +region = "bottom_non_empty_lines(14)" +visible_blocker = true +any = [ + { contains = ["hermes needs your"] }, + { line_regex = ['^\s*ask\s+\S'] }, + { contains = ["type your answer"] }, +] +all = [ + { any = [{ contains = ["enter confirm"] }, { contains = ["enter to confirm"] }, { contains = ["enter send"] }, { contains = ["press enter"] }, { contains = ["↑/↓ select"] }, { contains = ["↑/↓ to select"] }, { contains = ["other (type"] }] }, +] + +[[rules]] +id = "credential_prompt" +state = "blocked" +priority = 900 +region = "bottom_non_empty_lines(14)" visible_blocker = true any = [ - { contains = ["dangerous command"] }, - { contains = ["allow once", "allow for this session", "deny"] }, + { contains = ["sudo password"] }, + { contains = ["skill setup"] }, + { contains = ["🔑", "for "] }, ] + +[[rules]] +id = "confirmation_prompt" +state = "blocked" +priority = 900 +region = "bottom_non_empty_lines(14)" +visible_blocker = true all = [ - { any = [{ contains = ["enter to confirm"] }, { contains = ["↑/↓ to select"] }, { contains = ["show full command"] }] }, + { any = [{ contains = ["approve once", "cancel"] }, { contains = ["start a new session", "keep going"] }] }, + { any = [{ contains = ["enter to confirm"] }, { contains = ["enter confirm"] }, { contains = ["type 1/2/3"] }, { contains = ["y/n quick"] }] }, ] [[rules]] id = "interrupt_status_working" state = "working" -priority = 100 -region = "whole_recent" +priority = 950 +region = "bottom_non_empty_lines(5)" visible_working = true any = [ { contains = ["msg=interrupt"] }, - { contains = ["ctrl+c cancel"] }, + { contains = ["ctrl+c to interrupt"] }, ] + +[[rules]] +id = "classic_cancel_working" +state = "working" +priority = 500 +region = "bottom_non_empty_lines(5)" +visible_working = true +contains = ["ctrl+c cancel"] + +[[rules]] +id = "osc_title_idle" +state = "idle" +priority = 100 +region = "osc_title" +visible_idle = true +regex = ['^✓[\u{fe0e}\u{fe0f}]?(?:\s|$)'] |
