Skip to content

Commit 0d404bd

Browse files
committed
Release v0.3.0
1 parent c2daf80 commit 0d404bd

22 files changed

Lines changed: 458 additions & 37 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ resolver = "2"
33
members = ["crates/agentmonitor"]
44

55
[workspace.package]
6-
version = "0.2.0"
6+
version = "0.3.0"
77
edition = "2021"
88
license = "MIT"
99
repository = "https://github.com/jiweiyeah/AgentMonitor"

crates/agentmonitor/src/adapter/claude.rs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,19 @@ impl AgentAdapter for ClaudeAdapter {
6161
self.root.iter().cloned().collect()
6262
}
6363
fn matches_process(&self, cmd: &[String], _exe: Option<&Path>) -> bool {
64-
cmd.iter()
65-
.any(|s| s.ends_with("/bin/claude") || s == "claude" || s.contains("/.claude/local/"))
64+
// Cross-platform match: handles Unix paths (`/usr/local/bin/claude`,
65+
// `/.claude/local/...`), bare names (`claude`, `claude.exe`,
66+
// `claude.cmd`), Windows backslash paths (`C:\...\.claude\local\...`),
67+
// and the npm-installed case where `node.exe` runs
68+
// `node_modules\@anthropic-ai\claude-code\cli.js`. The original
69+
// Unix-only matching silently rejected every running agent on
70+
// Windows so the Live Processes panel was always empty.
71+
cmd.iter().any(|s| {
72+
super::process_match::path_ends_with(s, "/bin/claude")
73+
|| super::process_match::bare_name_matches(s, "claude")
74+
|| super::process_match::path_contains(s, "/.claude/local/")
75+
|| super::process_match::path_contains(s, "/@anthropic-ai/claude-code")
76+
})
6677
}
6778

6879
async fn parse_meta_fast(&self, path: &Path) -> Result<SessionMeta> {
@@ -1065,4 +1076,57 @@ mod tests {
10651076
.with_timezone(&Utc);
10661077
assert_eq!(meta.updated_at, Some(expected));
10671078
}
1079+
1080+
#[test]
1081+
fn matches_claude_cli_unix_paths() {
1082+
let adapter = ClaudeAdapter::new(None);
1083+
// Bare invocation (PATH-resolved).
1084+
assert!(adapter.matches_process(&["claude".to_string()], None));
1085+
// Standard /usr/local/bin form.
1086+
assert!(adapter.matches_process(&["/usr/local/bin/claude".to_string()], None));
1087+
// The `~/.claude/local/...` install (volta / nvm-style shim).
1088+
assert!(adapter.matches_process(
1089+
&["/Users/u/.claude/local/node_modules/.bin/claude".to_string()],
1090+
None,
1091+
));
1092+
// Should NOT match unrelated names.
1093+
assert!(!adapter.matches_process(&["claudette".to_string()], None));
1094+
assert!(!adapter.matches_process(&["/usr/bin/codex".to_string()], None));
1095+
}
1096+
1097+
#[test]
1098+
fn matches_claude_cli_windows_paths() {
1099+
// Regression: the Live Processes panel on Windows was always empty
1100+
// because the original Unix-only `s == "claude" || s.ends_with("/bin/claude")`
1101+
// check rejected every Windows-shaped argv entry. These forms are
1102+
// what `sysinfo` actually returns for a running Claude Code on
1103+
// Windows under different install methods.
1104+
let adapter = ClaudeAdapter::new(None);
1105+
// Bare exe (PATH-resolved, native installer drops claude.exe).
1106+
assert!(adapter.matches_process(&["claude.exe".to_string()], None));
1107+
// Common case: npm-installed CLI runs as `node.exe` with the cli.js
1108+
// as argv[1]. The .cmd shim never appears as a long-running process,
1109+
// so we match the @anthropic-ai/claude-code package path instead.
1110+
assert!(adapter.matches_process(
1111+
&[
1112+
"C:\\Program Files\\nodejs\\node.exe".to_string(),
1113+
"C:\\Users\\yjw\\AppData\\Roaming\\npm\\node_modules\\@anthropic-ai\\claude-code\\cli.js".to_string(),
1114+
],
1115+
None,
1116+
));
1117+
// Local `.claude\local\...` install with backslashes (the install
1118+
// script puts the runtime under `~/.claude/local/`).
1119+
assert!(adapter.matches_process(
1120+
&[
1121+
"C:\\Users\\yjw\\.claude\\local\\node.exe".to_string(),
1122+
"C:\\Users\\yjw\\.claude\\local\\node_modules\\@anthropic-ai\\claude-code\\cli.js".to_string(),
1123+
],
1124+
None,
1125+
));
1126+
// Mixed-separator path (some tools normalize even on Windows).
1127+
assert!(adapter.matches_process(
1128+
&["C:/Users/yjw/.claude/local/bin/claude.exe".to_string()],
1129+
None,
1130+
));
1131+
}
10681132
}

