aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorÖmer Hamid Kamışlı <hamidkamisli@icloud.com>2026-08-27 14:30:24 +0300
committerGitHub <noreply@github.com>2026-08-27 14:30:24 +0300
commit7b675f42af35508eab66ac42fe1598628597a893 (patch)
treef67d4a863c11c6967df6f4538b9d455bfc105163 /src
parentd79fd746a96ddb5642939c9727baefce642d78e6 (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.rs25
-rw-r--r--src/config/sound.rs3
-rw-r--r--src/detect/manifest.rs1
-rw-r--r--src/detect/manifest/tests.rs46
-rw-r--r--src/detect/manifests/muse.toml113
-rw-r--r--src/detect/mod.rs46
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]