diff options
| author | Ömer Hamid Kamışlı <hamidkamisli@icloud.com> | 2026-08-27 14:30:24 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-27 14:30:24 +0300 |
| commit | 7b675f42af35508eab66ac42fe1598628597a893 (patch) | |
| tree | f67d4a863c11c6967df6f4538b9d455bfc105163 /src | |
| parent | d79fd746a96ddb5642939c9727baefce642d78e6 (diff) | |
feat(detect): add muse agent with generic Pick blocked detection (#2489)
* feat(detect): add muse agent with generic Pick blocked detection
Add Muse agent manifests and runtime detection for idle/working/blocked
states. Blocked Pick is generic on footer 'Enter to select' so any
question title/message triggers, fixing missed waiting answer where
previous AND-gated rule required specific hint combos. Cover process
names muse, muse-bin, muse-code variants and YOLO footer.
refs #herdr
* fix(detect): tighten muse manifest and agent lookup
- manifests: change working_esc_interrupt and blocked_approval
from whole_recent to bottom_non_empty_lines(8) to use captured
bottom-buffer controls instead of incidental whole-pane text
- manifests: anchor idle_prompt line_regex to ^\s*⟩\s*$ so working
chrome "⟩ ..." does not match idle
- runtime: restrict muse lookup to explicit aliases
(muse|muse-bin|muse-code|muse-cli), remove starts_with("muse")
fallback that misclassified museum/muse-helper/muser; add
positive/negative identification tests
- keep website mirror identical; verified with agent_detection
manifest check --require-website
refs #2489
refs #2456
* fix(detect): address mjdouglas vendor findings for muse
- manifests: widen pick_request_blocked to bottom_non_empty_lines(8) and add
Enter to toggle for multi-select; add menu_request_blocked for user-opened
pickers (enter confirm/esc go back/esc close/space toggle); add
workspace_trust_blocked for Do you trust dialog; tighten blocked_approval
to footer chrome Allow once/Always allow/Yes proceed; split idle_prompt
into any gate for empty and typed draft and not-gate pick/menu anchors;
change idle_status_fallback from YOLO to model \u00b7 effort \u00b7 cwd shape
- runtime: replace bare muse-bin alias with muse-bin-<version> digit check
to match versioned launcher binary and avoid museum/muse-binary false
positives
- cross-checked with vendor-side captures reported on PR #2489 against
strings in shipped muse-bin binary
refs #2489
* fix(detect): handle path-qualified muse-bin versioned binary
lookup_agent now uses path_basename before matching so a launcher
exec with absolute argv0 like /usr/local/lib/muse/muse-bin-0.1.0-R708.1
is correctly identified. is_muse_versioned_binary also checks basename
so direct path-qualified names succeed without relying on the
argv0_argv fallback path. Prevents missed Muse detection when screen
state and sound overrides are not applied.
refs #2489
* fix(detect): add enter save to muse menu detection
Evidence documents /theme with enter save footer which was not
matched. Add enter save to menu_request_blocked any list and to
idle_prompt not-gate (highlighted row ⟩ <choice> shares glyph) and
keep website mirror in sync. Update evidence comment counts from
four to five.
refs #2489
* docs: add Muse to supported agents table
Muse detection (manifest, process identification, sound)
was added earlier but docs/next agents table omitted the row.
Add Muse as screen manifest / none (no integration hook,
consistent with Amp/Kiro/Maki) to en/ja/zh-cn translations.
* fix(detect): resize agent arrays for new upstream variant
Agent::ALL and SCREEN_MANIFEST_AGENTS gained an entry from master
but kept stale size annotations, breaking compilation after rebase.
refs #2489
* docs: add muse detection entry to next changelog
refs #2489
* docs: add muse sound override to config reference
refs #2489
* fix(detect): prevent false muse blocked states
refs #2489
---------
Co-authored-by: Omer Hamid Kamisli <ohkamisli@poikus.com>
Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/config/sidebar.rs | 25 | ||||
| -rw-r--r-- | src/config/sound.rs | 3 | ||||
| -rw-r--r-- | src/detect/manifest.rs | 1 | ||||
| -rw-r--r-- | src/detect/manifest/tests.rs | 46 | ||||
| -rw-r--r-- | src/detect/manifests/muse.toml | 113 | ||||
| -rw-r--r-- | src/detect/mod.rs | 46 |
6 files changed, 208 insertions, 26 deletions
diff --git a/src/config/sidebar.rs b/src/config/sidebar.rs index c548c839..f5715889 100644 --- a/src/config/sidebar.rs +++ b/src/config/sidebar.rs @@ -600,30 +600,7 @@ rows = [[{ token = "git_status", fg = "#ff00aa" }], [{ token = "$jj", bold = tru #[test] fn accepts_every_canonical_agent_override_key() { - let agents = [ - Agent::Pi, - Agent::Claude, - Agent::Codex, - Agent::Gemini, - Agent::Cursor, - Agent::Devin, - Agent::Antigravity, - Agent::Cline, - Agent::Omp, - Agent::Mastracode, - Agent::OpenCode, - Agent::GithubCopilot, - Agent::Kimi, - Agent::Kiro, - Agent::Droid, - Agent::Amp, - Agent::Grok, - Agent::Hermes, - Agent::Kilo, - Agent::Qodercli, - Agent::Qwen, - Agent::Maki, - ]; + let agents = Agent::ALL; let entries = agents .iter() .map(|agent| format!("{} = [[\"agent\"]]", crate::detect::agent_label(*agent))) diff --git a/src/config/sound.rs b/src/config/sound.rs index c84f0d59..2215c832 100644 --- a/src/config/sound.rs +++ b/src/config/sound.rs @@ -45,6 +45,7 @@ pub struct AgentSoundOverrides { pub qodercli: AgentSoundSetting, pub qwen: AgentSoundSetting, pub maki: AgentSoundSetting, + pub muse: AgentSoundSetting, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)] @@ -142,6 +143,7 @@ impl AgentSoundOverrides { Some(Agent::Qodercli) => self.qodercli, Some(Agent::Qwen) => self.qwen, Some(Agent::Maki) => self.maki, + Some(Agent::Muse) => self.muse, None => AgentSoundSetting::Default, } } @@ -182,6 +184,7 @@ impl Default for AgentSoundOverrides { qodercli: AgentSoundSetting::Default, qwen: AgentSoundSetting::Default, maki: AgentSoundSetting::Default, + muse: AgentSoundSetting::Default, } } } diff --git a/src/detect/manifest.rs b/src/detect/manifest.rs index 5e226ece..4ef77d2a 100644 --- a/src/detect/manifest.rs +++ b/src/detect/manifest.rs @@ -252,6 +252,7 @@ const BUNDLED_MANIFESTS: &[(&str, &str)] = &[ ("kimi", include_str!("manifests/kimi.toml")), ("kiro", include_str!("manifests/kiro.toml")), ("maki", include_str!("manifests/maki.toml")), + ("muse", include_str!("manifests/muse.toml")), ("opencode", include_str!("manifests/opencode.toml")), ("pi", include_str!("manifests/pi.toml")), ("qodercli", include_str!("manifests/qodercli.toml")), diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs index 82ce52d9..edffa2bb 100644 --- a/src/detect/manifest/tests.rs +++ b/src/detect/manifest/tests.rs @@ -360,6 +360,52 @@ fn devin_manifest_detects_idle_working_and_blocked_states() { } #[test] +fn muse_manifest_requires_complete_live_controls() { + let working = explain( + Agent::Muse, + "⟩ hello\n\n◆ Working (0s · esc to interrupt)\n\n────────────────\n⟩\n────────────────\ngpt-5.4 · minimal · /workspace", + ); + assert_eq!(working.state, AgentState::Working); + assert!(working.visible_working); + + let picker = explain( + Agent::Muse, + "Which option should I use?\n\n› 1. Alpha\n 2. Beta\n\nEnter to select · ↑/↓ to move · Tab for an optional note · Esc to interrupt\n\n────────────────\n⟩\n────────────────\ngpt-5.4 · minimal · /workspace", + ); + assert_eq!(picker.state, AgentState::Blocked); + assert!(picker.visible_blocker); + + let command_approval = explain( + Agent::Muse, + "Would you like to run the following command?\n\n$ printf muse-safe-probe\n\n› 1. Allow this stage once (y)\n 2. Always allow in this workspace: printf muse-safe-probe ... (p)\n 3. Abort the entire command (esc)\n────────────────\ngpt-5.4 · minimal · /workspace", + ); + assert_eq!(command_approval.state, AgentState::Blocked); + assert!(command_approval.visible_blocker); + + let network_approval = explain( + Agent::Muse, + "network: example.com:443 https\nrequested by:\n$ curl -fsS https://example.com\n\n› 1. Yes, proceed (y)\n 2. Yes, don't ask again this session (p) example.com:443 (https)\n 3. No, and tell Muse Code what to do differently (esc)\n────────────────\ngpt-5.4 · minimal · /workspace", + ); + assert_eq!(network_approval.state, AgentState::Blocked); + assert!(network_approval.visible_blocker); + + let menu = explain( + Agent::Muse, + "Theme\n\n⟩ Default (active)\n Dynamic\n\n↑↓ move · enter save · esc go back", + ); + assert_eq!(menu.state, AgentState::Unknown); + assert!(menu.skip_state_update); + assert!(!menu.visible_blocker); + + let ordinary_reply = explain( + Agent::Muse, + "⟩ say the phrase\n\n◆ Yes, proceed\n\n────────────────\n⟩\n────────────────\ngpt-5.4 · minimal · /workspace", + ); + assert_eq!(ordinary_reply.state, AgentState::Idle); + assert!(ordinary_reply.visible_idle); +} + +#[test] fn manifest_validation_rejects_unknown_fields_empty_rules_invalid_regions_and_regexes() { assert!(parse_manifest( r#" diff --git a/src/detect/manifests/muse.toml b/src/detect/manifests/muse.toml new file mode 100644 index 00000000..50818422 --- /dev/null +++ b/src/detect/manifests/muse.toml @@ -0,0 +1,113 @@ +id = "muse" +version = "2026.08.26.1" +min_engine_version = 2 +updated_at = "2026-08-26T00:00:00Z" +aliases = ["muse-code", "muse-cli"] + +# Evidence: live bottom-buffer captures from Muse Code 0.2.1 in Herdr, using a local +# deterministic Responses provider so each UI state could be held and inspected. +# +# Idle has a `⟩` prompt and `model · effort · cwd` footer. Active turns show +# `◆ Working (... · esc to interrupt)` or another activity label with the same interrupt hint. +# +# Structured questions show two co-occurring footer controls: +# Enter to select · ↑/↓ to move · Tab for an optional note · Esc to interrupt +# Multi-select uses `Enter to toggle` instead. The paired controls distinguish a real picker +# from ordinary transcript text that happens to mention one action. +# +# First launch in an untrusted directory shows `Do you trust this workspace?` together with +# `Trust and continue`. This is a real blocker. User-opened `/theme` and `/skills` menus are +# not blockers; their paired footer controls identify overlays whose prior state must be kept. +# +# Muse 0.2.1 command approval shows `Allow this stage once` together with +# `Always allow in this workspace`. Muse 0.1 used `Allow once` with +# `Allow for this session`. Network approval shows `Yes, proceed` together with +# `Yes, don't ask again this session`. Each approval rule requires a pair because Muse can +# emit any one of these phrases as ordinary assistant text after a completed turn. + +[[rules]] +id = "workspace_trust_blocked" +state = "blocked" +priority = 970 +region = "bottom_non_empty_lines(12)" +visible_blocker = true +contains = ["Do you trust this workspace?"] +any = [ + { contains = ["Trust and continue"] }, + { contains = ["Use Up/Down"] }, +] + +[[rules]] +id = "pick_request_blocked" +state = "blocked" +priority = 950 +region = "bottom_non_empty_lines(8)" +visible_blocker = true +any = [ + { contains = ["Enter to select", "Tab for an optional note"] }, + { contains = ["Enter to toggle", "Esc to interrupt"] }, +] + +[[rules]] +id = "menu_overlay" +state = "unknown" +priority = 940 +region = "bottom_non_empty_lines(8)" +skip_state_update = true +any = [ + { contains = ["enter confirm", "esc go back"] }, + { contains = ["enter save", "esc go back"] }, + { contains = ["space toggle", "esc close", "type filter"] }, +] + +[[rules]] +id = "working_esc_interrupt" +state = "working" +priority = 900 +region = "bottom_non_empty_lines(8)" +visible_working = true +contains = ["esc to interrupt"] +not = [ + { contains = ["Enter to select", "Tab for an optional note"] }, + { contains = ["Enter to toggle", "Esc to interrupt"] }, +] + +[[rules]] +id = "blocked_approval" +state = "blocked" +priority = 850 +region = "bottom_non_empty_lines(8)" +visible_blocker = true +any = [ + { contains = ["Allow this stage once", "Always allow in this workspace"] }, + { contains = ["Allow once", "Allow for this session"] }, + { contains = ["Yes, proceed", "Yes, don't ask again this session"] }, +] + +[[rules]] +id = "idle_prompt" +state = "idle" +priority = 700 +region = "bottom_non_empty_lines(5)" +visible_idle = true +any = [ + { line_regex = ['^\s*⟩\s*$'] }, + { line_regex = ['^\s*⟩\s+\S'] }, +] +not = [ + { contains = ["esc to interrupt"] }, + { contains = ["Enter to select", "Tab for an optional note"] }, + { contains = ["Enter to toggle", "Esc to interrupt"] }, + { contains = ["enter confirm", "esc go back"] }, + { contains = ["enter save", "esc go back"] }, + { contains = ["space toggle", "esc close", "type filter"] }, +] + +[[rules]] +id = "idle_status_fallback" +state = "idle" +priority = 500 +region = "bottom_non_empty_lines(3)" +visible_idle = true +line_regex = ['^\s*\S+ · (none|minimal|low|medium|high|xhigh|ultra) · '] +not = [{ contains = ["esc to interrupt"] }] diff --git a/src/detect/mod.rs b/src/detect/mod.rs index def440dc..0f956ee3 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -63,10 +63,11 @@ pub enum Agent { Qodercli, Qwen, Maki, + Muse, } impl Agent { - pub const ALL: [Self; 22] = [ + pub const ALL: [Self; 23] = [ Self::Pi, Self::Claude, Self::Codex, @@ -89,9 +90,10 @@ impl Agent { Self::Qodercli, Self::Qwen, Self::Maki, + Self::Muse, ]; - pub const SCREEN_MANIFEST_AGENTS: [Self; 20] = [ + pub const SCREEN_MANIFEST_AGENTS: [Self; 21] = [ Self::Pi, Self::Claude, Self::Codex, @@ -112,6 +114,7 @@ impl Agent { Self::Qodercli, Self::Qwen, Self::Maki, + Self::Muse, ]; } @@ -139,6 +142,7 @@ pub fn agent_label(agent: Agent) -> &'static str { Agent::Qodercli => "qodercli", Agent::Qwen => "qwen", Agent::Maki => "maki", + Agent::Muse => "muse", } } @@ -172,6 +176,7 @@ pub fn interactive_agent_executable(agent: Agent) -> &'static str { Agent::Qodercli => "qodercli", Agent::Qwen => "qwen", Agent::Maki => "maki", + Agent::Muse => "muse", } } @@ -186,6 +191,7 @@ pub(crate) fn parse_canonical_agent_label(label: &str) -> Option<Agent> { } fn lookup_agent(name: &str) -> Option<Agent> { + let name = path_basename(name); match name { "pi" => Some(Agent::Pi), "claude" | "claude-code" => Some(Agent::Claude), @@ -209,10 +215,25 @@ fn lookup_agent(name: &str) -> Option<Agent> { "qodercli" | "qoderclicn" | "qoder" | "qodercn" => Some(Agent::Qodercli), "qwen" | "qwen-code" | "qwen code" => Some(Agent::Qwen), "maki" => Some(Agent::Maki), + "muse" | "muse-code" | "muse-cli" => Some(Agent::Muse), + _ if is_muse_versioned_binary(name) => Some(Agent::Muse), _ => None, } } +/// Muse's install-dir launcher script resolves the active release and execs +/// `muse-bin-<version>` (e.g. `muse-bin-0.1.0-R708.1`), so the running +/// process never carries a bare `muse`/`muse-bin` alias. Require a digit +/// immediately after the `muse-bin-` prefix so unrelated binaries such as +/// `muse-binary` or a bare `muse-bin` stay unmatched. +/// Accepts path-qualified `argv0` values by checking only the basename, since +/// the launcher may `exec` with an absolute install-dir path. +fn is_muse_versioned_binary(name: &str) -> bool { + path_basename(name) + .strip_prefix("muse-bin-") + .is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_digit())) +} + /// Identify which agent is running from the process name. /// Returns `None` for plain shells or unrecognized programs. pub fn identify_agent(process_name: &str) -> Option<Agent> { @@ -766,6 +787,19 @@ mod tests { assert_eq!(identify_agent("qwen"), Some(Agent::Qwen)); assert_eq!(identify_agent("Qwen Code"), Some(Agent::Qwen)); assert_eq!(identify_agent("maki"), Some(Agent::Maki)); + assert_eq!(identify_agent("muse"), Some(Agent::Muse)); + assert_eq!(identify_agent("muse-code"), Some(Agent::Muse)); + assert_eq!(identify_agent("muse-cli"), Some(Agent::Muse)); + assert_eq!(identify_agent("muse-bin-0.1.0-R708.1"), Some(Agent::Muse)); + assert_eq!(identify_agent("muse-bin-1.2.3"), Some(Agent::Muse)); + assert_eq!( + identify_agent("/home/user/.local/bin/muse-bin-0.2.1-R1215.1"), + Some(Agent::Muse) + ); + assert_eq!( + identify_agent(r"C:\Users\user\muse-bin-0.2.1-R1215.1.exe"), + Some(Agent::Muse) + ); } #[test] @@ -836,6 +870,7 @@ mod tests { (Agent::Qodercli, "qodercli"), (Agent::Qwen, "qwen"), (Agent::Maki, "maki"), + (Agent::Muse, "muse"), ]; assert_eq!(expected.len(), Agent::ALL.len()); for (agent, executable) in expected { @@ -879,6 +914,13 @@ mod tests { assert_eq!(identify_agent("zsh"), None); assert_eq!(identify_agent("vim"), None); assert_eq!(identify_agent("node"), None); + assert_eq!(identify_agent("museum"), None); + assert_eq!(identify_agent("muse-helper"), None); + assert_eq!(identify_agent("muser"), None); + assert_eq!(identify_agent("musescore"), None); + assert_eq!(identify_agent("muse-bin"), None); + assert_eq!(identify_agent("muse-bin-"), None); + assert_eq!(identify_agent("muse-binary"), None); } #[test] |
