From 81f25c7759e34378a097e7a72578c3d5ab3a2d96 Mon Sep 17 00:00:00 2001 From: Frederic Lavigne Date: Fri, 14 Aug 2026 14:35:50 -0500 Subject: [PATCH] support for IBM Bob --- .../website/src/data/config-reference.json | 13 +- src/config/model.rs | 2 +- src/config/sidebar.rs | 1 + src/config/sound.rs | 3 + src/detect/manifest.rs | 1 + src/detect/manifests/bob.toml | 129 ++++++++++++++++++ src/detect/mod.rs | 16 ++- src/main.rs | 2 +- 8 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 src/detect/manifests/bob.toml diff --git a/docs/next/website/src/data/config-reference.json b/docs/next/website/src/data/config-reference.json index c16db7a193..43681bfe2c 100644 --- a/docs/next/website/src/data/config-reference.json +++ b/docs/next/website/src/data/config-reference.json @@ -1126,6 +1126,17 @@ "on", "off" ] + }, + { + "key": "ui.sound.agents.bob", + "type": "enum", + "default": "\"default\"", + "description": "Sound override for detected Bob Shell agents.", + "values": [ + "default", + "on", + "off" + ] } ] }, @@ -1209,7 +1220,7 @@ "key": "experimental.cjk_ime_agents", "type": "list of strings", "default": "[]", - "description": "Restrict `reveal_hidden_cursor_for_cjk_ime` to focused panes whose detected agent matches one of these names (case-insensitive). Empty list means apply to any focused pane. Unknown agent names are ignored; if the list contains no valid names, the reveal does not apply. Accepted names: pi, claude, codex, gemini, cursor, devin, cline, opencode, copilot, kimi, kiro, droid, amp, grok, hermes, kilo, qodercli, qoder, qwen, qwen-code, maki." + "description": "Restrict `reveal_hidden_cursor_for_cjk_ime` to focused panes whose detected agent matches one of these names (case-insensitive). Empty list means apply to any focused pane. Unknown agent names are ignored; if the list contains no valid names, the reveal does not apply. Accepted names: pi, claude, codex, gemini, cursor, devin, cline, opencode, copilot, kimi, kiro, droid, amp, grok, hermes, kilo, qodercli, qoder, qwen, qwen-code, maki, bob." }, { "key": "experimental.cjk_ime_cursor_shape", diff --git a/src/config/model.rs b/src/config/model.rs index 6d6083ad77..70721567be 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -989,7 +989,7 @@ pub struct ExperimentalConfig { /// if the list contains no valid names, the reveal does not apply. /// Accepted names: pi, claude, codex, gemini, cursor, devin, cline, /// opencode, copilot, kimi, kiro, droid, amp, grok, hermes, kilo, - /// qodercli, qoder, qwen, qwen-code, maki. + /// qodercli, qoder, qwen, qwen-code, maki, bob. /// Default: empty. pub cjk_ime_agents: Vec, /// Cursor shape rendered for the IME anchor when diff --git a/src/config/sidebar.rs b/src/config/sidebar.rs index c548c839b2..8db3b8d773 100644 --- a/src/config/sidebar.rs +++ b/src/config/sidebar.rs @@ -623,6 +623,7 @@ rows = [[{ token = "git_status", fg = "#ff00aa" }], [{ token = "$jj", bold = tru Agent::Qodercli, Agent::Qwen, Agent::Maki, + Agent::Bob, ]; let entries = agents .iter() diff --git a/src/config/sound.rs b/src/config/sound.rs index c84f0d59c9..8f8025e87c 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 bob: 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::Bob) => self.bob, None => AgentSoundSetting::Default, } } @@ -182,6 +184,7 @@ impl Default for AgentSoundOverrides { qodercli: AgentSoundSetting::Default, qwen: AgentSoundSetting::Default, maki: AgentSoundSetting::Default, + bob: AgentSoundSetting::Default, } } } diff --git a/src/detect/manifest.rs b/src/detect/manifest.rs index b9fe786a08..7479a2c7b9 100644 --- a/src/detect/manifest.rs +++ b/src/detect/manifest.rs @@ -239,6 +239,7 @@ fn default_region() -> String { const BUNDLED_MANIFESTS: &[(&str, &str)] = &[ ("amp", include_str!("manifests/amp.toml")), ("agy", include_str!("manifests/antigravity.toml")), + ("bob", include_str!("manifests/bob.toml")), ("claude", include_str!("manifests/claude.toml")), ("cline", include_str!("manifests/cline.toml")), ("codex", include_str!("manifests/codex.toml")), diff --git a/src/detect/manifests/bob.toml b/src/detect/manifests/bob.toml new file mode 100644 index 0000000000..2c74ed59be --- /dev/null +++ b/src/detect/manifests/bob.toml @@ -0,0 +1,129 @@ +id = "bob" +version = "2026.08.14.5" +min_engine_version = 1 +updated_at = "2026-08-14T00:00:00Z" +aliases = ["bobshell", "bob-shell"] + +# Evidence: bobshell 2.0.1 (release 2026-08-13) source analysis and live UI inspection. +# +# Bob Shell (IBM) renders a React/Ink TUI with a chat-style interface. +# The executable is `bob` (from the `bobshell` npm package, bin: bob.js). +# +# Working state: +# When a task is running/streaming, the bottom of the chat shows a dots spinner +# followed by one of: +# "Processing… (Enter to steer, Tab to queue)" — normal task mode +# "Processing…" — editing queued message mode +# The spinner uses Ink's built-in "dots" type. +# +# Blocked — action approval dialogs (live evidence): +# Two variants share the same options/footer structure: +# +# Command execution: +# Title: "Execute Command" +# Section: "Approve commands:" +# Options: "→ Approve Once" / "Always Allow Command for task" / "Reject" +# Footer: "Press Enter to confirm" +# Extra: "Wait for input after execution: No (Tab to toggle)" +# +# Skill use: +# Title: "Use Skill " +# Options: "→ Approve Once" / "Approve skill tools for task" / "Reject" +# Footer: "Press Enter to confirm" +# Extra: "Wait for input after execution: No (Tab to toggle)" +# +# Invariants across both: "approve once", "reject", "press enter to confirm" +# +# Blocked — tool use approval (older inline style): +# Bob presents tool approval options inline: +# "Allow once" "Always allow" "Reject" "Always reject" +# with footer "Enter to select · Esc to cancel" +# +# Blocked — folder trust prompt (on first run in a directory): +# Title: "Do you trust this folder?" +# Options: "Trust folder (name)", "Trust parent folder (name)", "Don't trust" +# Footer: "Press ESC or CTRL+C to exit" +# +# Blocked — task limit / cost limit error banners: +# "Task limit reached" with "Continue to double the limit" +# "Cost limit reached" with "Continue to double the limit" +# +# Blocked — content filter: +# "Request Blocked" shown in the chat +# +# Idle: +# No reliable single idle indicator exists beyond absence of working/blocked +# signals. The manifest relies on the working/blocked rules exhausting first +# and the known-agent fallback to idle. + +[[rules]] +id = "processing_working" +state = "working" +priority = 880 +region = "bottom_non_empty_lines(3)" +visible_working = true +contains = ["Processing…"] +not = [ + { contains = ["editing queued message"] }, +] + +[[rules]] +id = "processing_steer_hint_working" +state = "working" +priority = 860 +region = "whole_recent" +visible_working = true +contains = ["Processing…", "Enter to steer"] + +[[rules]] +id = "approval_dialog_blocked" +state = "blocked" +priority = 820 +region = "whole_recent" +visible_blocker = true +contains = ["approve once", "press enter to confirm"] + +[[rules]] +id = "tool_approval_footer_blocked" +state = "blocked" +priority = 800 +region = "whole_recent" +visible_blocker = true +contains = ["enter to select"] +any = [ + { contains = ["allow once"] }, + { contains = ["always allow"] }, + { contains = ["reject"] }, +] + +[[rules]] +id = "trust_folder_blocked" +state = "blocked" +priority = 780 +region = "whole_recent" +visible_blocker = true +contains = ["do you trust this folder?"] +any = [ + { contains = ["trust folder"] }, + { contains = ["don't trust"] }, +] + +[[rules]] +id = "task_limit_blocked" +state = "blocked" +priority = 750 +region = "whole_recent" +visible_blocker = true +contains = ["continue to double the limit"] +any = [ + { contains = ["task limit reached"] }, + { contains = ["cost limit reached"] }, +] + +[[rules]] +id = "request_blocked" +state = "blocked" +priority = 700 +region = "whole_recent" +visible_blocker = true +contains = ["request blocked", "content filter"] diff --git a/src/detect/mod.rs b/src/detect/mod.rs index f78567f6ac..80800f5d0d 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -63,10 +63,11 @@ pub enum Agent { Qodercli, Qwen, Maki, + Bob, } 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::Bob, ]; - 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::Bob, ]; } @@ -139,6 +142,7 @@ pub fn agent_label(agent: Agent) -> &'static str { Agent::Qodercli => "qodercli", Agent::Qwen => "qwen", Agent::Maki => "maki", + Agent::Bob => "bob", } } @@ -172,6 +176,7 @@ pub fn interactive_agent_executable(agent: Agent) -> &'static str { Agent::Qodercli => "qodercli", Agent::Qwen => "qwen", Agent::Maki => "maki", + Agent::Bob => "bob", } } @@ -209,6 +214,7 @@ fn lookup_agent(name: &str) -> Option { "qodercli" | "qoderclicn" | "qoder" | "qodercn" => Some(Agent::Qodercli), "qwen" | "qwen-code" | "qwen code" => Some(Agent::Qwen), "maki" => Some(Agent::Maki), + "bob" | "bobshell" | "bob-shell" => Some(Agent::Bob), _ => None, } } @@ -734,6 +740,8 @@ 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("bob"), Some(Agent::Bob)); + assert_eq!(identify_agent("bobshell"), Some(Agent::Bob)); } #[test] @@ -761,6 +769,9 @@ mod tests { assert_eq!(parse_agent_label("qwen-code"), Some(Agent::Qwen)); assert_eq!(parse_agent_label("maki"), Some(Agent::Maki)); assert_eq!(parse_agent_label("kilo-code"), Some(Agent::Kilo)); + assert_eq!(parse_agent_label("bob"), Some(Agent::Bob)); + assert_eq!(parse_agent_label("bobshell"), Some(Agent::Bob)); + assert_eq!(parse_agent_label("bob-shell"), Some(Agent::Bob)); } #[test] @@ -804,6 +815,7 @@ mod tests { (Agent::Qodercli, "qodercli"), (Agent::Qwen, "qwen"), (Agent::Maki, "maki"), + (Agent::Bob, "bob"), ]; assert_eq!(expected.len(), Agent::ALL.len()); for (agent, executable) in expected { diff --git a/src/main.rs b/src/main.rs index 8505e5cc28..9be44ae154 100644 --- a/src/main.rs +++ b/src/main.rs @@ -452,7 +452,7 @@ pane_history = false # If the list contains no valid names, the reveal does not apply. # Accepted: pi, claude, codex, gemini, cursor, devin, cline, opencode, # copilot, kimi, kiro, droid, amp, grok, hermes, kilo, qodercli, qoder, qwen, -# qwen-code, maki. +# qwen-code, maki, bob. # cjk_ime_agents = [] # Cursor shape rendered when reveal_hidden_cursor_for_cjk_ime is true. # Values: block, steady_block (default), underline, steady_underline, bar, steady_bar.