agent: Refine rules library window design (#31994)

Just polishing up a bit the Rules Library design. I think the most
confusing part here was the icon that was being used to tag a rule as
default; I've heard feedback more than once saying that was confusing,
so I'm now switching to a rather standard star icon, which I'd assume is
well-understood as a "favoriting" affordance.

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2025-06-03 14:59:17 -03:00 committed by GitHub
parent dea0a58727
commit e793740168
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 195 additions and 206 deletions

View file

@ -261,6 +261,7 @@ impl PickerDelegate for RulePickerDelegate {
let rule = self.matches.get(ix)?;
let default = rule.default;
let prompt_id = rule.id;
let element = ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
@ -272,9 +273,10 @@ impl PickerDelegate for RulePickerDelegate {
.child(Label::new(rule.title.clone().unwrap_or("Untitled".into()))),
)
.end_slot::<IconButton>(default.then(|| {
IconButton::new("toggle-default-rule", IconName::SparkleFilled)
IconButton::new("toggle-default-rule", IconName::StarFilled)
.toggle_state(true)
.icon_color(Color::Accent)
.icon_size(IconSize::Small)
.shape(IconButtonShape::Square)
.tooltip(Tooltip::text("Remove from Default Rules"))
.on_click(cx.listener(move |_, _, _, cx| {
@ -283,7 +285,7 @@ impl PickerDelegate for RulePickerDelegate {
}))
.end_hover_slot(
h_flex()
.gap_2()
.gap_1()
.child(if prompt_id.is_built_in() {
div()
.id("built-in-rule")
@ -299,8 +301,9 @@ impl PickerDelegate for RulePickerDelegate {
})
.into_any()
} else {
IconButton::new("delete-rule", IconName::Trash)
IconButton::new("delete-rule", IconName::TrashAlt)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.shape(IconButtonShape::Square)
.tooltip(Tooltip::text("Delete Rule"))
.on_click(cx.listener(move |_, _, _, cx| {
@ -309,16 +312,27 @@ impl PickerDelegate for RulePickerDelegate {
.into_any_element()
})
.child(
IconButton::new("toggle-default-rule", IconName::Sparkle)
IconButton::new("toggle-default-rule", IconName::Star)
.toggle_state(default)
.selected_icon(IconName::SparkleFilled)
.selected_icon(IconName::StarFilled)
.icon_color(if default { Color::Accent } else { Color::Muted })
.icon_size(IconSize::Small)
.shape(IconButtonShape::Square)
.tooltip(Tooltip::text(if default {
"Remove from Default Rules"
} else {
"Add to Default Rules"
}))
.map(|this| {
if default {
this.tooltip(Tooltip::text("Remove from Default Rules"))
} else {
this.tooltip(move |window, cx| {
Tooltip::with_meta(
"Add to Default Rules",
None,
"Always included in every thread.",
window,
cx,
)
})
}
})
.on_click(cx.listener(move |_, _, _, cx| {
cx.emit(RulePickerEvent::ToggledDefault { prompt_id })
})),
@ -1008,216 +1022,180 @@ impl RulesLibrary {
.size_full()
.relative()
.overflow_hidden()
.pl(DynamicSpacing::Base16.rems(cx))
.pt(DynamicSpacing::Base08.rems(cx))
.on_click(cx.listener(move |_, _, window, _| {
window.focus(&focus_handle);
}))
.child(
h_flex()
.group("active-editor-header")
.pr(DynamicSpacing::Base16.rems(cx))
.pt(DynamicSpacing::Base02.rems(cx))
.pb(DynamicSpacing::Base08.rems(cx))
.pt_2()
.px_2p5()
.gap_2()
.justify_between()
.child(
h_flex().gap_1().child(
div()
.max_w_80()
.on_action(cx.listener(Self::move_down_from_title))
.border_1()
.border_color(transparent_black())
.rounded_sm()
.group_hover("active-editor-header", |this| {
this.border_color(
cx.theme().colors().border_variant,
)
})
.child(EditorElement::new(
&rule_editor.title_editor,
EditorStyle {
background: cx.theme().system().transparent,
local_player: cx.theme().players().local(),
text: TextStyle {
color: cx
.theme()
.colors()
.editor_foreground,
font_family: settings
.ui_font
.family
.clone(),
font_features: settings
.ui_font
.features
.clone(),
font_size: HeadlineSize::Large
.rems()
.into(),
font_weight: settings.ui_font.weight,
line_height: relative(
settings.buffer_line_height.value(),
),
..Default::default()
},
scrollbar_width: Pixels::ZERO,
syntax: cx.theme().syntax().clone(),
status: cx.theme().status().clone(),
inlay_hints_style:
editor::make_inlay_hints_style(cx),
inline_completion_styles:
editor::make_suggestion_styles(cx),
..EditorStyle::default()
div()
.w_full()
.on_action(cx.listener(Self::move_down_from_title))
.border_1()
.border_color(transparent_black())
.rounded_sm()
.group_hover("active-editor-header", |this| {
this.border_color(cx.theme().colors().border_variant)
})
.child(EditorElement::new(
&rule_editor.title_editor,
EditorStyle {
background: cx.theme().system().transparent,
local_player: cx.theme().players().local(),
text: TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.ui_font.family.clone(),
font_features: settings
.ui_font
.features
.clone(),
font_size: HeadlineSize::Large.rems().into(),
font_weight: settings.ui_font.weight,
line_height: relative(
settings.buffer_line_height.value(),
),
..Default::default()
},
)),
),
scrollbar_width: Pixels::ZERO,
syntax: cx.theme().syntax().clone(),
status: cx.theme().status().clone(),
inlay_hints_style: editor::make_inlay_hints_style(
cx,
),
inline_completion_styles:
editor::make_suggestion_styles(cx),
..EditorStyle::default()
},
)),
)
.child(
h_flex()
.h_full()
.child(
h_flex()
.h_full()
.gap(DynamicSpacing::Base16.rems(cx))
.child(div()),
)
.child(
h_flex()
.h_full()
.gap(DynamicSpacing::Base16.rems(cx))
.children(rule_editor.token_count.map(
|token_count| {
let token_count: SharedString =
token_count.to_string().into();
let label_token_count: SharedString =
token_count.to_string().into();
.flex_shrink_0()
.gap(DynamicSpacing::Base04.rems(cx))
.children(rule_editor.token_count.map(|token_count| {
let token_count: SharedString =
token_count.to_string().into();
let label_token_count: SharedString =
token_count.to_string().into();
h_flex()
.id("token_count")
.tooltip(move |window, cx| {
let token_count =
token_count.clone();
Tooltip::with_meta(
format!(
"{} tokens",
token_count.clone()
),
None,
format!(
"Model: {}",
model
.as_ref()
.map(|model| model
.name()
.0)
.unwrap_or_default()
),
window,
cx,
)
})
.child(
Label::new(format!(
"{} tokens",
label_token_count.clone()
))
.color(Color::Muted),
)
},
))
.child(if prompt_id.is_built_in() {
div()
.id("built-in-rule")
.child(
Icon::new(IconName::FileLock)
.color(Color::Muted),
)
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Built-in rule",
None,
BUILT_IN_TOOLTIP_TEXT,
window,
cx,
)
})
.into_any()
} else {
IconButton::new("delete-rule", IconName::Trash)
.size(ButtonSize::Large)
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |window, cx| {
Tooltip::for_action(
"Delete Rule",
&DeleteRule,
window,
cx,
)
})
.on_click(|_, window, cx| {
window.dispatch_action(
Box::new(DeleteRule),
cx,
);
})
.into_any_element()
div()
.id("token_count")
.mr_1()
.flex_shrink_0()
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Token Estimation",
None,
format!(
"Model: {}",
model
.as_ref()
.map(|model| model.name().0)
.unwrap_or_default()
),
window,
cx,
)
})
.child(
IconButton::new(
"duplicate-rule",
IconName::BookCopy,
)
.size(ButtonSize::Large)
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |window, cx| {
Tooltip::for_action(
"Duplicate Rule",
&DuplicateRule,
window,
cx,
)
})
.on_click(|_, window, cx| {
window.dispatch_action(
Box::new(DuplicateRule),
cx,
);
}),
)
.child(
IconButton::new(
"toggle-default-rule",
IconName::Sparkle,
)
.style(ButtonStyle::Transparent)
.toggle_state(rule_metadata.default)
.selected_icon(IconName::SparkleFilled)
.icon_color(if rule_metadata.default {
Color::Accent
} else {
Color::Muted
})
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(Tooltip::text(
if rule_metadata.default {
"Remove from Default Rules"
} else {
"Add to Default Rules"
},
Label::new(format!(
"{} tokens",
label_token_count.clone()
))
.on_click(|_, window, cx| {
window.dispatch_action(
Box::new(ToggleDefaultRule),
cx,
);
}),
),
.color(Color::Muted),
)
}))
.child(if prompt_id.is_built_in() {
div()
.id("built-in-rule")
.child(
Icon::new(IconName::FileLock)
.color(Color::Muted),
)
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Built-in rule",
None,
BUILT_IN_TOOLTIP_TEXT,
window,
cx,
)
})
.into_any()
} else {
IconButton::new("delete-rule", IconName::TrashAlt)
.icon_size(IconSize::Small)
.tooltip(move |window, cx| {
Tooltip::for_action(
"Delete Rule",
&DeleteRule,
window,
cx,
)
})
.on_click(|_, window, cx| {
window
.dispatch_action(Box::new(DeleteRule), cx);
})
.into_any_element()
})
.child(
IconButton::new("duplicate-rule", IconName::BookCopy)
.icon_size(IconSize::Small)
.tooltip(move |window, cx| {
Tooltip::for_action(
"Duplicate Rule",
&DuplicateRule,
window,
cx,
)
})
.on_click(|_, window, cx| {
window.dispatch_action(
Box::new(DuplicateRule),
cx,
);
}),
)
.child(
IconButton::new("toggle-default-rule", IconName::Star)
.icon_size(IconSize::Small)
.toggle_state(rule_metadata.default)
.selected_icon(IconName::StarFilled)
.icon_color(if rule_metadata.default {
Color::Accent
} else {
Color::Muted
})
.map(|this| {
if rule_metadata.default {
this.tooltip(Tooltip::text(
"Remove from Default Rules",
))
} else {
this.tooltip(move |window, cx| {
Tooltip::with_meta(
"Add to Default Rules",
None,
"Always included in every thread.",
window,
cx,
)
})
}
})
.on_click(|_, window, cx| {
window.dispatch_action(
Box::new(ToggleDefaultRule),
cx,
);
}),
),
),
)
@ -1228,7 +1206,14 @@ impl RulesLibrary {
.on_action(cx.listener(Self::move_up_from_body))
.flex_grow()
.h_full()
.child(rule_editor.body_editor.clone()),
.child(
h_flex()
.py_2()
.pl_2p5()
.h_full()
.flex_1()
.child(rule_editor.body_editor.clone()),
),
),
)
}))