Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/next/website/src/content/docs/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Automatic detection works out of the box for common coding agents. The table sho
| Antigravity CLI | screen manifest | session |
| Kiro CLI | screen manifest | none |
| Maki | screen manifest | none |
| Muse | screen manifest | none |

Detected but less thoroughly tested: Gemini CLI and Cline. Unsupported agents still run normally as terminal processes. They just may not get rich state unless you add an integration or report state over the socket API.

Expand Down
1 change: 1 addition & 0 deletions docs/next/website/src/content/docs/ja/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Herdr は複数のコーディングエージェントを同時に動かすた
| Antigravity CLI | スクリーンマニフェスト | セッション |
| Kiro CLI | スクリーンマニフェスト | なし |
| Maki | スクリーンマニフェスト | なし |
| Muse | スクリーンマニフェスト | なし |

検出されるもののテストが薄いもの: Gemini CLI と Cline。未対応のエージェントも通常のターミナルプロセスとして問題なく動きます。ただし、インテグレーションを追加するかソケット API で状態を報告しない限り、詳細な状態は得られない可能性があります。

Expand Down
1 change: 1 addition & 0 deletions docs/next/website/src/content/docs/zh-cn/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Herdr 为同时运行多个编程智能体而生。每个智能体都待在一
| Antigravity CLI | 屏幕清单 | 会话 |
| Kiro CLI | 屏幕清单 | 无 |
| Maki | 屏幕清单 | 无 |
| Muse | 屏幕清单 | 无 |

可检测但测试较少: Gemini CLI 和 Cline。不受支持的智能体仍然可以作为普通终端进程正常运行,只是在你添加集成或通过 socket API 上报状态之前,可能得不到丰富的状态。

