From 7b9cc69c2bc2c4e25ac044441de7693eadb939c5 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:08:00 +0800 Subject: [PATCH] style(terminal): align the composer with the design: bottom inset, hairline ring, effort label --- src/terminal/composer.rs | 100 ++++++++++++++++++++++++--------------- src/ui/i18n/en.rs | 3 +- src/ui/i18n/ja.rs | 3 +- src/ui/i18n/mod.rs | 1 - src/ui/i18n/zh.rs | 3 +- 5 files changed, 65 insertions(+), 45 deletions(-) diff --git a/src/terminal/composer.rs b/src/terminal/composer.rs index 9a34be08..b9d3ea05 100644 --- a/src/terminal/composer.rs +++ b/src/terminal/composer.rs @@ -80,6 +80,8 @@ const INPUT_AREA_MAX_ROWS: usize = 40; const TEXT_PX: f32 = 13.5; const INPUT_PAD_X: f32 = 8.; const INPUT_PAD_Y: f32 = 2.; +/// How far the box stands in from the pane's sides and bottom. +const BOX_INSET: f32 = 16.; /// The key the toolbar sends: Shift+Tab cycles the permission mode in every /// agent that has one. @@ -644,6 +646,15 @@ fn model_alias(model: &str, window: Option) -> Option<&'static str> { Some(pair[long as usize]) } +/// How the toolbar spells an effort level: capitalized, as the model names +/// and modes are. +fn effort_label(level: &str) -> String { + match level { + "xhigh" => "Extra high".into(), + _ => level[..1].to_uppercase() + &level[1..], + } +} + /// How the model list spells an alias. fn alias_label(alias: &str) -> String { if alias == "default" { @@ -1315,7 +1326,7 @@ impl TerminalView { // Over the input area, painted in the grid's own background so // what is under it is gone rather than showing through. At least // as tall as the area; taller when the box needs it. The box sits - // at its top, where the agent's own input starts. + // at the bottom of the pane, where a chat's input sits. Some(rows) => div() .absolute() .left_0() @@ -1324,8 +1335,9 @@ impl TerminalView { .min_h(self.line_height * rows as f32 + px(GRID_PAD_Y)) .flex() .flex_col() - .px(px(GRID_PAD_X)) - .pb(px(8.)) + .justify_end() + .px(px(BOX_INSET)) + .pb(px(BOX_INSET)) .bg(cx.theme().background) .occlude() .child(frame) @@ -1334,7 +1346,8 @@ impl TerminalView { .flex_none() .w_full() .pt(px(8.)) - .pb(px(8.)) + .px(px(BOX_INSET - GRID_PAD_X)) + .pb(px(BOX_INSET)) .child(frame) .into_any_element(), }; @@ -1355,9 +1368,9 @@ impl TerminalView { let ink = theme.foreground; let muted = theme.muted_foreground; let amber = theme.warning; - let ring = match focused { - true => ink.opacity(0.22), - false => ink.opacity(0.12), + let (ring, ring_w) = match focused { + true => (ink.opacity(0.22), px(1.)), + false => (ink.opacity(0.15), crate::ui::theme::hairline(window)), }; let menu = self.composer_menu(cx); @@ -1488,17 +1501,18 @@ impl TerminalView { let divider = claude.then(|| { div() .flex_none() - .w(px(1.)) + .w(crate::ui::theme::hairline(window)) .h(px(14.)) .mx(px(4.)) - .bg(ink.opacity(0.12)) + .bg(ink.opacity(0.15)) }); // A toolbar list, opened over its own button. let popup = |picker: Picker, title: L10nKey, rows: Vec<(&'static str, String, bool)>, cx: &Context| { - div() + crate::ui::theme::floating_surface(div(), cx) + .rounded(px(10.)) .absolute() .left_0() .bottom_full() @@ -1508,11 +1522,6 @@ impl TerminalView { .flex() .flex_col() .gap(px(1.)) - .rounded(px(10.)) - .bg(theme.popover) - .border_1() - .border_color(theme.border) - .shadow_md() .text_size(px(13.)) .text_color(ink) .occlude() @@ -1552,11 +1561,10 @@ impl TerminalView { })) })) }; - // A toolbar button that opens a list: the label, a chevron, and the - // list itself when it is open. + // A toolbar button that opens a list over itself. let picker_button = |picker: Picker, id: &'static str, - label: String, + label: gpui::AnyElement, tip: L10nKey, title: L10nKey, rows: Vec<(&'static str, String, bool)>, @@ -1575,12 +1583,7 @@ impl TerminalView { .on_click(cx.listener(move |this, _, window, cx| { this.toggle_picker(picker, window, cx) })) - .child(label) - .child( - Icon::new(IconName::ChevronDown) - .size(px(9.)) - .text_color(ink.opacity(0.4)), - ), + .child(label), ) .when(open, |s| s.child(popup(picker, title, rows, cx))) }; @@ -1597,11 +1600,20 @@ impl TerminalView { .as_deref() .and_then(|m| model_alias(m, readout.context_window)) }); - let label = match (picked, readout.model.as_deref()) { + let name = match (picked, readout.model.as_deref()) { (Some(alias), _) => alias_label(alias), (None, Some(model)) => model_label(model), (None, None) => t(L10nKey::ComposerModel).to_string(), }; + let label = h_flex() + .gap(px(5.)) + .child(name) + .child( + Icon::new(IconName::ChevronDown) + .size(px(8.)) + .text_color(ink.opacity(0.4)), + ) + .into_any_element(); let rows = CLAUDE_MODELS .iter() .map(|&alias| (alias, alias_label(alias), current == Some(alias))) @@ -1618,15 +1630,22 @@ impl TerminalView { }); let current_effort = c.effort.clone().or_else(|| readout.effort.clone()); let effort = claude.then(|| { - let label = match ¤t_effort { - Some(level) => t_fmt(L10nKey::ComposerEffortLevel, &[("level", level)]), - None => t(L10nKey::ComposerEffort).to_string(), - }; + // "Effort" in the toolbar's grey, the level in the text's ink. + 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))), + ) + .into_any_element(); let rows = EFFORT_LEVELS .iter() .map(|&level| { let on = current_effort.as_deref() == Some(level); - (level, level.to_string(), on) + (level, effort_label(level), on) }) .collect(); picker_button( @@ -1717,7 +1736,8 @@ impl TerminalView { '/' => t(L10nKey::ComposerMenuCommands), _ => t(L10nKey::ComposerMenuFiles), }; - div() + crate::ui::theme::floating_surface(div(), cx) + .rounded(px(10.)) .absolute() .left_0() .bottom_full() @@ -1728,11 +1748,6 @@ impl TerminalView { .flex() .flex_col() .gap(px(1.)) - .rounded(px(10.)) - .bg(theme.popover) - .border_1() - .border_color(theme.border) - .shadow_md() .text_size(px(13.)) .child( div() @@ -1912,10 +1927,17 @@ impl TerminalView { .flex_col() .rounded(px(12.)) .bg(ink.opacity(0.035)) - .border_1() + .border_t(ring_w) + .border_b(ring_w) + .border_l(ring_w) + .border_r(ring_w) .border_color(ring) .drag_over::(move |s, _, _, _| { - s.border_color(ink.opacity(0.45)) + s.border_t(px(1.)) + .border_b(px(1.)) + .border_l(px(1.)) + .border_r(px(1.)) + .border_color(ink.opacity(0.48)) }) .children(chips) .child( @@ -2137,6 +2159,8 @@ mod tests { assert_eq!(model_alias("claude-fable-5-1", None), None); assert_eq!(alias_label("sonnet[1m]"), "Sonnet · 1M"); assert_eq!(alias_label("haiku"), "Haiku"); + assert_eq!(effort_label("medium"), "Medium"); + assert_eq!(effort_label("xhigh"), "Extra high"); } #[test] diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index c459b7d2..e0cd63bc 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -1865,8 +1865,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::ComposerModelTip => "Switch model", L10nKey::ComposerModelDefault => "Default (recommended)", L10nKey::ComposerEffort => "Effort", - L10nKey::ComposerEffortLevel => "Effort: {level}", - L10nKey::ComposerEffortTip => "Reasoning effort", + L10nKey::ComposerEffortTip => "Reasoning effort · click to change", L10nKey::ComposerContextTip => "Context {used} / {window}", L10nKey::ComposerPlaceholder => "Message {agent} · @ files / commands", L10nKey::ComposerPlaceholderFiles => "Message {agent} · @ files", diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index 0c080e98..ad6cd1a8 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -1918,8 +1918,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::ComposerModelTip => "モデルを切り替え", L10nKey::ComposerModelDefault => "デフォルト(推奨)", L10nKey::ComposerEffort => "推論の強さ", - L10nKey::ComposerEffortLevel => "強さ: {level}", - L10nKey::ComposerEffortTip => "推論の強さ", + L10nKey::ComposerEffortTip => "推論の強さ · クリックして変更", L10nKey::ComposerContextTip => "コンテキスト {used} / {window}", L10nKey::ComposerPlaceholder => "{agent} にメッセージ · @ ファイル / コマンド", L10nKey::ComposerPlaceholderFiles => "{agent} にメッセージ · @ ファイル", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 18df1740..674e522d 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -1587,7 +1587,6 @@ l10n_keys! { ComposerModelTip, ComposerModelDefault, ComposerEffort, - ComposerEffortLevel, ComposerEffortTip, ComposerContextTip, CmdFindNext, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index e3851b86..5d6dfba8 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -1730,8 +1730,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::ComposerModelTip => "切换模型", L10nKey::ComposerModelDefault => "默认(推荐)", L10nKey::ComposerEffort => "推理强度", - L10nKey::ComposerEffortLevel => "强度:{level}", - L10nKey::ComposerEffortTip => "推理强度", + L10nKey::ComposerEffortTip => "推理强度 · 点击切换", L10nKey::ComposerContextTip => "上下文 {used} / {window}", L10nKey::ComposerPlaceholder => "给 {agent} 发消息 · @ 文件 / 命令", L10nKey::ComposerPlaceholderFiles => "给 {agent} 发消息 · @ 文件",