From 63b3951ef3ef848d1d4298b18364f2b7ef3f8031 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:38:40 +0800 Subject: [PATCH] fix(agents): detect Qoder through the binary its docs tell you to run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The npm package installs two binaries. `qoder` is a dispatcher that routes to the CLI for a bare invocation, a flag, or a prompt, and only hands off to the IDE for `ide`/`chat`/`serve-web`/`tunnel` or a path that exists — and it is the one the documentation tells people to run. Both are `#!/usr/bin/env node` scripts, so what the pty carries is node plus the path to the shim; the dispatcher's child, where `qodercli` appears on the path, is not the process group leader and is never read. Detecting `qodercli` alone missed every session started the documented way. An IDE launch now wears the CLI's avatar for as long as the launcher takes to exit, which is the cost of covering the common case. Also: `--session-id` restores a session rather than naming a new one, so say that where the flag is stripped, and assert Qoder has no `Notification` seat instead of putting a payload through a hook map that has none. Claude-Session: https://claude.ai/code/session_01LAqfzqELnoDWU56LBXS1Nh --- crates/tty7-core/src/core/agent_hooks.rs | 18 ++++++--- crates/tty7-core/src/core/cli_agent.rs | 49 ++++++++++++++++++++---- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/crates/tty7-core/src/core/agent_hooks.rs b/crates/tty7-core/src/core/agent_hooks.rs index 6d60c386..e8865107 100644 --- a/crates/tty7-core/src/core/agent_hooks.rs +++ b/crates/tty7-core/src/core/agent_hooks.rs @@ -1758,12 +1758,20 @@ mod tests { r#"{"session_id":"q-1","prompt":"Look up my tickets"}"#, ); assert_eq!(state.status, AgentStatus::Working); - apply_hook( - &mut state, - "Notification", - r#"{"notification_type":"auth_success","message":"Signed in"}"#, + // Qoder says outright when it is blocked — `PermissionRequest` and + // `Elicitation` — so it must not also carry `Notification`, which + // fires for non-blocking alerts and would strand the pane on + // "waiting". Asserting the map has no seat for it is the check; a + // `Notification` payload put through `apply_hook` would be dropped + // for want of one and prove nothing. + assert!( + !HookAgent::QoderCLI + .hook_map_events() + .unwrap() + .iter() + .any(|(hook, _)| *hook == "Notification"), + "an unblocking alert must not read as a question" ); - assert_eq!(state.status, AgentStatus::Working); // The MCP tool is already authorized, so no PermissionRequest precedes // its request for more information from the user. diff --git a/crates/tty7-core/src/core/cli_agent.rs b/crates/tty7-core/src/core/cli_agent.rs index b12431eb..1af51403 100644 --- a/crates/tty7-core/src/core/cli_agent.rs +++ b/crates/tty7-core/src/core/cli_agent.rs @@ -85,8 +85,15 @@ impl CLIAgent { // kimi-cli install a `kimi` — same vendor, same brand, so one // detection covers them. Only the standalone one has hooks. CLIAgent::Kimi => &["kimi", "kimi-code"], - // `qoder` launches the IDE; CLI wrappers can use a custom rule. - CLIAgent::QoderCLI => &["qodercli"], + // The npm package installs two binaries and `qoder` is the one the + // documentation tells people to run: it dispatches to the CLI for a + // bare invocation, a flag, or a prompt, and only hands off to the + // IDE for `ide`/`chat`/`serve-web`/`tunnel` or a path that exists. + // Detecting only `qodercli` would miss every session started the + // documented way, since the dispatcher is what the pty sees. An IDE + // launch is the cost: it wears the CLI's avatar for as long as the + // launcher takes to exit. + CLIAgent::QoderCLI => &["qoder", "qodercli"], } } @@ -407,12 +414,12 @@ impl CLIAgent { "--worktree-ref", "--ref", ], - // `--resume`/`-r` resumes a past session and `--continue`/`-c` the - // most recent one, both of which clash with the `--resume {id}` - // this command appends; `--session-id` names a *new* session and is - // rejected next to `--resume`, and `--fork-session` is the flag the - // fork variant appends itself. `--worktree` would create or switch - // trees again; Qoder's `-w` means `--cwd` and must survive. + // `--resume`/`-r` restores a past session and `--continue`/`-c` the + // most recent one; `--session-id` is a third spelling of the same + // thing. All three clash with the `--resume {id}` this command + // appends, and `--fork-session` is the flag the fork variant + // appends itself. `--worktree` would create or switch trees again; + // Qoder's `-w` means `--cwd` and must survive. CLIAgent::QoderCLI => &[ "--resume", "-r", @@ -880,6 +887,32 @@ mod tests { ); } + /// The npm package installs `qoder` and `qodercli`, and the documentation + /// tells people to run the first one. Both are `#!/usr/bin/env node` + /// scripts, so what the pty carries is node plus the path to the shim — + /// the dispatcher's own child, which is where the name `qodercli` appears + /// on that path, is not the process group leader and is never read. + #[test] + fn qoder_is_detected_through_either_of_its_binaries() { + for launcher in [ + "qoder", + "qodercli", + "/opt/homebrew/bin/qoder", + "/opt/homebrew/bin/qodercli", + ] { + assert_eq!( + CLIAgent::detect_from_argv(&argv(&["node", launcher])), + Some(CLIAgent::QoderCLI), + "on {launcher}" + ); + assert_eq!( + CLIAgent::detect_from_argv(&argv(&[launcher])), + Some(CLIAgent::QoderCLI), + "on {launcher}" + ); + } + } + #[test] fn detects_npx_package_form() { assert_eq!(