crates/agentmonitor/src/adapter/codex.rs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,17 @@ impl CodexAdapter {
4343
}
4444

4545
fn is_codex_cli_process_arg(arg: &str) -> bool {
46-
arg.ends_with("/bin/codex")
47-
|| arg == "codex"
48-
|| arg.contains("/@openai/codex-")
49-
|| arg.contains("/codex-cli/")
46+
// Cross-platform: `path_ends_with` and `path_contains` normalize
47+
// backslashes and strip `.exe`/`.cmd` so Windows `codex.exe`,
48+
// `codex.cmd`, and `C:\...\@openai\codex-...` arguments all match.
49+
// `bare_name_matches` is restricted to no-path forms so the VSCode
50+
// chatgpt extension's bundled `bin/macos-aarch64/codex` "app-server"
51+
// process — which has basename `codex` but lives under a non-`bin`
52+
// parent — still doesn't get scooped up.
53+
super::process_match::path_ends_with(arg, "/bin/codex")
54+
|| super::process_match::bare_name_matches(arg, "codex")
55+
|| super::process_match::path_contains(arg, "/@openai/codex-")
56+
|| super::process_match::path_contains(arg, "/codex-cli/")
5057
}
5158

5259
fn is_codex_desktop_process(path: &Path) -> bool {
@@ -830,4 +837,39 @@ mod tests {
830837

831838
assert!(!adapter.matches_process(&cmd, None));
832839
}
840+
841+
#[test]
842+
fn matches_codex_cli_windows_paths() {
843+
// Regression: Live Processes was empty on Windows because the
844+
// pre-existing patterns required forward-slash separators and a bare
845+
// `codex` literal — neither of which sysinfo emits for a Windows
846+
// npm-installed CLI.
847+
let adapter = CodexAdapter::new(None);
848+
// Bare exe (native installer / PATH-resolved).
849+
assert!(adapter.matches_process(&["codex.exe".to_string()], None));
850+
// npm-installed CLI runs as node.exe with cli.js. The /codex-cli/
851+
// substring catches the @openai/codex-cli package path.
852+
assert!(adapter.matches_process(
853+
&[
854+
"C:\\Program Files\\nodejs\\node.exe".to_string(),
855+
"C:\\Users\\yjw\\AppData\\Roaming\\npm\\node_modules\\@openai\\codex-cli\\bin\\codex.js"
856+
.to_string(),
857+
],
858+
None,
859+
));
860+
// The /@openai/codex- substring also catches alternative package layouts.
861+
assert!(adapter.matches_process(
862+
&[
863+
"C:\\Program Files\\nodejs\\node.exe".to_string(),
864+
"C:\\Users\\yjw\\AppData\\Roaming\\npm\\node_modules\\@openai\\codex-foo\\index.js"
865+
.to_string(),
866+
],
867+
None,
868+
));
869+
// Negative: unrelated exe must not match.
870+
assert!(!adapter.matches_process(
871+
&["C:\\Windows\\System32\\notepad.exe".to_string()],
872+
None,
873+
));
874+
}
833875
}

crates/agentmonitor/src/adapter/gemini.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,13 @@ impl AgentAdapter for GeminiAdapter {
7070
}
7171

7272
fn matches_process(&self, cmd: &[String], _exe: Option<&Path>) -> bool {
73+
// Cross-platform: handle Windows `.exe`/`.cmd` wrappers and
74+
// backslash separators in the npm install path. See
75+
// adapter/process_match.rs for the rationale.
7376
cmd.iter().any(|s| {
74-
s.ends_with("/gemini")
75-
|| s == "gemini"
76-
|| s.contains("/@google/gemini-cli")
77+
super::process_match::path_ends_with(s, "/gemini")
78+
|| super::process_match::bare_name_matches(s, "gemini")
79+
|| super::process_match::path_contains(s, "/@google/gemini-cli")
7780
})
7881
}
7982

@@ -538,6 +541,26 @@ mod tests {
538541
));
539542
}
540543