Expand Down
3 changes: 3 additions & 0 deletions src/config/sound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub struct AgentSoundOverrides {
pub kilo: AgentSoundSetting,
pub qodercli: AgentSoundSetting,
pub maki: AgentSoundSetting,
pub muse: AgentSoundSetting,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
Expand Down Expand Up @@ -140,6 +141,7 @@ impl AgentSoundOverrides {
Some(Agent::Kilo) => self.kilo,
Some(Agent::Qodercli) => self.qodercli,
Some(Agent::Maki) => self.maki,
Some(Agent::Muse) => self.muse,
None => AgentSoundSetting::Default,
}
}
Expand Down Expand Up @@ -179,6 +181,7 @@ impl Default for AgentSoundOverrides {
kilo: AgentSoundSetting::Default,
qodercli: AgentSoundSetting::Default,
maki: AgentSoundSetting::Default,
muse: AgentSoundSetting::Default,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/detect/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down
168 changes: 168 additions & 0 deletions src/detect/manifests/muse.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
id = "muse"
version = "2026.08.10.1"
min_engine_version = 2
updated_at = "2026-08-10T00:00:00Z"
aliases = ["muse-code", "muse-cli"]

# Evidence: Live pane reads via `herdr pane read <pane> --source detection --format text`
# captured from Muse Code 0.1.0 (muse-spark-1.2-contributor) in herdr, cross-checked with
# vendor-side captures reported by a Muse Code developer on PR #2489 against strings in the
# shipped `muse-bin` binary.
#
# Idle (prompt ready) bottom chrome:
# ── Voice input (⌥ + v to start) ───── ... ─
# ⟩
# ───────────────────────────────────── ... ─
# muse-spark-1.2-contributor · xhigh · ~/Developer/herdr · YOLO
# The prompt is a single `⟩` (U+27E9) on its own line, or `⟩ <draft>` while composing (the
# draft can wrap to continuation lines). `YOLO` only appears when launched with --yolo, so
# idle_status_fallback keys on the invariant `model · effort · cwd` footer shape instead
# (`·` is U+00B7; effort is one of none/minimal/low/medium/high/xhigh/ultra).
#
# Working (agent running tools/commands):
# ◆ Running command · <description> (0s · esc to interrupt)
# └ (ctrl+b to send to background)
# ◇/◆ Finishing up (Ns · esc to interrupt)
# Any live activity line contains `esc to interrupt` (lowercase). This is the invariant
# between idle and working.
#
# Agent-initiated Pick / multi-select (blocked, waiting on the human):
# Enter to select · ↑/↓ to move · Tab for an optional note · Esc to interrupt (single-select)
# Enter to toggle · ↑/↓ to move · ... · Esc to interrupt (multi-select)
# Title/message vary; the footer chrome does not. On narrow panes the footer wraps to two
# lines, and up to four more lines of persistent prompt chrome (voice-input hint, `⟩`, rule
# line, status line) sit below it, so the footer can land as far as the 6th non-empty line
# from the bottom. `working_esc_interrupt`'s not-gate must cover the same region as this rule
# (both bottom_non_empty_lines(8)) since the picker footer's `Esc to interrupt` would
# otherwise satisfy the working rule's `esc to interrupt` contains match (contains is
# case-insensitive; regex/line_regex are not).
#
# Startup workspace-trust dialog (blocked, first launch in an untrusted directory):
# Do you trust this workspace?
# > 1 Trust and continue
# 2 Quit
# Use Up/Down or 1/2, then Enter. Esc quits.
# No footer chrome (no `esc to interrupt`/`Enter to select`), so without a dedicated rule
# this falls through to the known-agent idle fallback while the agent is actually blocked.
#
# User-opened menus (blocked by convention here; the agent isn't waiting on an answer, but
# the pane needs input before anything else can happen): `/model`, `/theme`, `/export`,
# `/subagents`, `/skills`, etc. Footer verb varies by menu — captured live: `/model` says
# `enter confirm`, `/theme` says `enter save`, `/skills` (a filterable checkbox list) says
# `enter use - space toggle - ... - esc close - type filter`. "Enter to select"/"Esc to
# interrupt" never appear. Matched on whichever of "enter confirm" / "enter save" / "esc go
# back" / "esc close" / "space toggle" is present rather than trying to enumerate every verb;
# more menu types than these five captures likely exist. The highlighted row in both `/model`
# and `/skills` also renders as `⟩ <choice>`, the same glyph idle_prompt's typed-draft variant
# matches, so idle_prompt's not-gate excludes all five menu anchors too, even though
# menu_request_blocked's higher priority already wins that race today.
#
# Tool approval (no --approval-judge, or judge declines to auto-allow): footer gains
# acceptance hints `Allow once` / `Allow for this session` / `Always allow in this workspace`.
# Anchored on this footer chrome rather than sentence prose ("Do you want to proceed?",
# "waiting for approval") because Muse will quote that prose back in its own replies and tool
# output, which would false-positive blocked right after a normal turn.
#
# Network-access approval is a distinct captured variant (live repro, `--approval-judge off`,
# a shell command doing an HTTPS fetch): title `Would you like to allow this network access?`,
# options `Yes, proceed (y)` / `Yes, don't ask again this session (p)` / `No, and tell Muse
# Code what to do differently (esc)`, footer `Press enter to confirm or esc to cancel`. No
# "esc to interrupt" here (esc cancels, not interrupts), and no "Allow"/footer text from the
# tool-approval variant, so without `blocked_approval` covering it this fell through to
# idle_status_fallback. The title itself is a bad anchor: with a `requested by: $ <command>`
# body line it landed as the 10th non-empty line from the bottom, past this rule's own
# bottom_non_empty_lines(8) region (the same wrap/offset trap as the Pick footer above).
# Matched on the footer option `Yes, proceed` instead, which stays a fixed few lines from the
# true bottom regardless of how long the request body gets.

[[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"] },
{ contains = ["Enter to toggle"] },
]

[[rules]]
id = "menu_request_blocked"
state = "blocked"
priority = 940
region = "bottom_non_empty_lines(8)"
visible_blocker = true
any = [
{ contains = ["enter confirm"] },
{ contains = ["enter save"] },
{ contains = ["esc go back"] },
{ contains = ["esc close"] },
{ contains = ["space toggle"] },
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

[[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"] },
{ contains = ["Enter to toggle"] },
]

[[rules]]
id = "blocked_approval"
state = "blocked"
priority = 850
region = "bottom_non_empty_lines(8)"
visible_blocker = true
any = [
{ contains = ["Allow once"] },
{ contains = ["Allow for this session"] },
{ contains = ["Always allow in this workspace"] },
{ contains = ["Yes, proceed"] },
]

[[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"] },
{ contains = ["Enter to toggle"] },
{ contains = ["enter confirm"] },
{ contains = ["enter save"] },
{ contains = ["esc go back"] },
{ contains = ["esc close"] },
{ contains = ["space toggle"] },
]

[[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"] }]
38 changes: 36 additions & 2 deletions src/detect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ pub enum Agent {
Kilo,
Qodercli,
Maki,
Muse,
}

impl Agent {
pub const ALL: [Self; 21] = [
pub const ALL: [Self; 22] = [
Self::Pi,
Self::Claude,
Self::Codex,
Expand All @@ -87,9 +88,10 @@ impl Agent {
Self::Kilo,
Self::Qodercli,
Self::Maki,
Self::Muse,
];

pub const SCREEN_MANIFEST_AGENTS: [Self; 19] = [
pub const SCREEN_MANIFEST_AGENTS: [Self; 20] = [
Self::Pi,
Self::Claude,
Self::Codex,
Expand All @@ -109,6 +111,7 @@ impl Agent {
Self::Kilo,
Self::Qodercli,
Self::Maki,
Self::Muse,
];
}

Expand All @@ -135,6 +138,7 @@ pub fn agent_label(agent: Agent) -> &'static str {
Agent::Kilo => "kilo",
Agent::Qodercli => "qodercli",
Agent::Maki => "maki",
Agent::Muse => "muse",
}
}

Expand Down Expand Up @@ -167,6 +171,7 @@ pub fn interactive_agent_executable(agent: Agent) -> &'static str {
Agent::Kilo => "kilo",
Agent::Qodercli => "qodercli",
Agent::Maki => "maki",
Agent::Muse => "muse",
}
}

Expand All @@ -181,6 +186,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),
Expand All @@ -203,10 +209,25 @@ fn lookup_agent(name: &str) -> Option<Agent> {
"kilo" | "kilo-code" | "kilo code" => Some(Agent::Kilo),
"qodercli" | "qoderclicn" | "qoder" | "qodercn" => Some(Agent::Qodercli),
"maki" => Some(Agent::Maki),
"muse" | "muse-code" | "muse-cli" => Some(Agent::Muse),
_ if is_muse_versioned_binary(name) => Some(Agent::Muse),
Comment thread
greptile-apps[bot] marked this conversation as resolved.
_ => 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> {
Expand Down Expand Up @@ -710,6 +731,11 @@ mod tests {
assert_eq!(identify_agent("kilo"), Some(Agent::Kilo));
assert_eq!(identify_agent("kilo-code"), Some(Agent::Kilo));
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));
}

#[test]
Expand Down Expand Up @@ -778,6 +804,7 @@ mod tests {
(Agent::Kilo, "kilo"),
(Agent::Qodercli, "qodercli"),
(Agent::Maki, "maki"),
(Agent::Muse, "muse"),
];
assert_eq!(expected.len(), Agent::ALL.len());
for (agent, executable) in expected {
Expand Down Expand Up @@ -820,6 +847,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]
Expand Down
4 changes: 4 additions & 0 deletions website/agent-detection/index.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ path = "kiro.toml"
id = "maki"
path = "maki.toml"

[[agents]]
id = "muse"
path = "muse.toml"

[[agents]]
id = "opencode"
path = "opencode.toml"
Expand Down
Loading
Loading