diff --git a/crates/tty7-core/src/core/agent_hooks.rs b/crates/tty7-core/src/core/agent_hooks.rs index 1cdcac26..bedad4bc 100644 --- a/crates/tty7-core/src/core/agent_hooks.rs +++ b/crates/tty7-core/src/core/agent_hooks.rs @@ -214,12 +214,34 @@ fn claude_readout(payload: &serde_json::Value, body: &mut serde_json::Value) { if let Some(effort) = std::env::var("CLAUDE_CODE_EFFORT_LEVEL") .ok() .filter(|e| !e.is_empty()) - .or_else(|| str_of(settings.get("effortLevel"))) + .or_else(|| configured_effort(&settings, model.as_deref())) { body["effort"] = effort.into(); } } +/// The effort level Claude's settings give a model: the one set for that +/// model if there is one, the general one otherwise. +fn configured_effort(settings: &serde_json::Value, model: Option<&str>) -> Option { + let level = |v: Option<&serde_json::Value>| { + v.and_then(|v| v.get("effortLevel")) + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + .map(str::to_string) + }; + let per_model = settings.get("modelSettings"); + model + .and_then(|m| { + let per_model = per_model?; + level( + per_model + .get(m) + .or_else(|| per_model.get(m.trim_end_matches("[1m]"))), + ) + }) + .or_else(|| level(Some(settings))) +} + fn claude_settings() -> serde_json::Value { let dir = std::env::var_os("CLAUDE_CONFIG_DIR") .filter(|d| !d.is_empty()) @@ -2103,6 +2125,28 @@ mod tests { ); } + #[test] + fn an_effort_set_for_the_model_wins_over_the_general_one() { + let settings = serde_json::json!({ + "effortLevel": "high", + "modelSettings": { "claude-opus-5-5": { "effortLevel": "medium" } }, + }); + assert_eq!( + configured_effort(&settings, Some("claude-opus-5-5")).as_deref(), + Some("medium") + ); + assert_eq!( + configured_effort(&settings, Some("claude-opus-5-5[1m]")).as_deref(), + Some("medium") + ); + assert_eq!( + configured_effort(&settings, Some("claude-sonnet-5-5")).as_deref(), + Some("high") + ); + assert_eq!(configured_effort(&settings, None).as_deref(), Some("high")); + assert_eq!(configured_effort(&serde_json::json!({}), None), None); + } + #[test] fn other_agents_carry_no_readout() { let stdin = r#"{"session_id":"s","permission_mode":"plan"}"#; diff --git a/src/terminal/composer.rs b/src/terminal/composer.rs index f95e7dca..6e22ce82 100644 --- a/src/terminal/composer.rs +++ b/src/terminal/composer.rs @@ -1526,6 +1526,10 @@ impl TerminalView { .left_0() .bottom_full() .mb(px(8.)) + .on_mouse_down(MouseButton::Left, |_, window, cx| { + window.prevent_default(); + cx.stop_propagation(); + }) .min_w(px(180.)) .p(px(5.)) .flex() @@ -1582,6 +1586,16 @@ impl TerminalView { div() .relative() .flex_none() + // A click anywhere else closes the list, as a menu's would. + // The button and the list stop their own clicks short of this. + .when(open, |s| { + s.on_mouse_down_out(cx.listener(|this, _: &MouseDownEvent, _w, cx| { + if let Some(c) = this.composer.as_mut() { + c.picker = None; + } + cx.notify(); + })) + }) .child( tool(id) .gap(px(5.)) @@ -1643,16 +1657,15 @@ impl TerminalView { }); let current_effort = c.effort.clone().or_else(|| readout.effort.clone()); let effort = claude.then(|| { - // "Effort" in the toolbar's grey, the level in the text's ink. + // The level alone, as the model button shows the model alone; + // the tooltip and the list's title say what it is. + let name = match current_effort.as_deref() { + Some(level) => effort_label(level), + None => t(L10nKey::ComposerEffort).to_string(), + }; let label = h_flex() .gap(px(5.)) - .text_color(ink.opacity(0.45)) - .child(t(L10nKey::ComposerEffort)) - .children( - current_effort - .as_deref() - .map(|level| div().text_color(ink).child(effort_label(level))), - ) + .child(name) .child( Icon::new(IconName::ChevronDown) .size(px(8.))