style(terminal): align the composer with the design: bottom inset, hairline ring, effort label

This commit is contained in:
l0ng-ai
2026-10-01 15:27:59 +08:00
parent 02c1da9321
commit 7b9cc69c2b
5 changed files with 65 additions and 45 deletions
+62 -38
View File
@@ -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<u64>) -> 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<Self>| {
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 &current_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::<ExternalPaths>(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]
+1 -2
View File
@@ -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",
+1 -2
View File
@@ -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} にメッセージ · @ ファイル",
-1
View File
@@ -1587,7 +1587,6 @@ l10n_keys! {
ComposerModelTip,
ComposerModelDefault,
ComposerEffort,
ComposerEffortLevel,
ComposerEffortTip,
ComposerContextTip,
CmdFindNext,
+1 -2
View File
@@ -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} 发消息 · @ 文件",