544+
#[test]
545+
fn test_matches_process_windows() {
546+
// Regression: gemini was invisible in Live Processes on Windows
547+
// because the matchers required `/`-prefixed paths and a bare
548+
// `gemini` literal.
549+
let adapter = GeminiAdapter::new(None);
550+
// Bare exe.
551+
assert!(adapter.matches_process(&["gemini.exe".to_string()], None));
552+
// npm-installed CLI: node.exe runs the cli script. The
553+
// /@google/gemini-cli substring catches the package path.
554+
assert!(adapter.matches_process(
555+
&[
556+
"C:\\Program Files\\nodejs\\node.exe".to_string(),
557+
"C:\\Users\\yjw\\AppData\\Roaming\\npm\\node_modules\\@google\\gemini-cli\\dist\\index.js"
558+
.to_string(),
559+
],
560+
None
561+
));
562+
}
563+
541564
#[test]
542565
fn test_infer_status_active() {
543566
let t = Utc::now() - chrono::Duration::seconds(30);

crates/agentmonitor/src/adapter/hermes.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,15 @@ impl AgentAdapter for HermesAdapter {
105105
}
106106

107107
fn matches_process(&self, cmd: &[String], _exe: Option<&Path>) -> bool {
108+
// Cross-platform: backslash-separator and `.exe`/`.cmd`/`.bat` wrapper
109+
// matching for Windows. The `hermes_cli.main` substring already works
110+
// verbatim on Windows because Python module dotted-paths don't carry
111+
// path separators.
108112
cmd.iter().any(|s| {
109-
s == "hermes"
110-
|| s.ends_with("/hermes")
113+
super::process_match::bare_name_matches(s, "hermes")
114+
|| super::process_match::path_ends_with(s, "/hermes")
111115
|| s.contains("hermes_cli.main")
112-
|| s.contains("hermes_cli/main.py")
116+
|| super::process_match::path_contains(s, "hermes_cli/main.py")
113117
})
114118
}
115119

@@ -738,6 +742,32 @@ mod tests {
738742
assert!(!adapter.matches_process(&["claude".to_string()], None));
739743
}
740744

745+
#[test]
746+
fn matches_hermes_process_windows() {
747+
let adapter = HermesAdapter::new(None);
748+
// Bare exe.
749+
assert!(adapter.matches_process(&["hermes.exe".to_string()], None));
750+
// Windows venv `python.exe -m hermes_cli.main` (the dotted module
751+
// path is identical across OSes).
752+
assert!(adapter.matches_process(
753+
&[
754+
"C:\\Users\\u\\.hermes\\hermes-agent\\venv\\Scripts\\python.exe".to_string(),
755+
"-m".to_string(),
756+
"hermes_cli.main".to_string(),
757+
"chat".to_string(),
758+
],
759+
None,
760+
));
761+
// Windows venv with the script form (`hermes_cli/main.py` substring).
762+
assert!(adapter.matches_process(
763+
&[
764+
"C:\\Users\\u\\.hermes\\hermes-agent\\venv\\Scripts\\python.exe".to_string(),
765+
"C:\\Users\\u\\.hermes\\hermes-agent\\hermes_cli\\main.py".to_string(),
766+
],
767+
None,
768+
));
769+
}
770+
741771
#[test]
742772
fn pick_updated_at_prefers_last_message_then_ended() {
743773
// last_msg > ended > started — last_msg wins.

crates/agentmonitor/src/adapter/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod conversation;
55
pub mod gemini;
66
pub mod hermes;
77
pub mod opencode;
8+
pub(crate) mod process_match;
89
pub mod types;
910

1011
use std::path::Path;

crates/agentmonitor/src/adapter/opencode.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,11 @@ impl AgentAdapter for OpencodeAdapter {
8282
}
8383

8484
fn matches_process(&self, cmd: &[String], _exe: Option<&Path>) -> bool {
85-
cmd.iter()
86-
.any(|s| s == "opencode" || s.contains("/opencode") || s.ends_with("opencode"))
85+
// Cross-platform: handle Windows backslash paths and exe wrappers.
86+
cmd.iter().any(|s| {
87+
super::process_match::bare_name_matches(s, "opencode")
88+
|| super::process_match::path_contains(s, "/opencode")
89+
})
8790
}
8891

8992
fn needs_fs_stat(&self) -> bool {
@@ -619,6 +622,27 @@ mod tests {
619622
assert!(!adapter.matches_process(&["claude".to_string()], None));
620623
}
621624

625+
#[test]
626+
fn matches_opencode_process_windows() {
627+
let adapter = OpencodeAdapter::new(None);
628+
// Bare exe.
629+
assert!(adapter.matches_process(&["opencode.exe".to_string()], None));
630+
// node-launched cli with /opencode/ in the path.
631+
assert!(adapter.matches_process(
632+
&[
633+
"C:\\Program Files\\nodejs\\node.exe".to_string(),
634+
"C:\\Users\\yjw\\AppData\\Roaming\\npm\\node_modules\\opencode\\bin\\opencode.js"
635+
.to_string(),
636+
],
637+
None,
638+
));
639+
// Native installer drop point.
640+
assert!(adapter.matches_process(
641+
&["C:\\Program Files\\opencode\\opencode.exe".to_string()],
642+
None,
643+
));
644+
}
645+
622646
#[test]
623647
fn infer_status_active() {
624648
let now = Some(Utc::now());

0 commit comments

Comments
 (0)